6.5K Views
August 22, 24
スライド概要
VideoToolBoxは、AVPlayerなどが属するAVFoundationのさらに低位のレイヤーで、動画の圧縮(エンコード)や伸張(デコード)を行います。AVPlayerは非常に優秀なため、ほとんどの場合、VideoToolBoxを直接利用する必要はありません。しかし、MPEGの構造を理解し、VideoToolBoxを使って特殊再生を行うことで、AVPlayerがどれほど複雑な処理を行っているかを垣間見ることができます。
このトークでは、カメラで撮影した映像ファイルをVideoToolBoxで特殊再生する方法を解説します。
・PTS (Presentation Time Stamp) と同期処理の基礎:
PTSの役割と、時間同期ついて説明します。
・GOP (Group of Pictures) の基礎:
Iフレーム、Pフレーム、Bフレームの圧縮方式とその制約について説明します。
・特殊再生の実現方法:
スキップ、シーク、倍速再生などの特殊再生を行うために、どのようにVideoToolBoxをどう利用すれば良いのかを具体的なコードを交えて説明します。
このトークを通じて、参加者はMPEGフォーマットの重要な仕組みを理解し、VideoToolBoxを用いた特殊再生の実装方法について学ぶことができます。
株式会社ゆめみ 所属 iOSエンジニア
MPEG構造を把握し、 VideoToolBoxを使った 特殊再生の実装方法を理解する k2moons 2024/8/22
自己紹介 k2moons / BlueEventHorizon 株式会社ゆめみ 現在:iOSエンジニア 昔々:ハードウェアエンジニア・組み込みエンジ ニア GitHub : BlueEventHorizon
本日おはなしする内容 https://developer.apple.com/jp/videos/play/wwdc2022/110565/ より VideoToolBoxフレームワークを使って 同じ様に動画を再生しようとすると、 どのくらい面倒くさくなるのか?
本日おはなしする内容 AVPlayer VideoToolBox https://developer.apple.com/jp/videos/play/wwdc2022/110565/ より
AVPlayerにできること をおさらい
AVPlayerにできること 再生対象 ・アルバムの動画ファイルを再生 ・URLから動画ストリームを再生
アルバムの動画ファイルを再生 再生制御 再生 ポーズ 10秒送り 10秒戻し 早送り 早戻し
アルバムの動画ファイルを再生 再生制御 再生 ポーズ 10秒送り 10秒戻し 早送り 早戻し 特殊再生
AVPlayerの実装 struct VideoPlayerViewX: View { @State var player: AVPlayer? let url: URL var body: some View { VStack { VideoPlayer(player: player) } .onAppear { Task { player = AVPlayer( playerItem: AVPlayerItem(asset: AVURLAsset(url: url)) ) player?.play() } } .onDisappear { player?.pause() player = nil } } }
MPEGってなんでしょう 動画や音声の高能率符号化 技術のひとつ 高能率符号化 = 品質を保ったまま、めっちゃ圧縮 して小さくすることで、データ量を極限まで削減す るための符号化技術
MPEGってなんでしょう JPEG MPEG-1 1992年 1993年 MPEG-2 1995年 MPEG-4 1999年 MPEG-4 (AVC) MPEG-H 2003年 2013年 静止画の圧縮標準。写真や画像の圧縮に広く使われ、カメラやインターネット上の 画像形式として標準となりました 音声とビデオのデジタル圧縮規格。ビデオCDで使用され、特にMP3形式が音楽配 信で広く普及しました。 高解像度のビデオを扱う規格。DVDやデジタルテレビ放送で標準となり、現在も利 用されています。 インターネット配信やモバイルデバイス向けの規格。 H.264(MPEG-4 Part 10, AVC)は、現在のビデオストリーミングやBlu-rayで 広く採用されています。 次世代の映像・音響体験を目指した規格。HEVC (H.265) は、4K/8Kビデオに対応 する高効率なビデオ圧縮技術。
MPEGってなんでしょう MPEG == Moving Picture Experts Group JPEG == Joint Photographic Experts Group
MPEG の技術構成 • 動画コーデック(圧縮伸長方式) • 音声コーデック(圧縮伸長方式) • コンテナ(システム) + • 通信プロトコル
動画コーデック(圧縮伸長方式) H.264 (MPEG-4 Part 10) / MPEG-4 AVC iPhone/ Androidカメラ、Blu-ray、ストリーミングサービス H.265 / HEVC iPhone/ Androidカメラ、Blu-ray、ストリーミングサービス MPEG2 Video DVD、デジタルTV放送 VP9 Youtube、ブラウザ
MPEG の技術構成 • 動画コーデック(圧縮伸長方式) • 音声コーデック(圧縮伸長方式) • コンテナ(システム) + • 通信プロトコル
音声コーデック(圧縮伸長方式) AAC iPhone/ Androidカメラ、Blu-ray、ストリーミングサービス MP3 音楽配信、ポッドキャスト Opus VoIP、動画会議 ALAC(Apple Lossless Audio Codec) Apple Music
MPEG の技術構成 • 動画コーデック(圧縮伸長方式) • 音声コーデック(圧縮伸長方式) • コンテナ(システム) + • 通信プロトコル
コンテナ(システム) MP4 (MPEG-4 Part 14) 主流のコンテナ、H.264、H.265、AAC、MP3 MOV H.264、ProRes、AAC WMV WMV、WMA、Windowsで利用される
コンテナ(システム) コンテナ: MP4 コンテナ: MP4 動画コーデック: H.264 動画コーデック: H.265 音声コーデック: AAC 音声コーデック: AAC
コンテナ(システム) コンテナ: MP4 コンテナ: MP4 動画コーデック: H.264 動画コーデック: H.265 音声コーデック: AAC 音声コーデック: AAC
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー VideoToolBox 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) AudioToolbox
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
データ読込 let asset = AVAsset(url: url) Assetは、QuickTimeムービーやMP3オーディオファイルのようなファイル ベースのメディアや、HTTPライブストリーミング(HLS)を使ってスト リーミングされたメディアをモデル化します。 https://developer.apple.com/documentation/avfoundation/avasset より
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
デマルチプレクサ https://developer.apple.com/documentation/avfoundation/avasset より // VideoのAVAssetTrack(Videoコーデック)を取り出す let track = try await asset.loadTracks(withMediaType: .video).first
デマルチプレクサ // VideoのAVAssetTrack(Videoコーデック)を取り出す let track = try await asset.loadTracks(withMediaType: .video).first Assetとは、1つ以上のAVAssetTrackを格納するコンテナオブジェクトのことで、トラックをモデ ル化します。一般的に使用されるトラックタイプはAudioとVideoですが、Assetには、クローズド キャプション、字幕、タイムドメタデータなどの補足トラックを含めることもできます。 https://developer.apple.com/documentation/avfoundation/avasset より
デマルチプレクサ(1) コンテナからVideoコーデック・データを取り出す // VideoのAVAssetTrack(Videoコーデック)を取り出す let track = try await asset.loadTracks(withMediaType: .video).first // CMTimeRange(再生時間)を取り出す let totalTimeRange = try await track.load(.timeRange) // [CMFormatDescription] (フォーマット情報)を取り出す let formatDescriptions = try await track.load(.formatDescriptions)
(lldb) po formatDescriptions.first
▿ Optional<CMFormatDescriptionRef>
- some : <CMVideoFormatDescription 0x3009adc20
[0x20af67c70]> {
mediaType:'vide'
mediaSubType:'avc1'
mediaSpecific: {
codecType: 'avc1'
dimensions: 1920 x 1080
}
extensions: {{
CVFieldCount = 1;
CVImageBufferChromaLocationBottomField = Left;
CVImageBufferChromaLocationTopField = Left;
CVImageBufferColorPrimaries = "ITU_R_709_2";
CVImageBufferTransferFunction = "ITU_R_709_2";
CVImageBufferYCbCrMatrix = "ITU_R_709_2";
Depth = 24;
FormatName = "H.264";
デマルチプレクサ(2) // Videoコーデックを読み出すAVAssetReaderを生成する let assetReader = try AVAssetReader(asset: asset) // 読み出す時間範囲を指定する assetReader.timeRange = timeRange // オリジナルのフォーマットで読み出す出力設定 let trackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil) assetReader.add(trackOutput)
動画再生の処理フロー VideoToolBox 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) AudioToolbox
動画デコーダ(1)
// 復号されたデータを受け取るためのコールバックを作成
let callback: VTDecompressionOutputCallback = {
refCon, _, status, _, imageBuffer, pts, _ in
guard status == noErr, let imageBuffer else { return }
// refConをMyClassインスタンスにキャスト
if let objectPointer = UnsafeMutableRawPointer(refCon) {
let object = Unmanaged<MyClass>
.fromOpaque(objectPointer).takeUnretainedValue()
object.setBufferSync(buffer: imageBuffer, pts: pts)
}
}
// 上記のコールバックと、そのコールバック内に渡されるコンテキスト情報を設定する
var outputCallback = VTDecompressionOutputCallbackRecord(
decompressionOutputCallback: callback,
decompressionOutputRefCon:
Unmanaged.passUnretained(self).toOpaque()
)
動画デコーダ(2)
// 復号されたデータを受け取るためのコールバックを作成
let callback: VTDecompressionOutputCallback = {
refCon, _, status, _, imageBuffer, pts, _ in
guard status == noErr, let imageBuffer else { return }
// refConをMyClassインスタンスにキャスト
}
if let objectPointer = UnsafeMutableRawPointer(refCon) {
let object = Unmanaged<MyClass>
.fromOpaque(objectPointer).takeUnretainedValue()
object.setBufferSync(buffer: imageBuffer, pts: pts)
}
画像データ
PTS
// 上記のコールバックと、そのコールバック内に渡されるコンテキスト情報を設定する
var outputCallback = VTDecompressionOutputCallbackRecord(
decompressionOutputCallback: callback,
decompressionOutputRefCon: Unmanaged.passUnretained(self).toOpaque()
)
動画デコーダ(3) let status = VTDecompressionSessionCreate( allocator: kCFAllocatorDefault, formatDescription: formatDescription, decoderSpecification: nil, imageBufferAttributes: nil, outputCallback: &outputCallback, decompressionSessionOut: &decompressionSession )
動画デコーダ(4) // AVAssetReaderを開始 reader.startReading() // フレームを読み込み、デコード while isDecoding, reader.status == .reading { if let sampleBuffer = trackOutput.copyNextSampleBuffer(), CMSampleBufferIsValid(sampleBuffer) { let pts = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) // フレームをデコード let osStatus = VTDecompressionSessionDecodeFrame( session, sampleBuffer: sampleBuffer, flags: [], frameRefcon: nil, infoFlagsOut: nil ) } }
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) PTS == Presentation Time Stamp
動画デコーダ(1)
// 復号されたデータを受け取るためのコールバックを作成
let callback: VTDecompressionOutputCallback = {
refCon, _, status, _, imageBuffer, pts, _ in
guard status == noErr, let imageBuffer else { return }
// refConをMyClassインスタンスにキャスト
if let objectPointer = UnsafeMutableRawPointer(refCon) {
let object = Unmanaged<MyClass>
.fromOpaque(objectPointer).takeUnretainedValue()
object.setBufferSync(buffer: imageBuffer, pts: pts)
}
}
// 上記のコールバックと、そのコールバック内に渡されるコンテキスト情報を設定する
var outputCallback = VTDecompressionOutputCallbackRecord(
decompressionOutputCallback: callback,
decompressionOutputRefCon:
Unmanaged.passUnretained(self).toOpaque()
)
再生
ログ pts = CMTime(value: 0, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 40, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 20, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 80, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 60, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 120, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 100, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 160, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 140, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 200, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 180, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 240, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) fl fl fl fl fl fl fl fl fl fl fl fl fl fl pts = CMTime(value: 220, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0)
GOPと動画圧縮 (MPEG)動画コーデックに 格納されている画像は 独立した、連続した、 画像の集合ではない
独立した、連続した、 画像の集合ではない 1 2 3 4 5 6 7 8 9 ❌❌❌❌ ❌❌ ❌❌ 10
自力でペンギンさんの画像を生成 できるのは最初と最後の2枚だけ 1 2 3 4 5 6 7 8 9 10
H.264 圧縮方式
1 2 3 4 5 6 7 8 9 10
動き補償予測 (Motion Compensated Prediction)
動き補償予測 1 2 3 動き補償予測 4 5 6 7 8 9 10
動き補償予測 1 2 3 動き補償予測 4 5 6 7 8 過去と未来から動き補償予測を行う 9 10
動き補償予測 1 2 3 動き補償予測 4 5 6 7 8 過去と未来から動き補償予測を行う 9 10
デコードの順番 1 4 2 3 7 5 6 10 8 9
データの構造 GOP(Group Of Pictures) 1 2 3 4 5 GOP 6 7 8 9 10
データの構造 GOP(Group Of Pictures) 1 2 3 4 5 GOP 6 7 8 9 10
データの構造 GOP(Group Of Pictures) I B B P B GOP B P B B I
GOP(Group Of Pictures) Iフレーム(Intra-coded frame) Pフレーム(Predictive-coded frame) Bフレーム(Bi-directionally predictive-coded frame) I B B P B GOP B P B B I
動画再生の処理フロー 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) 動画デコーダから取り出された映像を PTS順に並び替える 音声のタイミングに併せて表示する
タイミング制御(PTS Sync) システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder)
タイミング制御(PTS Sync) システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) フレームバッファ
pts = CMTime(value: 180, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 240, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 280, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 260, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 320, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 300, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) (40 - 20) ➗ 600 = 33.3 msec pts = CMTime(value: 360, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 340, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 400, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 380, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 440, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 420, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 480, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 460, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 520, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) pts = CMTime(value: 500, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) fl fl fl fl fl fl fl fl fl fl fl fl fl fl fl fl fl fl pts = CMTime(value: 560, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0) fl ログ pts = CMTime(value: 220, timescale: 600, ags: ̲̲C.CMTimeFlags(rawValue: 1), epoch: 0)
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコー ダ(Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコー ダ(Audio Decoder) フレームバッファ
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) func wait() async { while frameBuffers.count >= maxBufferCount && isDecoding { フレームバッファ try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC) } }
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) func wait() async { while frameBuffers.count >= maxBufferCount && isDecoding { フレームバッファ try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC) } }
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) フレームバッファ
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) フレームバッファ
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) フレームバッファ
システムクロック
private(set) var timebase: CMTimebase?
private var pointer: UnsafeMutablePointer<CMTimebase?>?
private func makeCMTimebase(time: CMTime = .zero, rate: Float64 =
1.0) {
let pointer = UnsafeMutablePointer<CMTimebase?
>.allocate(capacity: 1)
let status = CMTimebaseCreateWithSourceClock(
allocator: kCFAllocatorDefault,
sourceClock: CMClockGetHostTimeClock(),
timebaseOut: pointer )
if status != noErr {
// TODO: エラー表示
}
}
timebase = pointer.pointee
if let timebase {
CMTimebaseSetTime(timebase, time: time);
CMTimebaseSetRate(timebase, rate: rate);
}
タイミング制御(PTS Sync) フロー制御 システム クロック 動画デコーダ (Video Decoder) データ 読込 デマルチプレクサ タイミング制御 レンダリング (Demuxer) (PTS Sync) (Rendering) 音声デコーダ (Audio Decoder) フレームバッファ
システムクロック
func getFrame() async -> FrameBuffer? {
var frame: FrameBuffer?
// デコードされた画像をPTSの順番に取り出す
repeat {
frame = getOldestFrame()
if frame == nil {
try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC)
}
} while (frame == nil && isDecoding)
// PTSに同期してレンダリング
if let frame, frame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
return frame
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
}
}
}
let secToWait = frame.timestamp.seconds - currentTime
let mSecToWait: UInt64
if secToWait > 0 {
mSecToWait = UInt64(secToWait * 1000)
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
return frame
return frame
システムクロック func getFrame() async -> FrameBuffer? { var frame: FrameBuffer? // デコードされた画像をPTSの順番に取り出す repeat { frame = getOldestFrame() if frame == nil { try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC) } } while (frame == nil && isDecoding)
システムクロック
func getFrame() async -> FrameBuffer? {
var frame: FrameBuffer?
// デコードされた画像をPTSの順番に取り出す
repeat {
frame = getOldestFrame()
if frame == nil {
try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC)
}
} while (frame == nil && isDecoding)
// PTSに同期してレンダリング
if let frame, frame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
return frame
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
}
}
}
let secToWait = frame.timestamp.seconds - currentTime
let mSecToWait: UInt64
if secToWait > 0 {
mSecToWait = UInt64(secToWait * 1000)
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
return frame
return frame
システムクロック
// PTSに同期してレンダリング
if let frame, frame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
return frame
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
let secToWait = frame.timestamp.seconds - currentTime
if secToWait > 0 {
let mSecToWait = UInt64(secToWait * 1000)
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
return frame
}
}
return frame
}
特殊再生
private(set) var timebase: CMTimebase?
private var pointer: UnsafeMutablePointer<CMTimebase?>?
private func makeCMTimebase(time: CMTime = .zero, rate: Float64 = 1.0) {
let pointer = UnsafeMutablePointer<CMTimebase?>.allocate(capacity: 1)
let status = CMTimebaseCreateWithSourceClock(
allocator: kCFAllocatorDefault,
sourceClock: CMClockGetHostTimeClock(),
timebaseOut: pointer )
if status != noErr {
// TODO: エラー表示
}
}
timebase = pointer.pointee
if let timebase {
CMTimebaseSetTime(timebase, time: time);
CMTimebaseSetRate(timebase, rate: rate);
}
特殊再生 CMTimebaseSetRate(timebase, rate: rate);
特殊再生
特殊再生 早送り class VideoRenderingUIView: UIImageView { func renderFrame(_ frame: FrameBuffer) { Task { guard let videoImage = UIImage(pixelBuffer: frame.buffer) else { return } let rotatedImage: UIImage? switch frame.orientation { case .up: rotatedImage = videoImage case .down: rotatedImage = videoImage.rotated(by: .pi) case .left: rotatedImage = videoImage.rotated(by: .pi / 2) case .right: rotatedImage = videoImage.rotated(by: .pi * 3 / 2) } DispatchQueue.main.async { self.image = rotatedImage?.resizeAndCrop(to: self.frame.size) } } } }
システムクロック
// PTSに同期してレンダリング
if let frame, frame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
return frame
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
}
}
}
let secToWait = frame.timestamp.seconds - currentTime
if secToWait > 0 {
let mSecToWait = UInt64(secToWait * 1000)
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
return frame
return frame
システムクロック
// PTSに同期してレンダリング
if let localFrame, localFrame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
return frame
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
let secToWait = localFrame.timestamp.seconds - currentTime
if secToWait > 0 {
let mSecToWait = UInt64(secToWait * 1000)
if mSecToWait < 20 {
frame = nil
} else {
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
}
}
}
return frame
特殊再生
早送り
// デコードされた画像をPTSの順番に取り出す
repeat {
frame = getOldestFrame()
if let localFrame = frame, localFrame.timestamp.isValid, let timebase {
if firstTime {
firstTime = false
} else {
let currentCMTime = CMTimebaseGetTime(timebase)
let currentTime = currentCMTime.seconds
let secToWait = (localFrame.timestamp.seconds - currentTime) / rate
if secToWait > 0 {
let mSecToWait = UInt64(secToWait * 1000)
if mSecToWait < 20 {
frame = nil
} else {
try? await Task.sleep(nanoseconds: mSecToWait * NSEC_PER_MSEC)
}
} else {
// このフレームは必要ない
}
frame = nil
}
} else {
try? await Task.sleep(nanoseconds: 5 * NSEC_PER_MSEC)
}
} while (frame == nil && isDecoding)
I B B P B B P B GOP(Group Of Pictures) B I
GOP(Group Of Pictures) Iフレーム(Intra-coded frame) Pフレーム(Predictive-coded frame) Bフレーム(Bi-directionally predictive-coded frame) I B B P B B (Group Of Pictures) P B B I
データ読込 import AVFoundation let asset = AVAsset(url: url)
デマルチプレクサ(2) import AVFoundation // Videoコーデックを読み出すAVAssetReaderを生成する let assetReader = try AVAssetReader(asset: asset) // オリジナルのフォーマットで読み出す出力設定 let trackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil) assetReader.add(trackOutput) // 読み出す時間範囲を指定する if let timeRange { assetReader.timeRange = timeRange }
まとめ 並び順が特殊な圧縮データを復号する必要がある 表示時間(PTS)にあわせて画像を並び替える必要がある 音声データにも同期させて出力する必要がある さらにメモリなどのリソース制約から、フロー制御を行 う必要がある
ありがとうございました