由於公司專案的原因,一開始參照github上的kxmovie,利用FFMPEG和OpenGL寫了一個RTMPVideoPlayer。在播放解析的過程中,因為CPU和Memory的使用率比較大,手機播放久了會發熱。所以就只能想辦法解決這個問題了。在網上搜了一天的資料,發現iOS 8.0以後,Apple開放了VideoToolbox這個framework,可以用於視訊的硬編碼。可是在Apple的開發者官網找了好久都沒有找到相關的資料啊,簡直欲哭無淚啊。。後來只能在Stack Overflow和Apple的視訊裡面找到資料。把坑給填好。
介面概述
在iOS中,與視訊相關的介面有5個,從頂層開始分別是 AVKit - AVFoundation - VideoToolbox - Core Media - Core Video
其中VideoToolbox可以將視訊解壓到CVPixelBuffer,也可以壓縮到CMSampleBuffer。
如果需要使用硬編碼的話,在5個介面中,就需要用到AVKit,AVFoundation和VideoToolbox。在這裡我就只介紹VideoToolbox。
VideoToolbox物件
- CVPixelBuffer - 未壓縮光柵影象快取區(Uncompressed Raster Image Buffer)
- CVPixelBufferPool - 顧名思義,存放CVPixelBuffer
-
pixelBufferAttributes - CFDictionary物件,可能會包含視訊的寬高,畫素格式型別(32RGBA, YCbCr420),是否可以用於OpenGL ES等相關資訊
-
CMTime - 分子是64-bit的時間值,分母是32-bit的時標(time scale)
-
CMVideoFormatDescription - 視訊寬高,格式(kCMPixelFormat_32RGBA, kCMVideoCodecType_H264), 其他諸如顏色空間等資訊的擴充套件
-
CMBlockBuffer -
- CMSampleBuffer - 對於壓縮的視訊幀來說,包含了CMTime,CMVideoFormatDesc和CMBlockBuffer;對於未壓縮的光柵影象的話,則包含了CMTime,CMVideoFormatDesc和CMPixelBuffer
-
CMClock - 封裝了時間源,其中
CMClockGetHostTimeClock()
封裝了mach_absolute_time()
-
CMTimebase - CMClock上的控制檢視。提供了時間的對映:
CMTimebaseSetTime(timebase, kCMTimeZero);
; 速率控制:CMTimebaseSetRate(timebase, 1.0);
Case One - 播放視訊流檔案
使用VideoToolbox硬編碼來播放網路上的流檔案時,整個完整的流程是這樣的:獲取網路檔案 -> 獲取多個已壓縮的H.264取樣 -> 呼叫AVSampleBufferDisplayLayer -> 播放
更詳細點看的話,在AVSamplerBufferDisplayLayer這一層中,我們還需要將視訊解碼到CVPixelBuffer中
處理過程
下面要介紹的就是流檔案到CMSampleBuffers的H.264的處理過程:
在H.264的語法中,有一個最基礎的層,叫做Network Abstraction Layer, 簡稱為NAL。H.264流資料正是由一系列的NAL單元(NAL Unit, 簡稱NALU)組成的。
一個NALU可能包含有:
- 視訊幀(或者是視訊幀的片段) - P幀, I幀, B幀
- H.264屬性集合:Sequence Parameter Set(SPS)和Picture Parameter Set(PPS)
流資料中,屬性集合可能是這樣的:
經過處理之後,在Format Description中則是:
要從基礎的流資料將SPS和PPS轉化為Format Desc中的話,需要呼叫CMVideoFormatDescriptionCreateFromH264ParameterSets()
方法
NALU header
對於流資料來說,一個NALU的Header中,可能是0x00 00 01或者是0x00 00 00 01作為開頭(兩者都有可能,下面以0x00 00 01作為例子)。0x00 00 01因此被稱為開始碼(Start code).
一個MP4檔案的話,則是以0x00 00 80 00作為開頭。因此要將基本流資料轉換成CMSampleBuffer的話,需CMBlockBuffer+CMVideoFormatDesc+CMTime(Optional)。我們可以呼叫CMSampleBufferCreate()
來完成轉換
時間控制
如果需要控制每一幀圖片的顯示時間的話,可以通過CMTimebase進行時間的控制
sbDisplayLayer.controlTimebase = CMTimebaseCreateWithMasterClock(CMClockGetHostTimeClock());
CMTimebaseSetTime(sbDisplayLayer.controlTimebase, CMTimeMake(5, 1));CMTimebaseSetRate(sbDisplayLayer.controlTimebase, 1.0);
複製程式碼
總結
播放一個網路流檔案的流程大概就是這樣,總結起來就是:
1) 建立AVSampleBufferDisplayLayer
2)將H.264基礎流轉換為CMSampleBuffer
3)將CMSampleBuffers提供給AVSampleBufferDisplayLayer
4)可以使用自定義的CMTimebase
Case Two - 從已壓縮的流中獲取CVPixelBuffers
獲取解碼器
這個步驟中,我們所需要的有:
- 源資料的描述 - CMVideoFormatDescription
- 輸出快取所需要的引數 - pixelBufferAttributes:
e.g :
NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES],(id)kCVPixelBufferOpenGLESCompatibilityKey,nil];
複製程式碼
- 回撥函式 - VTDecompressionOutputCallback。該回撥函式接收一下引數: CVPixelBuffer輸出,時間戳,編碼的錯誤碼,丟棄的幀
以上為Apple的Keynote中的介紹,下面通過程式碼來解釋
在我自己的Project中,我利用FFMPEG和VideoToolbox來進行網路MP4檔案的解析。關於FFMPEG的部分我就不解釋了。只貼VideoToolbox硬解碼部分。
另外,關於H.264開始碼這部分相關的資訊,也可以參考我另一篇文章
...
// 利用FFMPEG的解碼器,獲取到sps和pps,IDR資料
// SPS和PPS資料在codec中的extradata中
// IDR資料在packet的data中
- (void)setupVideoDecoder {
_pCodecCtx = _pFormatCtx->streams[_videoStream]->codec;
while (av_read_frame(_pFormatCtx, &_packet) >= 0) {
// Whether is video stream
if (_packet.stream_index == _videoStream) {
[self.videoDecoder decodeWithCodec:_pCodecCtx packet:_packet];
}
}
}
...
複製程式碼
Decoder.m
#import "UFVideoDecoder.h"
@interface UFVideoDecoder () {
NSData *_spsData;
NSData *_ppsData;
VTDecompressionSessionRef _decompressionSessionRef;
CMVideoFormatDescriptionRef _formatDescriptionRef;
OSStatus _status;
}
@end
@implementation UFVideoDecoder
- (void)decodeWithCodec:(AVCodecContext *)codec packet:(AVPacket)packet {
[self findSPSAndPPSInCodec:codec];
[self decodePacket:packet];
}
#pragma mark - Private Methods
// 找尋SPS和PPS資料
- (void)findSPSAndPPSInCodec:(AVCodecContext *)codec {
// 將用不上的位元組替換掉,在SPS和PPS前新增開始碼
// 假設extradata資料為 0x01 64 00 0A FF E1 00 19 67 64 00 00...其中67開始為SPS資料
// 則替換後為0x00 00 00 01 67 64...
// 使用FFMPEG提供的方法。
// 我一開始以為FFMPEG的這個方法會直接獲取到SPS和PPS,誰知道只是替換掉開始碼。
// 要注意的是,這段程式碼會一直報**Packet header is not contained in global extradata, corrupted stream or invalid MP4/AVCC bitstream**。可是貌似對資料獲取沒什麼影響。我就直接忽略了
uint8_t *dummy = NULL;
int dummy_size;
AVBitStreamFilterContext* bsfc = av_bitstream_filter_init("h264_mp4toannexb");
av_bitstream_filter_filter(bsfc, codec, NULL, &dummy, &dummy_size, NULL, 0, 0);
av_bitstream_filter_close(bsfc);
// 獲取SPS和PPS的資料和長度
int startCodeSPSIndex = 0;
int startCodePPSIndex = 0;
uint8_t *extradata = codec->extradata;
for (int i = 3; i < codec->extradata_size; i++) {
if (extradata[i] == 0x01 && extradata[i-1] == 0x00 && extradata[i-2] == 0x00 && extradata[i-3] == 0x00) {
if (startCodeSPSIndex == 0) startCodeSPSIndex = i + 1;
if (i > startCodeSPSIndex) {
startCodePPSIndex = i + 1;
break;
}
}
}
// 這裡減4是因為需要減去PPS的開始碼的4個位元組
int spsLength = startCodePPSIndex - 4 - startCodeSPSIndex;
int ppsLength = codec->extradata_size - startCodePPSIndex;
_spsData = [NSData dataWithBytes:&extradata[startCodeSPSIndex] length:spsLength];
_ppsData = [NSData dataWithBytes:&extradata[startCodePPSIndex] length:ppsLength];
if (_spsData != nil && _ppsData != nil) {
// Set H.264 parameters
const uint8_t* parameterSetPointers[2] = { (uint8_t *)[_spsData bytes], (uint8_t *)[_ppsData bytes] };
const size_t parameterSetSizes[2] = { [_spsData length], [_ppsData length] };
// 建立CMVideoFormatDesc
_status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, 4, &_formatDescriptionRef);
if (_status != noErr) NSLog(@"\n\nFormat Description ERROR: %d", (int)_status);
}
if (_status == noErr && _decompressionSessionRef == NULL) [self createDecompressionSession];
}
// 建立session
- (void)createDecompressionSession {
// Make sure to destory the old VTD session
_decompressionSessionRef = NULL;
// 回撥函式
VTDecompressionOutputCallbackRecord callbackRecord;
callbackRecord.decompressionOutputCallback = decompressionSessionDecodeFrameCallback;
// 如果需要在回撥函式中呼叫到self的話
callbackRecord.decompressionOutputRefCon = (__bridge void*)self;
// pixelBufferAttributes
NSDictionary *destinationImageBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], (id)kCVPixelBufferOpenGLCompatibilityKey, [NSNumber numberWithInt:kCVPixelFormatType_32BGRA], (id)kCVPixelBufferPixelFormatTypeKey, nil];
_status = VTDecompressionSessionCreate(NULL, _formatDescriptionRef, NULL, (__bridge CFDictionaryRef)(destinationImageBufferAttributes), &callbackRecord, &_decompressionSessionRef);
if(_status != noErr) NSLog(@"\t\t VTD ERROR type: %d", (int)_status);
}
// 回撥函式
void decompressionSessionDecodeFrameCallback(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef imageBuffer, CMTime presentationTimestamp, CMTime presentationDuration) {
UFVideoDecoder *decoder = (__bridge UFVideoDecoder*)decompressionOutputRefCon;
if (status != noErr) {
NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
NSLog(@"Decompressed error: %@", error);
} else {
[decoder.delegate getDecodeImageData:imageBuffer];
}
}
// 解析IDR或no-IDR資料
- (void)decodePacket:(AVPacket)packet {
uint8_t* frame = packet.data;
int size = packet.size;
int startIndex = 4; // 資料都從第5位開始
int nalu_type = ((uint8_t)frame[startIndex] & 0x1F);
// 1為IDR,5為no-IDR
if (nalu_type == 1 || nalu_type == 5) {
// 建立CMBlockBuffer
CMBlockBufferRef blockBufferRef = NULL;
_status = CMBlockBufferCreateWithMemoryBlock(NULL, frame, size, kCFAllocatorNull, NULL, 0, size, 0, &blockBufferRef);
// 移除掉前面4個位元組的資料
int reomveHeaderSize = size - 4;
const uint8_t sourceBytes[] = {(uint8_t)(reomveHeaderSize >> 24), (uint8_t)(reomveHeaderSize >> 16), (uint8_t)(reomveHeaderSize >> 8), (uint8_t)reomveHeaderSize};
_status = CMBlockBufferReplaceDataBytes(sourceBytes, blockBufferRef, 0, 4);
// CMSampleBuffer
CMSampleBufferRef sbRef = NULL;
// int32_t timeSpan = 90000;
// CMSampleTimingInfo timingInfo;
// timingInfo.presentationTimeStamp = CMTimeMake(0, timeSpan);
// timingInfo.duration = CMTimeMake(3000, timeSpan);
// timingInfo.decodeTimeStamp = kCMTimeInvalid;
const size_t sampleSizeArray[] = {size};
_status = CMSampleBufferCreate(kCFAllocatorDefault, blockBufferRef, true, NULL, NULL, _formatDescriptionRef, 1, 0, NULL, 1, sampleSizeArray, &sbRef);
// 解析
VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
VTDecodeInfoFlags flagOut;
_status = VTDecompressionSessionDecodeFrame(_decompressionSessionRef, sbRef, flags, &sbRef, &flagOut);
CFRelease(sbRef);
}
}
@end
複製程式碼
根據以下步驟的話,就可以完成流的硬編碼:
1) FFMPEG解析
2)獲取SPS和PPS資料,建立CMVideoFormatDescription物件
3)建立VTDecompressionSession:注意回撥函式和pixelBufferAttributes
4)解析IDR資料, 建立CMBlockBuffer物件
5)去除IDR前面4個位元組的資料
6)建立CMSampleBuffer
7) 解碼:VTDecompressionSessionDecodeFrame
展示的部分還在寫,關於VideoToolbox的話就先寫到這裡。
下面這個傳送門通向SO裡關於硬解碼的一個回答,回答很詳細。可以作為參照
傳送門: