Preface
最近小編發(fā)現(xiàn)電腦里的磁盤容量不夠了,
下載的大電影已經(jīng)存不下了(小編發(fā)4并沒有下載小電影).
所以小編一直在苦惱如何把大電影能進一步壓縮呢?
然后小編了解到,HEVC壓縮方案可以使1080P視頻內(nèi)容時的壓縮效率提高50%左右.
所以,就先寫個h264->h265的demo吧
Result
源文件的信息:
視頻編碼:h264
視頻分辨率:720x480
幀率:30 fps
音頻編碼:ac3
文件大小:602kB
轉(zhuǎn)換后的視頻文件大小:
視頻編碼:h265
視頻分辨率:720x480
幀率:30 fps
音頻編碼:aac
文件大小:3.2MB
額,這就尷尬了,不是說h265視頻大小會變小的么,怎么還大了將近5倍!
這個問題,就等聰明的你來回答把,小編也表示有點懵逼.
Content
1 背景知識
1.1 裸數(shù)據(jù)格式
裸數(shù)據(jù)格式,就是音視頻信息,被硬件捕獲后得到的最原始的數(shù)據(jù)格式.
對于視頻,就是RGB格式,或者YUV格式,每一幅圖像就是一個視頻幀(VideoFrame).
對于音頻,就是PCM格式,每一個采樣音頻數(shù)據(jù)就是一個音頻幀(AudioFrame).
這些原始的數(shù)據(jù),都比較大,并且有很多冗余信息,所以有必要進行編碼壓縮.
1.2 編碼
編碼的主要目的,就是縮小視頻文件的大小.
對于視頻,編碼格式常見的有h264,h265等.
每一幀視頻幀經(jīng)過編碼后,得到視頻包(VideoPacket)
對于音頻,編碼格式常見的有aac,ac3等.
每一幀音頻幀經(jīng)過編碼后,得到音頻包(AudioPacket)
1.3 封裝
封裝,就是把視頻包,和音頻包按照一定的規(guī)律排列起來.
常見的格式有,mp4,mov等.
一般都是按照時間順序排列起來.
1.4 軌道
雖然視頻文件的包排列順序是按照時間交錯排列的.
但是,視頻包之間會被組織成一個視頻隊列,便于查找,即視頻軌道.
同理,音頻包之間也會被組織成一個音頻軌道.
1.5 轉(zhuǎn)碼
所以,如果我們要對一個已有的文件進行轉(zhuǎn)碼處理需要這么做:
2 轉(zhuǎn)碼流程圖
具體的實現(xiàn),請參考demo,demo地址位于文章末尾.
3 部分關(guān)鍵代碼
3.1 變量定義
#import <AVFoundation/AVFoundation.h>
@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];
//創(chuàng)建AVAssetReader
NSError *error = nil;
mavAssetReader = [AVAssetReader assetReaderWithAsset:mavAsset error:&error];
//設(shè)置Reader輸出的內(nèi)容的格式.
NSDictionary * dic_videoOutputSetting = @{
(NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)
};
/*
獲取資源的一個視頻軌道
添加資源的第一個視頻軌道
*/
AVAssetTrack *track = [[mavAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
//這個寬高,有點不準確吶??
mi_videoHeight = track.naturalSize.height;
mi_videoWidth = track.naturalSize.width;
//創(chuàng)建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.
//好像這個屬性是必須要設(shè)置的.
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度的旋轉(zhuǎn).
mavAssetWriterInput_video.transform = CGAffineTransformMakeRotation(0);
//接受的數(shù)據(jù)幀的格式
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 處理每一幀
/**
開始讀取和處理每一幀數(shù)據(jù)
*/
- (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沒有獲取到數(shù)據(jù),AssertReader應該已經(jīng)讀取數(shù)據(jù)完畢.");
}
}
}
if(self->mavAssetReader.status == AVAssetReaderStatusCompleted) {
NSLog(@"AssetReader數(shù)據(jù)已經(jīng)讀取完畢");
switch (self->mavAssetWriter.status) {
case AVAssetWriterStatusWriting:{
[self onTranscodeFinish];
break;
}
case AVAssetWriterStatusCompleted:{
NSLog(@"AssetWriter寫入數(shù)據(jù)完畢");
break;
}
default:{
NSLog(@"AssetWriter狀態(tài)異常");
break;
}
}
}
else if(self->mavAssetReader.status == AVAssetReaderStatusFailed){
NSLog(@"AVAssetReader讀取失敗,可能是格式設(shè)置問題.");
}
else {
NSLog(@"AVAssetReader狀態(tài)異常:%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 轉(zhuǎn)碼完畢
- (void)onTranscodeFinish {
[self->mavAssetWriterInput_audio markAsFinished];
[self->mavAssetWriterInput_video markAsFinished];
//mavAssetWriterfinish可以釋放很多內(nèi)存.
[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:@"轉(zhuǎn)換完畢,一共耗時:%.2fs,there are %d audio,%d video",duration,self->mi_audioFrameCount,self->mi_videoFrameCount];
NSLog(@"%@",strInfo);
}];
}
3.6 開始轉(zhuǎn)碼
- (IBAction)onClickStart:(id)sender {
if(!mb_isTranscoding) {
mb_isTranscoding = YES;
[self initReader];
[self initWriter];
[self startProcessEveryFrame];
}
}
Summary
0,Demo地址:TransodeDemo
1,關(guān)于為什么視頻文件反而會變大了,還需要進一步確認.