VideoToolboxを使用してH.264ビデオストリームを解凍する方法
質問
Apple のハードウェア アクセラレーション ビデオ フレームワークを使用して H.264 ビデオ ストリームを解凍する方法を見つけるのに、かなり苦労しました。 数週間後、私はそれを理解し、広範な例を見つけることができなかったので、それを共有したいと思いました。
で紹介した Video Toolbox の徹底的で有益な例を挙げることが私の目標です。 WWDC '14 セッション 513 . 私のコードは、基本的な H.264 ストリーム (ファイルから読み込んだビデオまたはオンラインからのストリーミングなど) と統合する必要があり、特定のケースに応じて調整する必要があるため、コンパイルまたは実行されません。
私は、このテーマをググっている間に学んだことを除いて、ビデオ エン/デコードの経験がほとんどないことを述べておく必要があります。 ビデオ フォーマット、パラメータ構造などに関するすべての詳細を知っているわけではないので、知る必要があると思われることだけを記載しました。
私は XCode 6.2 を使用しており、iOS 8.1 と 8.2 を実行している iOS デバイスにデプロイしています。
どのように解決するのですか?
コンセプトは?
NALUです。
NALUは、NALU開始コードヘッダを持つ、様々な長さのデータのチャンクです。
0x00 00 00 01 YY
の最初の5ビットが
YY
の最初の5ビットは、これがどのタイプのNALUであるか、したがって、ヘッダの後にどのタイプのデータが続くかを示します。 (あなたは最初の5ビットしか必要としないので、私は
YY & 0x1F
を使って関連するビットだけを取得します)。 これらのタイプがすべて何であるかをメソッドにリストアップします。
NSString * const naluTypesStrings[]
メソッドにリストアップしていますが、これらがすべて何であるかを知る必要はないでしょう。
パラメータです。 デコーダはH.264のビデオデータがどのように保存されているかを知るために、パラメータが必要です。 設定する必要のある2つのパラメータは シーケンスパラメータセット(SPS) と ピクチャーパラメーターセット(PPS) で、それぞれにNALUの型番があります。 パラメータの意味を知る必要はなく、デコーダがパラメータを使って何をするか知っています。
H.264 ストリーム フォーマット。 ほとんどの H.264 ストリームでは、PPS および SPS パラメータの初期セットと、i フレーム (別名 IDR フレームまたはフラッシュ フレーム) NALU を受け取ります。 その後、いくつかのPフレームNALU(数十個程度)を受信し、別のパラメータセット(最初のパラメータと同じかもしれません)とiフレーム、さらにPフレームなどを受信します。 概念的には、i フレームはビデオの画像全体であり、P フレームは、次の i フレームを受け取るまで、その i フレームに加えられた変更に過ぎないと考えることができます。
手順
-
H.264 ストリームから個々の NALU を生成します。 使用しているビデオソースに大きく依存するため、このステップのコードを表示することはできません。 私は作業内容を示すためにこの図を作成しましたが (図中の "data" は次のコード中の "frame" です)、あなたの場合は異なるかもしれませんし、おそらく異なるでしょう。 私の方法
receivedRawVideoFrame:
はフレームを受け取るたびに呼び出されます (uint8_t *frame
) を受け取るたびに呼び出されます。 図では、これらの2つのフレームタイプは2つの大きな紫色のボックスです。 -
CMVideoFormatDescriptionCreateFromH264ParameterSets()により、SPSとPPSのNALUからCMVideoFormatDescriptionRefを作成する。 . これを行わないと、フレームを表示できません。 SPSとPPSは数字の羅列のように見えるかもしれませんが、VTDはそれらをどうすればよいかを知っています。 あなたが知るべきことは、次のことです。
CMVideoFormatDescriptionRef
はビデオデータの記述です。幅/高さ、フォーマットタイプ(kCMPixelFormat_32BGRA
,kCMVideoCodecType_H264
など)、アスペクト比、色空間など。 デコーダーは、新しいセットが届くまでパラメータを保持します (パラメータが変更されていなくても、定期的に再送信されることがあります)。 -
IDRおよび非IDRフレームNALUをquot;AVCC"のフォーマットに従って再パッケージ化します。 これは、NALU の開始コードを削除し、NALU の長さを示す 4 バイトのヘッダーに置き換えることを意味します。SPSとPPSのNALUについては、これを行う必要はありません。 (4バイトのNALU長ヘッダーはビッグエンディアンであることに注意してください。
UInt32
にコピーする前にバイトスワップされなければなりません。CMBlockBuffer
を使ってCFSwapInt32
. 私のコードでは、これをhtonl
関数呼び出しで行っています)。 -
IDRと非IDRのNALUフレームをCMBlockBufferにパッケージする。 SPSのPPSパラメータNALUでこれを行わないでください。 必要なのは
CMBlockBuffers
について知っておくべきことは、それらがコアメディアで任意のデータブロックをラップする方法であるということです。(ビデオ パイプライン内の圧縮されたビデオ データはすべてこれでラップされます)。 -
CMBlockBufferをCMSampleBufferにパッケージ化します。 について知っておく必要があるのは
CMSampleBuffers
について知っておくべきことは、これらは私たちのCMBlockBuffers
を他の情報 (ここではCMVideoFormatDescription
とCMTime
であればCMTime
が使われている場合)。 -
VTDecompressionSessionRefを作成し、サンプルバッファをVTDecompressionSessionDecodeFrame()に投入します。 別の方法として
AVSampleBufferDisplayLayer
とそのenqueueSampleBuffer:
メソッドを使用すれば、VTDecompSessionを使用する必要はありません。 セットアップがよりシンプルになりますが、VTDのように何か問題が発生してもエラーを出しません。 -
VTDecompSessionコールバックで、結果のCVImageBufferRefを使用してビデオフレームを表示します。 を変換する必要がある場合
CVImageBuffer
をUIImage
に変更するには、私のStackOverflowの回答を参照してください。 ここで .
その他の注意事項
-
H.264 ストリームは大きく変化する可能性があります。 私が学んだところでは NALU開始コードヘッダは時々3バイトです。 (
0x00 00 01
) と、時々4 (0x00 00 00 01
). 私のコードは4バイトで動作します。3バイトで動作させる場合は、いくつかの点を変更する必要があります。 -
もしあなたが <強い NALUについてもっと知りたい ということで、私は この回答 はとても参考になりました。 私の場合、説明されているように "emulation prevention" バイトを無視する必要がないことがわかったので、個人的にはその手順をスキップしましたが、そのことについて知っておく必要があるかもしれません。
-
もしあなたの VTDecompressionSession がエラー番号 (-12909 など) を出力した場合 XCode プロジェクトでエラーコードを調べます。 プロジェクト ナビゲータで VideoToolbox フレームワークを見つけ、それを開いてヘッダ VTErrors.h を見つけます。それが見つからない場合、私は別の回答で以下のすべてのエラー コードも含めています。
コード例。
それでは、いくつかのグローバル変数を宣言して、VTフレームワーク(VT = Video Toolbox)を含めることから始めましょう。
#import <VideoToolbox/VideoToolbox.h>
@property (nonatomic, assign) CMVideoFormatDescriptionRef formatDesc;
@property (nonatomic, assign) VTDecompressionSessionRef decompressionSession;
@property (nonatomic, retain) AVSampleBufferDisplayLayer *videoLayer;
@property (nonatomic, assign) int spsSize;
@property (nonatomic, assign) int ppsSize;
以下の配列は、受信している NALU フレームの種類を出力するためにのみ使用されます。 もし、これらのタイプが何を意味するのか知っているなら、それは良いことです。) 私のコードでは、タイプ 1、5、7、8 のみを処理します。
NSString * const naluTypesStrings[] =
{
@"0: Unspecified (non-VCL)",
@"1: Coded slice of a non-IDR picture (VCL)", // P frame
@"2: Coded slice data partition A (VCL)",
@"3: Coded slice data partition B (VCL)",
@"4: Coded slice data partition C (VCL)",
@"5: Coded slice of an IDR picture (VCL)", // I frame
@"6: Supplemental enhancement information (SEI) (non-VCL)",
@"7: Sequence parameter set (non-VCL)", // SPS parameter
@"8: Picture parameter set (non-VCL)", // PPS parameter
@"9: Access unit delimiter (non-VCL)",
@"10: End of sequence (non-VCL)",
@"11: End of stream (non-VCL)",
@"12: Filler data (non-VCL)",
@"13: Sequence parameter set extension (non-VCL)",
@"14: Prefix NAL unit (non-VCL)",
@"15: Subset sequence parameter set (non-VCL)",
@"16: Reserved (non-VCL)",
@"17: Reserved (non-VCL)",
@"18: Reserved (non-VCL)",
@"19: Coded slice of an auxiliary coded picture without partitioning (non-VCL)",
@"20: Coded slice extension (non-VCL)",
@"21: Coded slice extension for depth view components (non-VCL)",
@"22: Reserved (non-VCL)",
@"23: Reserved (non-VCL)",
@"24: STAP-A Single-time aggregation packet (non-VCL)",
@"25: STAP-B Single-time aggregation packet (non-VCL)",
@"26: MTAP16 Multi-time aggregation packet (non-VCL)",
@"27: MTAP24 Multi-time aggregation packet (non-VCL)",
@"28: FU-A Fragmentation unit (non-VCL)",
@"29: FU-B Fragmentation unit (non-VCL)",
@"30: Unspecified (non-VCL)",
@"31: Unspecified (non-VCL)",
};
さて、ここですべてのマジックが起こります。
-(void) receivedRawVideoFrame:(uint8_t *)frame withSize:(uint32_t)frameSize isIFrame:(int)isIFrame
{
OSStatus status;
uint8_t *data = NULL;
uint8_t *pps = NULL;
uint8_t *sps = NULL;
// I know what my H.264 data source's NALUs look like so I know start code index is always 0.
// if you don't know where it starts, you can use a for loop similar to how i find the 2nd and 3rd start codes
int startCodeIndex = 0;
int secondStartCodeIndex = 0;
int thirdStartCodeIndex = 0;
long blockLength = 0;
CMSampleBufferRef sampleBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
int nalu_type = (frame[startCodeIndex + 4] & 0x1F);
NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
// if we havent already set up our format description with our SPS PPS parameters, we
// can't process any frames except type 7 that has our parameters
if (nalu_type != 7 && _formatDesc == NULL)
{
NSLog(@"Video error: Frame is not an I Frame and format description is null");
return;
}
// NALU type 7 is the SPS parameter NALU
if (nalu_type == 7)
{
// find where the second PPS start code begins, (the 0x00 00 00 01 code)
// from which we also get the length of the first SPS code
for (int i = startCodeIndex + 4; i < startCodeIndex + 40; i++)
{
if (frame[i] == 0x00 && frame[i+1] == 0x00 && frame[i+2] == 0x00 && frame[i+3] == 0x01)
{
secondStartCodeIndex = i;
_spsSize = secondStartCodeIndex; // includes the header in the size
break;
}
}
// find what the second NALU type is
nalu_type = (frame[secondStartCodeIndex + 4] & 0x1F);
NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
}
// type 8 is the PPS parameter NALU
if(nalu_type == 8)
{
// find where the NALU after this one starts so we know how long the PPS parameter is
for (int i = _spsSize + 4; i < _spsSize + 30; i++)
{
if (frame[i] == 0x00 && frame[i+1] == 0x00 && frame[i+2] == 0x00 && frame[i+3] == 0x01)
{
thirdStartCodeIndex = i;
_ppsSize = thirdStartCodeIndex - _spsSize;
break;
}
}
// allocate enough data to fit the SPS and PPS parameters into our data objects.
// VTD doesn't want you to include the start code header (4 bytes long) so we add the - 4 here
sps = malloc(_spsSize - 4);
pps = malloc(_ppsSize - 4);
// copy in the actual sps and pps values, again ignoring the 4 byte header
memcpy (sps, &frame[4], _spsSize-4);
memcpy (pps, &frame[_spsSize+4], _ppsSize-4);
// now we set our H264 parameters
uint8_t* parameterSetPointers[2] = {sps, pps};
size_t parameterSetSizes[2] = {_spsSize-4, _ppsSize-4};
// suggestion from @Kris Dude's answer below
if (_formatDesc)
{
CFRelease(_formatDesc);
_formatDesc = NULL;
}
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2,
(const uint8_t *const*)parameterSetPointers,
parameterSetSizes, 4,
&_formatDesc);
NSLog(@"\t\t Creation of CMVideoFormatDescription: %@", (status == noErr) ? @"successful!" : @"failed...");
if(status != noErr) NSLog(@"\t\t Format Description ERROR type: %d", (int)status);
// See if decomp session can convert from previous format description
// to the new one, if not we need to remake the decomp session.
// This snippet was not necessary for my applications but it could be for yours
/*BOOL needNewDecompSession = (VTDecompressionSessionCanAcceptFormatDescription(_decompressionSession, _formatDesc) == NO);
if(needNewDecompSession)
{
[self createDecompSession];
}*/
// now lets handle the IDR frame that (should) come after the parameter sets
// I say "should" because that's how I expect my H264 stream to work, YMMV
nalu_type = (frame[thirdStartCodeIndex + 4] & 0x1F);
NSLog(@"~~~~~~~ Received NALU Type \"%@\" ~~~~~~~~", naluTypesStrings[nalu_type]);
}
// create our VTDecompressionSession. This isnt neccessary if you choose to use AVSampleBufferDisplayLayer
if((status == noErr) && (_decompressionSession == NULL))
{
[self createDecompSession];
}
// type 5 is an IDR frame NALU. The SPS and PPS NALUs should always be followed by an IDR (or IFrame) NALU, as far as I know
if(nalu_type == 5)
{
// find the offset, or where the SPS and PPS NALUs end and the IDR frame NALU begins
int offset = _spsSize + _ppsSize;
blockLength = frameSize - offset;
data = malloc(blockLength);
data = memcpy(data, &frame[offset], blockLength);
// replace the start code header on this NALU with its size.
// AVCC format requires that you do this.
// htonl converts the unsigned int from host to network byte order
uint32_t dataLength32 = htonl (blockLength - 4);
memcpy (data, &dataLength32, sizeof (uint32_t));
// create a block buffer from the IDR NALU
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, // memoryBlock to hold buffered data
blockLength, // block length of the mem block in bytes.
kCFAllocatorNull, NULL,
0, // offsetToData
blockLength, // dataLength of relevant bytes, starting at offsetToData
0, &blockBuffer);
NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
}
// NALU type 1 is non-IDR (or PFrame) picture
if (nalu_type == 1)
{
// non-IDR frames do not have an offset due to SPS and PSS, so the approach
// is similar to the IDR frames just without the offset
blockLength = frameSize;
data = malloc(blockLength);
data = memcpy(data, &frame[0], blockLength);
// again, replace the start header with the size of the NALU
uint32_t dataLength32 = htonl (blockLength - 4);
memcpy (data, &dataLength32, sizeof (uint32_t));
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, // memoryBlock to hold data. If NULL, block will be alloc when needed
blockLength, // overall length of the mem block in bytes
kCFAllocatorNull, NULL,
0, // offsetToData
blockLength, // dataLength of relevant data bytes, starting at offsetToData
0, &blockBuffer);
NSLog(@"\t\t BlockBufferCreation: \t %@", (status == kCMBlockBufferNoErr) ? @"successful!" : @"failed...");
}
// now create our sample buffer from the block buffer,
if(status == noErr)
{
// here I'm not bothering with any timing specifics since in my case we displayed all frames immediately
const size_t sampleSize = blockLength;
status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer, true, NULL, NULL,
_formatDesc, 1, 0, NULL, 1,
&sampleSize, &sampleBuffer);
NSLog(@"\t\t SampleBufferCreate: \t %@", (status == noErr) ? @"successful!" : @"failed...");
}
if(status == noErr)
{
// set some values of the sample buffer's attachments
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
// either send the samplebuffer to a VTDecompressionSession or to an AVSampleBufferDisplayLayer
[self render:sampleBuffer];
}
// free memory to avoid a memory leak, do the same for sps, pps and blockbuffer
if (NULL != data)
{
free (data);
data = NULL;
}
}
次のメソッドでVTDセッションを作成します。 を受け取るたびに、それを再作成します。 新しい パラメータを受け取るたびに再作成します。 (再作成する必要はありません。 それぞれ に再作成する必要はありません)。
宛先の属性を設定したい場合
CVPixelBuffer
に属性を設定したいのであれば、以下の記事を読んでください。
CoreVideo PixelBufferAttributes の値です。
を読み、それらを
NSDictionary *destinationImageBufferAttributes
.
-(void) createDecompSession
{
// make sure to destroy the old VTD session
_decompressionSession = NULL;
VTDecompressionOutputCallbackRecord callBackRecord;
callBackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;
// this is necessary if you need to make calls to Objective C "self" from within in the callback method.
callBackRecord.decompressionOutputRefCon = (__bridge void *)self;
// you can set some desired attributes for the destination pixel buffer. I didn't use this but you may
// if you need to set some attributes, be sure to uncomment the dictionary in VTDecompressionSessionCreate
NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],
(id)kCVPixelBufferOpenGLESCompatibilityKey,
nil];
OSStatus status = VTDecompressionSessionCreate(NULL, _formatDesc, NULL,
NULL, // (__bridge CFDictionaryRef)(destinationImageBufferAttributes)
&callBackRecord, &_decompressionSession);
NSLog(@"Video Decompression Session Create: \t %@", (status == noErr) ? @"successful!" : @"failed...");
if(status != noErr) NSLog(@"\t\t VTD ERROR type: %d", (int)status);
}
さて、このメソッドはVTDに送ったフレームの解凍が終わるたびに呼び出されます。 このメソッドは、エラーが発生した場合やフレームが削除された場合にも呼び出されます。
void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon,
void *sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration)
{
THISCLASSNAME *streamManager = (__bridge THISCLASSNAME *)decompressionOutputRefCon;
if (status != noErr)
{
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"Decompressed error: %@", error);
}
else
{
NSLog(@"Decompressed sucessfully");
// do something with your resulting CVImageBufferRef that is your decompressed frame
[streamManager displayDecodedFrame:imageBuffer];
}
}
ここで実際にsampleBufferをVTDに送り、デコードしてもらいます。
- (void) render:(CMSampleBufferRef)sampleBuffer
{
VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
VTDecodeInfoFlags flagOut;
NSDate* currentTime = [NSDate date];
VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags,
(void*)CFBridgingRetain(currentTime), &flagOut);
CFRelease(sampleBuffer);
// if you're using AVSampleBufferDisplayLayer, you only need to use this line of code
// [videoLayer enqueueSampleBuffer:sampleBuffer];
}
もし、あなたが
AVSampleBufferDisplayLayer
を使用している場合は、viewDidLoad または他の init メソッドの内部で、必ずこのようにレイヤーを init してください。
-(void) viewDidLoad
{
// create our AVSampleBufferDisplayLayer and add it to the view
videoLayer = [[AVSampleBufferDisplayLayer alloc] init];
videoLayer.frame = self.view.frame;
videoLayer.bounds = self.view.bounds;
videoLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// set Timebase, you may need this if you need to display frames at specific times
// I didn't need it so I haven't verified that the timebase is working
CMTimebaseRef controlTimebase;
CMTimebaseCreateWithMasterClock(CFAllocatorGetDefault(), CMClockGetHostTimeClock(), &controlTimebase);
//videoLayer.controlTimebase = controlTimebase;
CMTimebaseSetTime(self.videoLayer.controlTimebase, kCMTimeZero);
CMTimebaseSetRate(self.videoLayer.controlTimebase, 1.0);
[[self.view layer] addSublayer:videoLayer];
}
関連
-
[解決済み] 角括弧 < > と引用符 " " を使って #import する。
-
[解決済み] Xcodeはクラッシュの原因となる行を表示しない
-
[解決済み] RootViewControllerのスイッチングアニメーション
-
[解決済み] このgdbの出力は何を意味しているのでしょうか?
-
[解決済み] 2つの日付の差を計算するにはどうすればよいですか?
-
[解決済み] 別のUIViewの下にあるUIViewとのインタラクションを許可する
-
[解決済み] Objective-CのReadonlyプロパティ?
-
[解決済み] Xcode 4(および依存関係)におけるコンパイル、ビルド、またはアーカイブの問題
-
[解決済み] Xcodeバージョン(CFBundleShortVersionString)とビルド(CFBundleVersion)の違いについて
-
[解決済み] Objective-CのクラスからSwiftの関数を呼び出す
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み] 角括弧 < > と引用符 " " を使って #import する。
-
[解決済み] Objective-CでNSArrayを新しいNSArrayにフィルタリングする。
-
[解決済み] Swiftでクラスをプロトコルに準拠させるには?
-
[解決済み] performSelectorの使用: メソッドを呼び出すだけと比較した場合
-
[解決済み] メソッドが存在するかどうかをチェックする
-
[解決済み] 別のUIViewの下にあるUIViewとのインタラクションを許可する
-
[解決済み] Swiftの変数はアトミックか?
-
[解決済み] 宣言されたプロパティは、対応するインスタンス変数を必要としますか?
-
[解決済み] BOOLのデフォルト値
-
[解決済み] H.264ストリームのシーケンス/ピクチャーパラメーターセットの配置場所について