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)行布局。
- 在
KFVideoCapture
的sessionInitSuccessCallBack
回調(diào)中實(shí)現(xiàn)貌虾。
- 在
- 2)在采集模塊的數(shù)據(jù)回調(diào)中將數(shù)據(jù)交給編碼模塊進(jìn)行編碼吞加。
- 在
KFVideoCapture
的sampleBufferOutputCallBack
回調(diào)中實(shí)現(xiàn)。
- 在
- 3)在編碼模塊的數(shù)據(jù)回調(diào)中獲取編碼后的 H.264/H.265 數(shù)據(jù)尽狠,并將數(shù)據(jù)交給封裝器
KFMP4Muxer
進(jìn)行封裝衔憨。 - 在
KFVideoEncoder
的sampleBufferOutputCallBack
回調(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 等工具來查看它的格式
- 完 -
推薦閱讀
《iOS AVDemo(8):視頻編碼》