Preface
最近小编发现电脑里的磁盘容量不够了,
下载的大电影已经存不下了(小编发4并没有下载小电影).
所以小编一直在苦恼如何把大电影能进一步压缩呢?
然后小编了解到,HEVC压缩方案可以使1080P视频内容时的压缩效率提高50%左右.
所以,就先写个h264->h265的demo吧
Result
源文件的信息:
视频编码:h264
视频分辨率:720x480
帧率:30 fps
音频编码:ac3
文件大小:602kB
转换后的视频文件大小:
视频编码:h265
视频分辨率:720x480
帧率:30 fps
音频编码:aac
文件大小:3.2MB
额,这就尴尬了,不是说h265视频大小会变小的么,怎么还大了将近5倍!
这个问题,就等聪明的你来回答把,小编也表示有点懵逼.
Content
1 背景知识
1.1 裸数据格式
裸数据格式,就是音视频信息,被硬件捕获后得到的最原始的数据格式.
对于视频,就是RGB格式,或者YUV格式,每一幅图像就是一个视频帧(VideoFrame).
对于音频,就是PCM格式,每一个采样音频数据就是一个音频帧(AudioFrame).
这些原始的数据,都比较大,并且有很多冗余信息,所以有必要进行编码压缩.
1.2 编码
编码的主要目的,就是缩小视频文件的大小.
对于视频,编码格式常见的有h264,h265等.
每一帧视频帧经过编码后,得到视频包(VideoPacket)
对于音频,编码格式常见的有aac,ac3等.
每一帧音频帧经过编码后,得到音频包(AudioPacket)
1.3 封装
封装,就是把视频包,和音频包按照一定的规律排列起来.
常见的格式有,mp4,mov等.
一般都是按照时间顺序排列起来.
1.4 轨道
虽然视频文件的包排列顺序是按照时间交错排列的.
但是,视频包之间会被组织成一个视频队列,便于查找,即视频轨道.
同理,音频包之间也会被组织成一个音频轨道.
1.5 转码
所以,如果我们要对一个已有的文件进行转码处理需要这么做:
2 转码流程图
具体的实现,请参考demo,demo地址位于文章末尾.
3 部分关键代码
3.1 变量定义
#import
@interface ViewController ()<
AVCaptureMetadataOutputObjectsDelegate
>
{
//Reader
AVAsset * mavAsset;
AVAssetReader * mavAssetReader;
int mi_videoWidth,mi_videoHeight;
AVAssetReaderTrackOutput * mavAssetReaderTrackOutput_video;
AVAssetReaderTrackOutput * mavAssetReaderTrackOutput_audio;
// AVAssetReaderAudioMixOutput
//Writer
AVAssetWriter * mavAssetWriter;
AVAssetWriterInput * mavAssetWriterInput_video;
AVAssetWriterInput * mavAssetWriterInput_audio;
AVAssetWriterInputPixelBufferAdaptor * mavAssetWriterInputPixelBufferAdaptor;
CFAbsoluteTime time_startConvert;
CFAbsoluteTime time_endConvert;
CMTime cmtime_processing;
//statics
int mi_videoFrameCount,mi_audioFrameCount;
CMSampleBufferRef mcmSampleBufferRef_video;
CMTime mcmTime_video;
CMSampleBufferRef mcmSampleBufferRef_audio;
CMTime mcmTime_audio;
//
BOOL mb_isTranscoding;
}
@end
3.2 初始化Reader
- (void)initReader {
NSString * filePath = [[NSBundle mainBundle] pathForResource:@"Butterfly_h264_ac3.mp4" ofType:nil];
NSURL * fileUrl = [NSURL fileURLWithPath:filePath];
//TODO:这个选项是什么意思??
NSDictionary *inputOptions = @{
AVURLAssetPreferPreciseDurationAndTimingKey:@(YES)
};
mavAsset = [AVURLAsset URLAssetWithURL:fileUrl options:inputOptions];
//创建AVAssetReader
NSError *error = nil;
mavAssetReader = [AVAssetReader assetReaderWithAsset:mavAsset error:&error];
//设置Reader输出的内容的格式.
NSDictionary * dic_videoOutputSetting = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)
};
/*
获取资源的一个视频轨道
添加资源的第一个视频轨道
*/
AVAssetTrack *track = [[mavAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
//这个宽高,有点不准确呐??
mi_videoHeight = track.naturalSize.height;
mi_videoWidth = track.naturalSize.width;
//创建AVAssetReaderTrackOutput
mavAssetReaderTrackOutput_video = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:dic_videoOutputSetting];
mavAssetReaderTrackOutput_video.alwaysCopiesSampleData = NO;
if([mavAssetReader canAddOutput:mavAssetReaderTrackOutput_video]){
[mavAssetReader addOutput:mavAssetReaderTrackOutput_video];
NSLog(@"添加视频Output成功.");
}
else {
NSLog(@"添加视频Output失败.");
}
NSArray *audioTracks = [mavAsset tracksWithMediaType:AVMediaTypeAudio];
// This might need to be extended to handle movies with more than one audio track
AVAssetTrack* audioTrack = [audioTracks objectAtIndex:0];
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
// channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
NSData * data = [[NSData alloc] initWithBytes:&channelLayout length:sizeof(AudioChannelLayout)];
NSDictionary * dic_audioOutputSetting = @{
AVFormatIDKey : @(kAudioFormatLinearPCM),
AVSampleRateKey : @(44100),
AVNumberOfChannelsKey : @(1),
AVLinearPCMBitDepthKey : @(16),
AVLinearPCMIsNonInterleaved:@(false),
AVLinearPCMIsFloatKey:@(false),
AVLinearPCMIsBigEndianKey:@(false),
AVChannelLayoutKey:data
};
mavAssetReaderTrackOutput_audio = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:dic_audioOutputSetting];
mavAssetReaderTrackOutput_audio.alwaysCopiesSampleData = NO;
if([mavAssetReader canAddOutput:mavAssetReaderTrackOutput_audio]){
[mavAssetReader addOutput:mavAssetReaderTrackOutput_audio];
NSLog(@"添加音频Output成功.");
}
else {
NSLog(@"添加音频Output失败.");
}
}
3.3 初始化Writer
-(void)initWriter{
NSLog(@"Config writer");
NSString * outputFilePath = @"/Users/gikkiares/Desktop/Output.mp4";
//全局变量还是临时变量?
NSURL * outputFileUrl = [NSURL fileURLWithPath:outputFilePath];
//如果文件存在,则删除,一定要确保文件不存在.
unlink([outputFilePath UTF8String]);
//.mp4 //AVFileTypeMPEG4
//.mov //AVFileTypeQuickTimeMovie
mavAssetWriter = [AVAssetWriter assetWriterWithURL:outputFileUrl fileType:AVFileTypeMPEG4 error:nil];
// Set this to make sure that a functional movie is produced, even if the recording is cut off mid-stream. Only the last second should be lost in that case.
//好像这个属性是必须要设置的.
mavAssetWriter.movieFragmentInterval = CMTimeMakeWithSeconds(1.0, 1000);
//视频input
//视频属性 AVVideoCodecTypeHEVC
NSDictionary * dic_videoCompressionSettings = @{
AVVideoCodecKey : AVVideoCodecTypeHEVC,
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill,
AVVideoWidthKey : @(mi_videoWidth),
AVVideoHeightKey : @(mi_videoHeight)
};
//初始化写入器,并制定了媒体格式
mavAssetWriterInput_video = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:dic_videoCompressionSettings];
mavAssetWriterInput_video.expectsMediaDataInRealTime = YES;
//默认值是PI/2,导致导出的视频有一个90度的旋转.
mavAssetWriterInput_video.transform = CGAffineTransformMakeRotation(0);
//接受的数据帧的格式
NSDictionary *sourcePixelBufferAttributesDictionary =@{
(NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA),
(NSString *)kCVPixelBufferWidthKey:@(mi_videoWidth),
(NSString *)kCVPixelBufferHeightKey:@(mi_videoHeight)
};
mavAssetWriterInputPixelBufferAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:mavAssetWriterInput_video sourcePixelBufferAttributes:sourcePixelBufferAttributesDictionary];
//添加视频input
if([mavAssetWriter canAddInput:mavAssetWriterInput_video]) {
[mavAssetWriter addInput:mavAssetWriterInput_video];
NSLog(@"Wirter add video input,successed.");
}
else {
NSLog(@"Wirter add video input,failed.");
}
//添加音频input
//kAudioFormatLinearPCM
AudioChannelLayout channelLayout;
memset(&channelLayout, 0, sizeof(AudioChannelLayout));
// channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Mono;
NSData * data = [[NSData alloc] initWithBytes:&channelLayout length:sizeof(AudioChannelLayout)];
NSDictionary * dic_audioCompressionSettings = @{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVSampleRateKey : @(44100),
AVNumberOfChannelsKey : @(1),
AVChannelLayoutKey:data
};
//初始化写入器,并制定了媒体格式
mavAssetWriterInput_audio = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:dic_audioCompressionSettings];
if([mavAssetWriter canAddInput:mavAssetWriterInput_audio]) {
[mavAssetWriter addInput:mavAssetWriterInput_audio];
NSLog(@"Wirter add audio input,successed.");
}
else {
NSLog(@"Wirter add audio input,failed.");
}
}
3.4 处理每一帧
/**
开始读取和处理每一帧数据
*/
- (void)startProcessEveryFrame {
//TODO:AssetReader开始一次之后,不能再次开始.
if ([mavAssetReader startReading]) {
NSLog(@"Assert reader start reading,成功.");
}
else {
AVAssetReaderStatus status = mavAssetReader.status;
NSError * error = mavAssetReader.error;
NSLog(@"Assert reader start reading,失败,status is %ld,%@",(long)status,error.userInfo);
return;
}
if([mavAssetWriter startWriting]) {
NSLog(@"Assert writer start writing,成功.");
[mavAssetWriter startSessionAtSourceTime:kCMTimeZero];
}
else {
NSLog(@"Assert writer start writing,失败.");
return;
}
//这个操作不能放主线程,播放不了的.
// dispatch_queue_t queue = dispatch_queue_create("com.writequeue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
self->time_startConvert = CFAbsoluteTimeGetCurrent();
while (self->mavAssetReader.status == AVAssetReaderStatusReading||self->mcmSampleBufferRef_audio||self->mcmSampleBufferRef_video) {
if(!self->mcmSampleBufferRef_video) {
self->mcmSampleBufferRef_video = [self->mavAssetReaderTrackOutput_video copyNextSampleBuffer];
}
if(!self->mcmSampleBufferRef_audio) {
self->mcmSampleBufferRef_audio = [self->mavAssetReaderTrackOutput_audio copyNextSampleBuffer];
}
CMTime cmTime_videoTime = CMSampleBufferGetPresentationTimeStamp(self->mcmSampleBufferRef_video);
CMTime cmTime_audioTime = CMSampleBufferGetPresentationTimeStamp(self->mcmSampleBufferRef_audio);
if(self->mcmSampleBufferRef_video && self->mcmSampleBufferRef_audio) {
float videoTime = CMTimeGetSeconds(cmTime_videoTime);
float audioTime = CMTimeGetSeconds(cmTime_audioTime);
if(videoTime<=audioTime) {
//处理视频
[self processSampleBuffer:self->mcmSampleBufferRef_video isVideo:YES pts:cmTime_videoTime];
}
else {
//处理音频
[self processSampleBuffer:self->mcmSampleBufferRef_audio isVideo:NO pts:cmTime_audioTime];
}
}
else {
if(self->mcmSampleBufferRef_audio) {
[self processSampleBuffer:self->mcmSampleBufferRef_audio isVideo:NO pts:cmTime_audioTime];
}
else if(self->mcmSampleBufferRef_video) {
[self processSampleBuffer:self->mcmSampleBufferRef_video isVideo:YES pts:cmTime_videoTime];
}
else {
//没有音频也没有视频
NSLog(@"copyNextSampleBuffer没有获取到数据,AssertReader应该已经读取数据完毕.");
}
}
}
if(self->mavAssetReader.status == AVAssetReaderStatusCompleted) {
NSLog(@"AssetReader数据已经读取完毕");
switch (self->mavAssetWriter.status) {
case AVAssetWriterStatusWriting:{
[self onTranscodeFinish];
break;
}
case AVAssetWriterStatusCompleted:{
NSLog(@"AssetWriter写入数据完毕");
break;
}
default:{
NSLog(@"AssetWriter状态异常");
break;
}
}
}
else if(self->mavAssetReader.status == AVAssetReaderStatusFailed){
NSLog(@"AVAssetReader读取失败,可能是格式设置问题.");
}
else {
NSLog(@"AVAssetReader状态异常:%ld",self->mavAssetReader.status);
}
});
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer isVideo:(BOOL)isVideo pts:(CMTime)cmTime{
if(isVideo) {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
while(![mavAssetWriterInput_video isReadyForMoreMediaData]) {
sleep(0.1);
}
[mavAssetWriterInputPixelBufferAdaptor appendPixelBuffer:pixelBuffer withPresentationTime:cmTime];
//释放刚刚的cgimage
CFRelease(sampleBuffer);
mcmSampleBufferRef_video = nil;
self->mi_videoFrameCount ++;
}
else {
while(![mavAssetWriterInput_audio isReadyForMoreMediaData]) {
sleep(0.1);
}
[mavAssetWriterInput_audio appendSampleBuffer:sampleBuffer];
CFRelease(sampleBuffer);
mcmSampleBufferRef_audio = nil;
self->mi_audioFrameCount++;
}
}
3.5 转码完毕
- (void)onTranscodeFinish {
[self->mavAssetWriterInput_audio markAsFinished];
[self->mavAssetWriterInput_video markAsFinished];
//mavAssetWriterfinish可以释放很多内存.
[mavAssetWriter finishWritingWithCompletionHandler:^{
[self->mavAssetReader cancelReading];
self->time_endConvert = CFAbsoluteTimeGetCurrent();
CFTimeInterval duration = self->time_endConvert - self->time_startConvert;
self->mb_isTranscoding = NO;
NSString *strInfo = [NSString stringWithFormat:@"转换完毕,一共耗时:%.2fs,there are %d audio,%d video",duration,self->mi_audioFrameCount,self->mi_videoFrameCount];
NSLog(@"%@",strInfo);
}];
}
3.6 开始转码
- (IBAction)onClickStart:(id)sender {
if(!mb_isTranscoding) {
mb_isTranscoding = YES;
[self initReader];
[self initWriter];
[self startProcessEveryFrame];
}
}
Summary
0,Demo地址:TransodeDemo
1,关于为什么视频文件反而会变大了,还需要进一步确认.