iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助本地平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
过程,并借助音视频工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染
流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是第四篇:iOS 音频解封装 Demo。这个 Demo 里包含以下内容:
-
1)实现一个音频解封装模块;
-
2)实现对 MP4 文件中音频部分的解封装逻辑并将解封装后的编码数据存储为 AAC 文件;
-
3)详尽的代码注释,帮你理解代码逻辑和原理。
1、音频解封装模块
首先,实现一个 KFDemuxerConfig
类用于定义音频解封装参数的配置。这里包括了:待解封装的资源、解封装类型这几个参数。这样设计是因为这个配置类不仅会用于音频解封装,后续的视频解封装也会使用。
KFDemuxerConfig.h
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFMediaBase.h"
NS_ASSUME_NONNULL_BEGIN
@interface KFDemuxerConfig : NSObject
@property (nonatomic, strong) AVAsset *asset; // 待解封装的资源。
@property (nonatomic, assign) KFMediaType demuxerType; // 解封装类型。
@end
NS_ASSUME_NONNULL_END
KFDemuxerConfig.m
#import "KFDemuxerConfig.h"
@implementation KFDemuxerConfig
- (instancetype)init {
self = [super init];
if (self) {
_demuxerType = KFMediaAV;
}
return self;
}
@end
其中用到的 KFMediaType
是定义在 KFMediaBase.h
中的一个枚举:
#ifndef KFMediaBase_h
#define KFMediaBase_h
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, KFMediaType) {
KFMediaNone = 0,
KFMediaAudio = 1 << 0, // 仅音频。
KFMediaVideo = 1 << 1, // 仅视频。
KFMediaAV = KFMediaAudio | KFMediaVideo, // 音视频都有。
};
#endif /* KFMediaBase_h */
接下来,我们实现一个 KFMP4Demuxer
类来实现 MP4 的解封装。它能从符合 MP4 标准的文件中解封装出音频编码数据。
KFMP4Demuxer.h
#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
#import "KFDemuxerConfig.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, KFMP4DemuxerStatus) {
KFMP4DemuxerStatusUnknown = 0,
KFMP4DemuxerStatusRunning = 1,
KFMP4DemuxerStatusFailed = 2,
KFMP4DemuxerStatusCompleted = 3,
KFMP4DemuxerStatusCancelled = 4,
};
@interface KFMP4Demuxer : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFDemuxerConfig *)config;
@property (nonatomic, strong, readonly) KFDemuxerConfig *config;
@property (nonatomic, copy) void (^errorCallBack)(NSError *error);
@property (nonatomic, assign, readonly) BOOL hasAudioTrack; // 是否包含音频数据。
@property (nonatomic, assign, readonly) BOOL hasVideoTrack; // 是否包含视频数据。
@property (nonatomic, assign, readonly) CGSize videoSize; // 视频大小。
@property (nonatomic, assign, readonly) CMTime duration; // 媒体时长。
@property (nonatomic, assign, readonly) CMVideoCodecType codecType; // 编码类型。
@property (nonatomic, assign, readonly) KFMP4DemuxerStatus demuxerStatus; // 解封装器状态。
@property (nonatomic, assign, readonly) BOOL audioEOF; // 是否音频结束。
@property (nonatomic, assign, readonly) BOOL videoEOF; // 是否视频结束。
@property (nonatomic, assign, readonly) CGAffineTransform preferredTransform; // 图像的变换信息。比如:视频图像旋转。
- (void)startReading:(void (^)(BOOL success, NSError *error))completeHandler; // 开始读取数据解封装。
- (void)cancelReading; // 取消读取。
- (BOOL)hasAudioSampleBuffer; // 是否还有音频数据。
- (CMSampleBufferRef)copyNextAudioSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份音频采样。
- (BOOL)hasVideoSampleBuffer; // 是否还有视频数据。
- (CMSampleBufferRef)copyNextVideoSampleBuffer CF_RETURNS_RETAINED; // 拷贝下一份视频采样。
@end
NS_ASSUME_NONNULL_END
上面是 KFMP4Demuxer
的接口设计,除了初始化方法,主要还有一些获取解封装器信息或者状态的属性接口,此外就是执行开始/取消读取数据、拷贝音频/视频采样数据的操作接口。
这里大家可能会疑惑,为什么 KFMP4Demuxer
不像前面的 Demo 中设计的 KFAudioCapture
、KFAudioEncoder
的接口那样,有一个解封装后的数据回调接口。主要是因为解封装的速度是非常快的,不会成为一个音视频 pipeline 的瓶颈,而且考虑到解封装的资源可能会很大,所以一般不会一直不停地解出数据往外抛,这样下一个处理节点可能处理不过来这些数据。基于这些原因,解封装器的接口设计是让外部调用方主动找解封装器要数据来触发解封装操作,并且还要控制一定的缓存量防止内存占用过大。
【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~
在上面的拷贝下一份音频/视频采样数据
接口中,我们使用的是依然 CMSampleBufferRef[1] 作为返回值类型。在这个接口中我们通过 CMSampleBufferRef
打包的是从 MP4/M4A 文件解封装后得到的 AAC 编码数据。
KFMP4Demuxer.m
#import "KFMP4Demuxer.h"
#define KFMP4DemuxerBadFileError 2000
#define KFMP4DemuxerAddVideoOutputError 2001
#define KFMP4DemuxerAddAudioOutputError 2002
#define KFMP4DemuxerQueueMaxCount 3
@interface KFMP4Demuxer () {
CMSimpleQueueRef _audioQueue;
CMSimpleQueueRef _videoQueue;
}
@property (nonatomic, strong, readwrite) KFDemuxerConfig* config;
@property (nonatomic, strong) AVAssetReader *demuxReader; // 解封装器实例。
@property (nonatomic, strong) AVAssetReaderTrackOutput *readerAudioOutput; // Demuxer 的音频输出。
@property (nonatomic, strong) AVAssetReaderTrackOutput *readerVideoOutput; // Demuxer 的视频输出。
@property (nonatomic, strong) dispatch_queue_t demuxerQueue;
@property (nonatomic, strong) dispatch_semaphore_t demuxerSemaphore;
@property (nonatomic, strong) dispatch_semaphore_t audioQueueSemaphore;
@property (nonatomic, strong) dispatch_semaphore_t videoQueueSemaphore;
@property (nonatomic, assign) CMTime lastAudioCopyN