电脑磁盘不够? iOS原生转码h264转码h265

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,关于为什么视频文件反而会变大了,还需要进一步确认.

你可能感兴趣的:(电脑磁盘不够? iOS原生转码h264转码h265)