iOSエンジニアの桐山です。 頑張りすぎるMacBook Proがヒーターと化していて、あったかいです。
iPhoneからHDR動画をアップロードしようとした場合に、HDR動画再生未対応機種(iPhone 11以前のiPhone、Android端末)でも再生できるようにした話を書きます。
発生していた事象
HDR動画とは
HDRとは、High Dynamic Range(ハイダイナミックレンジ)の略称で、従来のSDR(スタンダードダイナミックレンジ)に比べてより広い明るさの幅(ダイナミックレンジ)を表現できる表示技術です。 一般的なSDR映像では日陰が黒つぶれしたり日向が白飛びしたりしますが、HDR映像では明るい部分と暗い部分どちらの階調も犠牲にすることなく、より自然でリアルな描写が可能になります。
引用元 www.eizo.co.jp
iPhone 12 以降のモデルでは、HDR動画を撮影できるようになりました。
※以降、従来の動画についてはSDR動画と表記します
WWDC2020でのセッション
iPhone 12 以降での、HDR動画の再生、編集、エクスポートについての説明があります。
1つ目のセッションについてはサンプルコードがあり、こちらからダウンロードが可能です。
対応しなければならないこと
- エクスポート処理の
AVAssetExportSession
を生成する時に、AVAsset
ではなくAVComposition
を使用するように変更する AVAssetExportSession
にAVVideoComposition
を設定する- HDR動画をSDR動画としてエクスポートする設定を追加する
HDR動画をSDR動画としてエクスポートするコード
エクスポート処理
let (avComposition, videoComposition) = AssetLoader.loadAsCompositions(asset: urlAsset) guard let assetExportSession = AVAssetExportSession( asset: avComposition, presetName: AVAssetExportPreset960x540 ) else { return } assetExportSession.videoComposition = videoComposition assetExportSession.outputFileType = .mp4 assetExportSession.outputURL = outputURL assetExportSession.exportAsynchronously { [weak self] in guard let self = self else { return } switch assetExportSession.status { case .completed: // 成功時の処理 case .failed: // 失敗時の処理 case .exporting: // エクスポート中の処理 default: break } }
変換処理
// 音声データを設定
までのイメージ図。
Adobe After EffectsやiMovieなどの動画編集ツールのタイムラインと同じようなイメージ。
final class AssetLoader { static func loadAsCompositions(asset: AVAsset) -> (AVComposition, AVVideoComposition) { // 動画および音声データを管理するオブジェクト let composition = AVMutableComposition() let assetVideoTrack = asset.tracks(withMediaType: .video).first! // 動画データを設定 let mutableCompositionVideoTrack = composition.addMutableTrack( withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid )! try? mutableCompositionVideoTrack.insertTimeRange( CMTimeRange(start: .zero, duration: asset.duration), of: assetVideoTrack, at: .zero ) // 音声データを設定 let audioTracks = asset.tracks(withMediaType: .audio) if !audioTracks.isEmpty { let mutableCompositionAudioTrack = composition.addMutableTrack( withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid )! for audioTrack in audioTracks { try? mutableCompositionAudioTrack.insertTimeRange( audioTrack.timeRange, of: audioTrack, at: audioTrack.timeRange.start ) } } let assetTransform = assetVideoTrack.fixedPreferredTransform var videoSize = assetVideoTrack.naturalSize if isPortrait(transform: assetTransform) { videoSize = CGSize( width: assetVideoTrack.naturalSize.height, height: assetVideoTrack.naturalSize.width ) } let compositionVideoTrack = composition.tracks(withMediaType: .video).first! let layerInstruction = AVMutableVideoCompositionLayerInstruction( assetTrack: compositionVideoTrack ) // AVAssetとレイヤーのアフィン変換を一致させないと、書き出した動画の描画位置がずれる layerInstruction.setTransform(assetTransform, at: .zero) let compositionInstruction = AVMutableVideoCompositionInstruction() compositionInstruction.timeRange = compositionVideoTrack.timeRange compositionInstruction.layerInstructions = [layerInstruction] let videoComposition = AVMutableVideoComposition() videoComposition.instructions = [compositionInstruction] // フレームレート設定 videoComposition.frameDuration = CMTime(value: 1, timescale: 30) videoComposition.renderSize = videoSize // HDR動画をSDR動画として書き出すための設定 videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2 videoComposition.colorTransferFunction = AVVideoTransferFunction_ITU_R_709_2 videoComposition.colorYCbCrMatrix = AVVideoYCbCrMatrix_ITU_R_709_2 return (composition, videoComposition) } private static func isPortrait(transform: CGAffineTransform) -> Bool { return abs(transform.b) == 1 && abs(transform.c) == 1 } } // 反転を含むすべての動画の向きに対応 extension AVAssetTrack { var fixedPreferredTransform: CGAffineTransform { var t = preferredTransform switch (t.a, t.b, t.c, t.d) { case (1, 0, 0, 1): // Landscape Left t.tx = 0 t.ty = 0 case (1, 0, 0, -1): // Landscape Right (Reverse) t.tx = 0 t.ty = naturalSize.height case (-1, 0, 0, 1): // Landscape Left (Reverse) t.tx = naturalSize.width t.ty = 0 case (-1, 0, 0, -1): // Landscape Right t.tx = naturalSize.width t.ty = naturalSize.height case (0, -1, 1, 0): // Upside Down t.tx = 0 t.ty = naturalSize.width case (0, 1, -1, 0): // Portrait t.tx = naturalSize.height t.ty = 0 case (0, 1, 1, 0): // Portrait (Reverse) t.tx = 0 t.ty = 0 case (0, -1, -1, 0): // Upside Down (Reverse) t.tx = naturalSize.height t.ty = naturalSize.width default: break } return t } }
2021/3/18追記
- 動画をトリミング、反転させた場合にエクスポート結果が正しくなるように修正しました
2021/1/18追記
- 音声データが存在していない動画で
insertTimeRange
をしてしまうとエクスポート時にエラーになるため、これを修正しました AVMutableVideoComposition
のrenderSize
同様、AVMutableVideoCompositionLayerInstruction
のtransform
についても縦向き動画の考慮を追加しました
追記ここまで
コードは記載しませんが、同様の方法で以下のような編集処理も可能になります。
感想
本事象に対応着手する前は、「Appleに公式のドキュメントがあるし、ほとんどコピペでいけるだろう😀」と思っていましたが、意図しない向きに動画が回転してしまったり😇、動画サイズが期待する描画サイズとずれてしまったり😇、結果的に丸1日かかってしまいました。
もともと、動画周りのコードは触ったことがなかったのですが、今回の対応を通して、AVFoundation周りの知見がかなり増えました💪
同様の事象で悩んでいるエンジニアの時間救済に役立てれば幸いです。
参考リンク
- Apple公式のエクスポート処理ドキュメント
- AVFoundationを使った動画編集
- 動画が意図しない回転をしてしまう事象
- AVAssetとAVFoundationの記事まとめ(記事中の全リンク読ませていただきました)
PR
子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中!
急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP: http://timers-inc.com/engineerings