電腦磁盤不夠? iOS原生轉(zhuǎn)碼h264轉(zhuǎn)碼h265

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)于為什么視頻文件反而會變大了,還需要進一步確認.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

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