iOS AVDemo(9):視頻封裝,采集編碼 H.264/H.265 并封裝 MP4丨音視頻工程示例

vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』來及時獲得最新的音視頻技術(shù)文章宪睹。

莫奈《早晨的塞納河》

這個公眾號會路線圖 式的遍歷分享音視頻技術(shù)音視頻基礎(chǔ)(完成)音視頻工具(完成)音視頻工程示例(進(jìn)行中) → 音視頻工業(yè)實(shí)戰(zhàn)(準(zhǔn)備)慎冤。

iOS/Android 客戶端開發(fā)同學(xué)如果想要開始學(xué)習(xí)音視頻開發(fā)整葡,最絲滑的方式是對音視頻基礎(chǔ)概念知識有一定了解后姆打,再借助 iOS/Android 平臺的音視頻能力上手去實(shí)踐音視頻的采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染過程良姆,并借助音視頻工具來分析和理解對應(yīng)的音視頻數(shù)據(jù)。

音視頻工程示例這個欄目幔戏,我們將通過拆解采集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染流程并實(shí)現(xiàn) Demo 來向大家介紹如何在 iOS/Android 平臺上手音視頻開發(fā)歇盼。

這里是第九篇:iOS 視頻封裝 Demo。這個 Demo 里包含以下內(nèi)容:

  • 1)實(shí)現(xiàn)一個視頻采集模塊评抚;
  • 2)實(shí)現(xiàn)一個視頻編碼模塊,支持 H.264/H.265伯复;
  • 3)實(shí)現(xiàn)一個視頻封裝模塊慨代;
  • 4)串聯(lián)視頻采集、編碼啸如、封裝模塊侍匙,將采集到的視頻數(shù)據(jù)輸入給編碼模塊進(jìn)行編碼,再將編碼后的數(shù)據(jù)輸入給 MP4 封裝模塊封裝和存儲叮雳;
  • 5)詳盡的代碼注釋想暗,幫你理解代碼邏輯和原理。

在本文中帘不,我們將詳解一下 Demo 的具體實(shí)現(xiàn)和源碼说莫。讀完本文內(nèi)容相信就能幫你掌握相關(guān)知識。

不過寞焙,如果你的需求是:1)直接獲得全部工程源碼储狭;2)想進(jìn)一步咨詢音視頻技術(shù)問題;3)咨詢音視頻職業(yè)發(fā)展問題捣郊×杀罚可以根據(jù)自己的需要考慮是否加入『關(guān)鍵幀的音視頻開發(fā)圈』,這是一個收費(fèi)的社群服務(wù)呛牲,目前還有少量優(yōu)惠券可用刮萌。vx 搜索『gjzkeyframe』 關(guān)注『關(guān)鍵幀Keyframe』咨詢,或知識星球搜『關(guān)鍵幀的音視頻開發(fā)圈』即可加入娘扩。

1着茸、視頻采集模塊

在這個 Demo 中壮锻,視頻采集模塊 KFVideoCapture 的實(shí)現(xiàn)與 《iOS 視頻采集 Demo》 中一樣,這里就不再重復(fù)介紹了元扔,其接口如下:

KFVideoCapture.h

#import <Foundation/Foundation.h>
#import "KFVideoCaptureConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface KFVideoCapture : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFVideoCaptureConfig *)config;

@property (nonatomic, strong, readonly) KFVideoCaptureConfig *config;
@property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 視頻預(yù)覽渲染 layer躯保。
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 視頻采集數(shù)據(jù)回調(diào)。
@property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 視頻采集會話錯誤回調(diào)澎语。
@property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 視頻采集會話初始化成功回調(diào)途事。

- (void)startRunning; // 開始采集。
- (void)stopRunning; // 停止采集擅羞。
- (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切換攝像頭尸变。
@end

NS_ASSUME_NONNULL_END

2、視頻編碼模塊

同樣的减俏,視頻編碼模塊 KFVideoEncoder 的實(shí)現(xiàn)與《iOS 視頻編碼 Demo》中一樣召烂,這里就不再重復(fù)介紹了,其接口如下:

KFVideoEncoder.h


#import <Foundation/Foundation.h>
#import "KFVideoEncoderConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface KFVideoEncoder : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFVideoEncoderConfig*)config;

@property (nonatomic, strong, readonly) KFVideoEncoderConfig *config; // 視頻編碼配置參數(shù)娃承。
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sampleBuffer); // 視頻編碼數(shù)據(jù)回調(diào)奏夫。
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 視頻編碼錯誤回調(diào)。

- (void)encodePixelBuffer:(CVPixelBufferRef)pixelBuffer ptsTime:(CMTime)timeStamp; // 編碼历筝。
- (void)refresh; // 刷新重建編碼器酗昼。
- (void)flush; // 清空編碼緩沖區(qū)。
- (void)flushWithCompleteHandler:(void (^)(void))completeHandler; // 清空編碼緩沖區(qū)并回調(diào)完成梳猪。
@end

NS_ASSUME_NONNULL_END

3麻削、視頻封裝模塊

視頻編碼模塊即 KFMP4Muxer,復(fù)用了《iOS 音頻封裝 Demo》中介紹的 muxer春弥,這里就不再重復(fù)介紹了呛哟,其接口如下:

KFMP4Muxer.h

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFMuxerConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface KFMP4Muxer : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFMuxerConfig *)config;

@property (nonatomic, strong, readonly) KFMuxerConfig *config;
@property (nonatomic, copy) void (^errorCallBack)(NSError *error); // 封裝錯誤回調(diào)。

- (void)startWriting; // 開始封裝寫入數(shù)據(jù)匿沛。
- (void)cancelWriting; // 取消封裝寫入數(shù)據(jù)扫责。
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 添加封裝數(shù)據(jù)。
- (void)stopWriting:(void (^)(BOOL success, NSError *error))completeHandler; // 停止封裝寫入數(shù)據(jù)俺祠。
@end

NS_ASSUME_NONNULL_END

4公给、采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼以及 MP4 封裝和存儲

我們還是在一個 ViewController 中來實(shí)現(xiàn)采集視頻數(shù)據(jù)進(jìn)行 H.264/H.265 編碼以及 MP4 封裝和存儲的邏輯。

KFVideoMuxerViewController.m

#import "KFVideoMuxerViewController.h"
#import "KFVideoCapture.h"
#import "KFVideoEncoder.h"
#import "KFMP4Muxer.h"

@interface KFVideoMuxerViewController ()
@property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig;
@property (nonatomic, strong) KFVideoCapture *videoCapture;
@property (nonatomic, strong) KFVideoEncoderConfig *videoEncoderConfig;
@property (nonatomic, strong) KFVideoEncoder *videoEncoder;
@property (nonatomic, strong) KFMuxerConfig *muxerConfig;
@property (nonatomic, strong) KFMP4Muxer *muxer;
@property (nonatomic, assign) BOOL isWriting;
@end

@implementation KFVideoMuxerViewController
#pragma mark - Property
- (KFVideoCaptureConfig *)videoCaptureConfig {
    if (!_videoCaptureConfig) {
        _videoCaptureConfig = [[KFVideoCaptureConfig alloc] init];
    }
    return _videoCaptureConfig;
}

- (KFVideoCapture *)videoCapture {
    if (!_videoCapture) {
        _videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig];
        __weak typeof(self) weakSelf = self;
        _videoCapture.sessionInitSuccessCallBack = ^() {
            dispatch_async(dispatch_get_main_queue(), ^{
                // 預(yù)覽渲染蜘渣。
                [weakSelf.view.layer insertSublayer:weakSelf.videoCapture.previewLayer atIndex:0];
                weakSelf.videoCapture.previewLayer.backgroundColor = [UIColor blackColor].CGColor;
                weakSelf.videoCapture.previewLayer.frame = weakSelf.view.bounds;
            });
        };
        _videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            if (sampleBuffer && weakSelf.isWriting) {
                // 編碼淌铐。
                [weakSelf.videoEncoder encodePixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer) ptsTime:CMSampleBufferGetPresentationTimeStamp(sampleBuffer)];
            }
        };
        _videoCapture.sessionErrorCallBack = ^(NSError *error) {
            NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription);
        };
    }
    
    return _videoCapture;
}

- (KFVideoEncoderConfig *)videoEncoderConfig {
    if (!_videoEncoderConfig) {
        _videoEncoderConfig = [[KFVideoEncoderConfig alloc] init];
    }
    
    return _videoEncoderConfig;
}

- (KFVideoEncoder *)videoEncoder {
    if (!_videoEncoder) {
        _videoEncoder = [[KFVideoEncoder alloc] initWithConfig:self.videoEncoderConfig];
        __weak typeof(self) weakSelf = self;
        _videoEncoder.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
            // 視頻編碼數(shù)據(jù)回調(diào)。
            if (weakSelf.isWriting) {
                // 當(dāng)標(biāo)記封裝寫入中時蔫缸,將編碼的 H.264/H.265 數(shù)據(jù)送給封裝器腿准。
                [weakSelf.muxer appendSampleBuffer:sampleBuffer];
            }
        };
    }
    
    return _videoEncoder;
}

- (KFMuxerConfig *)muxerConfig {
    if (!_muxerConfig) {
        _muxerConfig = [[KFMuxerConfig alloc] init];
        NSString *videoPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.mp4"];
        NSLog(@"MP4 file path: %@", videoPath);
        [[NSFileManager defaultManager] removeItemAtPath:videoPath error:nil];
        _muxerConfig.outputURL = [NSURL fileURLWithPath:videoPath];
        _muxerConfig.muxerType = KFMediaVideo;
    }
    
    return _muxerConfig;
}

- (KFMP4Muxer *)muxer {
    if (!_muxer) {
        _muxer = [[KFMP4Muxer alloc] initWithConfig:self.muxerConfig];
    }
    
    return _muxer;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];

    // 啟動后即開始請求視頻采集權(quán)限并開始采集。
    [self requestAccessForVideo];
    [self setupUI];
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    self.videoCapture.previewLayer.frame = self.view.bounds;
}

- (void)dealloc {
    
}

#pragma mark - Action
- (void)start {
    if (!self.isWriting) {
        // 啟動封裝,
        [self.muxer startWriting];
        // 標(biāo)記開始封裝寫入吐葱。
        self.isWriting = YES;
    }
}

- (void)stop {
    if (self.isWriting) {
        __weak typeof(self) weakSelf = self;
        [self.videoEncoder flushWithCompleteHandler:^{
            weakSelf.isWriting = NO;
            [weakSelf.muxer stopWriting:^(BOOL success, NSError * _Nonnull error) {
                NSLog(@"muxer stop %@", success ? @"success" : @"failed");
            }];
        }];
    }
}

- (void)changeCamera {
    [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack];
}

- (void)singleTap:(UIGestureRecognizer *)sender {
    
}

-(void)handleDoubleTap:(UIGestureRecognizer *)sender {
    [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack];
}

#pragma mark - Private Method
- (void)requestAccessForVideo {
    __weak typeof(self) weakSelf = self;
    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    switch (status) {
        case AVAuthorizationStatusNotDetermined:{
            // 許可對話沒有出現(xiàn)街望,發(fā)起授權(quán)許可。
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [weakSelf.videoCapture startRunning];
                } else {
                    // 用戶拒絕弟跑。
                }
            }];
            break;
        }
        case AVAuthorizationStatusAuthorized:{
            // 已經(jīng)開啟授權(quán)灾前,可繼續(xù)。
            [weakSelf.videoCapture startRunning];
            break;
        }
        default:
            break;
    }
}

- (void)setupUI {
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.title = @"Video Muxer";
    self.view.backgroundColor = [UIColor whiteColor];
    
    // 添加手勢孟辑。
    UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(singleTap:)];
    singleTapGesture.numberOfTapsRequired = 1;
    singleTapGesture.numberOfTouchesRequired = 1;
    [self.view addGestureRecognizer:singleTapGesture];
    
    UITapGestureRecognizer *doubleTapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(handleDoubleTap:)];
    doubleTapGesture.numberOfTapsRequired = 2;
    doubleTapGesture.numberOfTouchesRequired = 1;
    [self.view addGestureRecognizer:doubleTapGesture];
    
    [singleTapGesture requireGestureRecognizerToFail:doubleTapGesture];

    
    // Navigation item.
    UIBarButtonItem *startBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Start" style:UIBarButtonItemStylePlain target:self action:@selector(start)];
    UIBarButtonItem *stopBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Stop" style:UIBarButtonItemStylePlain target:self action:@selector(stop)];
    UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Camera" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)];
    self.navigationItem.rightBarButtonItems = @[stopBarButton, startBarButton, cameraBarButton];
}

@end

上面是 KFVideoMuxerViewController 的實(shí)現(xiàn)哎甲,其中主要包含這幾個部分:

  • 1)啟動后即開始請求視頻采集權(quán)限并開始采集。
    • -requestAccessForVideo 方法中實(shí)現(xiàn)饲嗽。
  • 2)在采集會話初始化成功的回調(diào)中炭玫,對采集預(yù)覽渲染視圖層進(jìn)行布局。
    • KFVideoCapturesessionInitSuccessCallBack 回調(diào)中實(shí)現(xiàn)貌虾。
  • 2)在采集模塊的數(shù)據(jù)回調(diào)中將數(shù)據(jù)交給編碼模塊進(jìn)行編碼吞加。
    • KFVideoCapturesampleBufferOutputCallBack 回調(diào)中實(shí)現(xiàn)。
  • 3)在編碼模塊的數(shù)據(jù)回調(diào)中獲取編碼后的 H.264/H.265 數(shù)據(jù)尽狠,并將數(shù)據(jù)交給封裝器 KFMP4Muxer 進(jìn)行封裝衔憨。
    • KFVideoEncodersampleBufferOutputCallBack 回調(diào)中實(shí)現(xiàn)。
  • 4)在調(diào)用 -stop 停止整個流程后袄膏,如果沒有出現(xiàn)錯誤巫财,封裝的 MP4 文件會被存儲到 muxerConfig 設(shè)置的路徑。

5哩陕、用工具播放 MP4 文件

完成 Demo 后,可以將 App Document 文件夾下面的 test.mp4 文件拷貝到電腦上赫舒,使用 ffplay 播放來驗(yàn)證一下效果是否符合預(yù)期:

$ ffplay -I test.mp4

關(guān)于播放 MP4 文件的工具悍及,可以參考《FFmpeg 工具》第 2 節(jié) ffplay 命令行工具《可視化音視頻分析工具》第 3.5 節(jié) VLC 播放器

我們還可以用《可視化音視頻分析工具》第 3.1 節(jié) MP4Box.js 等工具來查看它的格式

Demo 生成的 MP4 文件結(jié)構(gòu)

- 完 -

推薦閱讀
《iOS AVDemo(8):視頻編碼》

《iOS AVDemo(7):視頻采集》

《iOS 音頻處理框架及重點(diǎn) API 合集》

《iOS AVDemo(6):音頻渲染》

《iOS AVDemo(5):音頻解碼》

《iOS AVDemo(4):音頻解封裝》

《iOS AVDemo(3):音頻封裝》

《iOS AVDemo(2):音頻編碼》

《iOS AVDemo(1):音頻采集》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末接癌,一起剝皮案震驚了整個濱河市心赶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缺猛,老刑警劉巖缨叫,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異荔燎,居然都是意外死亡耻姥,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門有咨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琐簇,“玉大人,你說我怎么就攤上這事座享⊥裆蹋” “怎么了似忧?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長丈秩。 經(jīng)常有香客問我盯捌,道長,這世上最難降的妖魔是什么蘑秽? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任饺著,我火速辦了婚禮,結(jié)果婚禮上筷狼,老公的妹妹穿的比我還像新娘瓶籽。我一直安慰自己,他們只是感情好埂材,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布塑顺。 她就那樣靜靜地躺著,像睡著了一般俏险。 火紅的嫁衣襯著肌膚如雪严拒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天竖独,我揣著相機(jī)與錄音裤唠,去河邊找鬼。 笑死莹痢,一個胖子當(dāng)著我的面吹牛种蘸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播竞膳,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼航瞭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坦辟?” 一聲冷哼從身側(cè)響起刊侯,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锉走,沒想到半個月后滨彻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挪蹭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年亭饵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梁厉。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冬骚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情只冻,我是刑警寧澤庇麦,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站喜德,受9級特大地震影響山橄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舍悯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一航棱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧萌衬,春花似錦饮醇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至混移,卻和暖如春祠墅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背歌径。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工毁嗦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人回铛。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓狗准,卻偏偏與公主長得像,于是被迫代替她去往敵國和親茵肃。 傳聞我的和親對象是個殘疾皇子驶俊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容