电脑磁盘不够? 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 <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];
		//创建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,关于为什么视频文件反而会变大了,还需要进一步确认.

posted @ 2021-08-15 10:16  GikkiAres  阅读(474)  评论(0)    收藏  举报