在视频功能里,一般对视频加文字是使用AVMutableVideoComposition加载一层文字图层,但是想要对一个视频嵌入一个srt文件中的所有字幕呢?
难点是一个srt文件中,每一段文字都有一个时间节点,需要解析时间节点把文字嵌入到视频中。
第一种方式:
AVAssetReader+AVAssetWriter
这两个对象都是AVFoundation中的,通过它们可以对视频解码生成CMSampleBufferRef对象,这个对象包含了视频,音频中每一片流中的信息,并且可以转换成UIImage单独对流操作,可以对视频、音频文件重新编码:码率、帧率、分辨率、比特率、声道等。
AVAssetReader读取视频中的appendPixelBuffer,把它转换为UIImage,根据时间节点把字幕嵌入到UIImage中,这样一张图片就有了文字,然后再转换回CVPixelBufferRef,再用AVAssetWriterInputPixelBufferAdaptor的appendPixelBuffer方法写入一个新的视频流文件中,最终生成一个带有字幕的视频。
缺点:耗时久,cpu加载过高,把控好内存泄漏。
还有一个坑点,因为视频中可能会有旋转角度,比方说我使用8P拍的竖屏视频,再使用AVAssetReader解码的时候,解出来的CMSampleBufferRef都会旋转90度,所以需要对AVAssetWriterInput对象中的transform赋值,但是这种方式是在输出的时候对流旋转,在现在的字幕的方案的流程是
CMSampleBufferRef转换UIImage、
UIImage写文字、
UIImage转换CVPixelBufferRef。
所以需要改为:
CMSampleBufferRef转换UIImage,UIImage旋转90度
UIImage写文字,UIImage旋转回去、
UIImage转换CVPixelBufferRef。
效果图
Untitled.gif
AVAssetReader读取类:
+ (instancetype)initReader:(NSString *)videoPath{
ccAssetReaderManager* manager = [[ccAssetReaderManager alloc] init];
manager.videoAsset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
if ([manager.reader canAddOutput:manager.readerTrackOutput_video]) {
[manager.reader addOutput:manager.readerTrackOutput_video];
}
if ([manager.reader canAddOutput:manager.readerTrackOutput_audio]) {
[manager.reader addOutput:manager.readerTrackOutput_audio];
}
[manager.reader startReading];
return manager;
}
- (CMSampleBufferRef)nextVideoSample{
if (!_readerTrackOutput_video) {
return nil;
}
return [_readerTrackOutput_video copyNextSampleBuffer];
}
- (CMSampleBufferRef)nextAudioSample{
if (!_readerTrackOutput_audio) {
return nil;
}
return [_readerTrackOutput_audio copyNextSampleBuffer];
}
- (void)cancel{
[self.reader cancelReading];
}
-(AVAssetReader *)reader{
if (!_reader) {
_reader = [[AVAssetReader alloc] initWithAsset:self.videoAsset error:nil];
}
return _reader;
}
-(AVAssetReaderTrackOutput *)readerTrackOutput_video{
if (!_readerTrackOutput_video) {
AVAssetTrack *videoTrack = [[self.videoAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
_readerTrackOutput_video = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:self.videoSetting];
_readerTrackOutput_video.alwaysCopiesSampleData = NO;
}
return _readerTrackOutput_video;
}
-(AVAssetReaderTrackOutput *)readerTrackOutput_audio{
if (!_readerTrackOutput_audio) {
AVAssetTrack *audioTrack = [[self.videoAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
_readerTrackOutput_audio = [[AVAssetReaderTrackOutput alloc] initWithTrack:audioTrack outputSettings:self.audioSetting];
_readerTrackOutput_audio.alwaysCopiesSampleData = NO;
}
return _readerTrackOutput_audio;
}
-(NSDictionary *)videoSetting{
if (!_videoSetting) {
_videoSetting = @{
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
};
}
return _videoSetting;
}
-(NSDictionary *)audioSetting{
if (!_audioSetting) {
_audioSetting = @{
AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]
};
}
return _audioSetting;
}
AVAssetWriter写入类:
+ (instancetype)initWriter:(NSString *)outPutPath inputPath:(NSString*)inputPath{
ccAssetWriterManager* manager = [[ccAssetWriterManager alloc] init];
manager.videoUrl = [NSURL fileURLWithPath:outPutPath];
manager.videoUrl_input = [NSURL fileURLWithPath:inputPath];
manager.group = dispatch_group_create();
manager.queue_video = dispatch_queue_create("queue_video", DISPATCH_QUEUE_CONCURRENT);
manager.queue_audio = dispatch_queue_create("queue_audio", DISPATCH_QUEUE_CONCURRENT);
AVAsset* asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:inputPath]];
//需要根据原视频的旋转角度旋转
manager.assetWriterInput_video.transform = [manager getVideoOrientationWithAsset:asset];
if ([manager.writer canAddInput:manager.assetWriterInput_video]) {
[manager.writer addInput:manager.assetWriterInput_video];
}
if ([manager.writer canAddInput:manager.assetWriterInput_audio]) {
[manager.writer addInput:manager.assetWriterInput_audio];
}
[manager adaptor];
[manager.writer startWriting];
[manager.writer startSessionAtSourceTime:kCMTimeZero];
return manager;
}
// 开始写入
- (void)pushAudioBuffer:(CMSampleBufferRef(^)(void))getBufferBlock{
dispatch_group_enter(self.group);
[self.assetWriterInput_audio requestMediaDataWhenReadyOnQueue:self.queue_audio usingBlock:^{
if ([self startWrite:self.assetWriterInput_audio getBufferBlock:getBufferBlock]) {
//关闭会话
[self.assetWriterInput_audio markAsFinished];
dispatch_group_leave(self.group);
}
}];
}
- (void)pushVideoBuff:(CVPixelBufferRef(^)(void))getBufferBlock getBufferTimeBlock:(CMTime(^)(void))getBufferTimeBlock{
dispatch_group_enter(self.group);
dispatch_async(self.queue_video, ^{
if ([self startWrite:self.adaptor getBufferBlock:getBufferBlock getBufferTimeBlock:getBufferTimeBlock]) {
//关闭会话
[self.assetWriterInput_video markAsFinished];
dispatch_group_leave(self.group);
}
});
}
- (void)finishHandle:(void(^)(bool))handle{
//队列执行完成
dispatch_group_notify(self.group, dispatch_get_main_queue(), ^{
[self.writer finishWritingWithCompletionHandler:^{
AVAssetWriterStatus status = self.writer.status;
dispatch_async(dispatch_get_main_queue(), ^{
if (handle) {
handle(status == AVAssetWriterStatusCompleted);
}
});
}];
});
}
- (void)cancel{
[self.writer cancelWriting];
}
- (BOOL)startWrite:(AVAssetWriterInputPixelBufferAdaptor*)writerInput getBufferBlock:(CVPixelBufferRef(^)(void))getBufferBlock getBufferTimeBlock:(CMTime(^)(void))getBufferTimeBlock{
BOOL complete = NO;
AVAsset* asset = [AVAsset assetWithURL:self.videoUrl_input];
while (!complete && self.assetWriterInput_video.isReadyForMoreMediaData) {
//可以写入
@autoreleasepool {
CVPixelBufferRef buffer = getBufferBlock();
CMTime pts = getBufferTimeBlock();
if (CMTIME_COMPARE_INLINE(pts, >, asset.duration)) {
complete = YES;
break;
}
[self.writer startSessionAtSourceTime:pts];
NSLog(@"插入图片:%f---%ld",CMTimeGetSeconds(pts),(long)self.writer.status);
if (buffer) {
[_adaptor appendPixelBuffer:buffer withPresentationTime:pts];
CFRelease(buffer);
buffer = NULL;
}else{
complete = YES;
}
}
}
return complete;
}
- (BOOL)startWrite:(AVAssetWriterInput*)writerInput getBufferBlock:(CMSampleBufferRef(^)(void))getBufferBlock{
BOOL complete = NO;
while (!complete && writerInput.isReadyForMoreMediaData) {
//可以写入
@autoreleasepool {
CMSampleBufferRef buffer = getBufferBlock();
if (buffer) {
[writerInput appendSampleBuffer:buffer];
CFRelease(buffer);
buffer = NULL;
}else{
complete = YES;
}
}
}
return complete;
}
//获取视频旋转角度
- (CGSize)getVideoOutPutNaturalSizeWithAsset:(AVAsset*)asset{
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
CGFloat width = videoTrack.naturalSize.width;
CGFloat height = videoTrack.naturalSize.height;
CGSize size = CGSizeZero;
CGAffineTransform videoTransform = videoTrack.preferredTransform;//矩阵旋转角度
if (videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0) {
size = CGSizeMake(width, height);
}
if (videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0) {
size = CGSizeMake(width, height);
}
if (videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0) {
size = CGSizeMake(height, width);
}
if (videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0) {
size = CGSizeMake(height, width);
}
return size;
}
//获取视频旋转角度
- (CGAffineTransform)getVideoOrientationWithAsset:(AVAsset*)asset{
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
return videoTrack.preferredTransform;
}
-(AVAssetWriter *)writer{
if (!_writer) {
_writer = [[AVAssetWriter alloc] initWithURL:self.videoUrl fileType:AVFileTypeMPEG4 error:nil];
}
return _writer;
}
-(AVAssetWriterInputPixelBufferAdaptor *)adaptor{
if (!_adaptor) {
CGSize size = [self getVideoOutPutNaturalSizeWithAsset:[AVAsset assetWithURL:self.videoUrl_input]];
NSDictionary *dic = @{
(id)kCVPixelBufferPixelFormatTypeKey:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],
AVVideoCodecKey : AVVideoCodecTypeH264,
AVVideoWidthKey : @(size.width),
AVVideoHeightKey : @(size.height),
(id)kCVPixelFormatOpenGLESCompatibility : @(NO)
};
_adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.assetWriterInput_video sourcePixelBufferAttributes:dic];
}
return _adaptor;
}
-(AVAssetWriterInput *)assetWriterInput_video{
if (!_assetWriterInput_video) {
CGSize size = [self getVideoOutPutNaturalSizeWithAsset:[AVAsset assetWithURL:self.videoUrl_input]];
self.videoSetting = @{
AVVideoCodecKey : AVVideoCodecTypeH264,
AVVideoWidthKey : @(size.width),
AVVideoHeightKey : @(size.height)
};
_assetWriterInput_video = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoSetting];
_assetWriterInput_video.expectsMediaDataInRealTime = YES;
}
return _assetWriterInput_video;
}
-(AVAssetWriterInput *)assetWriterInput_audio{
if (!_assetWriterInput_audio) {
self.audioSetting = @{
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVNumberOfChannelsKey : @(2),
AVSampleRateKey : @(44100),
AVEncoderBitRateKey : @(64000),
};
_assetWriterInput_audio = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioSetting];
}
return _assetWriterInput_audio;
}
使用它们嵌入字幕
- (void)initReader:(NSString*)inputPath subtitles:(NSArray*)subtitles outputPath:(NSString*)outputPath handle:(void(^)(bool))handle{
//初始化
ccAssetReaderManager* manager_reader = [ccAssetReaderManager initReader:inputPath];
ccAssetWriterManager* manager_writer = [ccAssetWriterManager initWriter:outputPath inputPath:inputPath];
//读取buffer 写入文件
__block CMTime pts;
[manager_writer pushVideoBuff:^CVPixelBufferRef _Nonnull{
CMSampleBufferRef buffer = [manager_reader nextVideoSample];
__block CVImageBufferRef CVPixelBuffer = CMSampleBufferGetImageBuffer(buffer);
//时间点
pts = CMSampleBufferGetPresentationTimeStamp(buffer);
CGFloat time = CMTimeGetSeconds(pts);
[subtitles enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
float begin = [obj[@"begin"] floatValue];
float end = [obj[@"end"] floatValue];
NSString* subtitle = obj[@"subtitle"];
if (time>=begin && time<=end) {
UIImage* image = [self imageWithSampleBuffer:buffer];
UIImage* image_text = [self addText:subtitle addToView:image];
UIImage* image_transfrom = [self transfromImage:image_text];
CVPixelBuffer = [self pixelBufferFromCGImage:image_transfrom.CGImage];
}
}];
return CVPixelBuffer;
} getBufferTimeBlock:^CMTime{
return pts;
}];
[manager_writer pushAudioBuffer:^CMSampleBufferRef _Nonnull{
return [manager_reader nextAudioSample];
}];
[manager_writer finishHandle:^(bool success) {
if (success) {
[manager_reader cancel];
[manager_writer cancel];
}
handle(success);
}];
}
//转CMSampleBufferRef-->UIImage 并且旋转
- (UIImage*)imageWithSampleBuffer:(CMSampleBufferRef)buffer {
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(buffer);
CIImage *ciimage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
// 旋转的方法
CIImage *image = [ciimage imageByApplyingCGOrientation:kCGImagePropertyOrientationRight];
return [UIImage imageWithCIImage:image];
}
//转UIImage-->UIImage 并且旋转回去
- (UIImage*)transfromImage:(UIImage*)image {
// 旋转的方法
CIImage* ciimage = [CIImage imageWithCGImage:image.CGImage];
ciimage = [ciimage imageByApplyingCGOrientation:kCGImagePropertyOrientationLeft];
// CIImage *ciimage = [image.CIImage imageByApplyingCGOrientation:kCGImagePropertyOrientationLeft];
CIContext *context = [CIContext contextWithOptions:nil];
CGImageRef myImage = [context createCGImage:ciimage fromRect:CGRectMake(0, 0, image.size.height, image.size.width)];
return [UIImage imageWithCGImage:myImage];
}
//CGImageRef --> CVPixelBufferRef
- (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image
{
CGSize frameSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image));
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kCVPixelBufferCGImageCompatibilityKey,
[NSNumber numberWithBool:NO], kCVPixelBufferCGBitmapContextCompatibilityKey,
nil];
CVPixelBufferRef pxbuffer = NULL;
CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, frameSize.width,
frameSize.height, kCVPixelFormatType_32BGRA, (__bridge CFDictionaryRef) options,
&pxbuffer);
NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL);
CVPixelBufferLockBaseAddress(pxbuffer, 0);
void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer);
CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB();
// kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst 需要转换成需要的32BGRA空间
CGContextRef context = CGBitmapContextCreate(pxdata, frameSize.width,
frameSize.height, 8, 4*frameSize.width, rgbColorSpace,
kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image),
CGImageGetHeight(image)), image);
CGColorSpaceRelease(rgbColorSpace);
CGContextRelease(context);
CVPixelBufferUnlockBaseAddress(pxbuffer, 0);
return pxbuffer;
}
// 添加文字水印
- (UIImage*)addText:(NSString*)text addToView:(UIImage*)image{
int w = image.size.width;
int h = image.size.height;
UIGraphicsBeginImageContext(image.size);
[image drawInRect:CGRectMake(0, 0, w, h)];
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = NSLineBreakByWordWrapping;
textStyle.alignment = NSTextAlignmentCenter;//水平居中
UIFont* font = [UIFont systemFontOfSize:40];
NSDictionary *attr = @{NSFontAttributeName: font, NSForegroundColorAttributeName : [UIColor whiteColor], NSParagraphStyleAttributeName:textStyle,NSKernAttributeName:@(2),NSBackgroundColorAttributeName:[UIColor blackColor]};
[text drawInRect:CGRectMake(0, h - 60, w, 60) withAttributes:attr];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return newImage;
}
附带srt文件解析
// 设置字幕字符串
- (NSArray*)setSrt:(NSString *)srt {
// 去除\t\r
NSString *lyric = [NSString stringWithString:srt];
lyric = [lyric stringByReplacingOccurrencesOfString:@"\r" withString:@""];
lyric = [lyric stringByReplacingOccurrencesOfString:@"\t" withString:@""];
NSArray *arr = [lyric componentsSeparatedByString:@"\n"];
NSMutableArray *tempArr = [NSMutableArray new]; // 存放Item的数组
NSMutableDictionary *itemDic = [NSMutableDictionary dictionary]; // 存放歌词信息的Item
__block NSInteger i = 0; // 标记, 0:序号 1: 时间 2:英文 3:中文
for (NSString *str in arr) {
@autoreleasepool {
NSString *tempStr = [NSString stringWithString:str];
tempStr = [tempStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (tempStr.length > 0) {
switch (i) {
case 0:
[itemDic setObject:tempStr forKey:@"index"];
break;
case 1:{
//时间
NSRange range2 = [tempStr rangeOfString:@"-->"];
if (range2.location != NSNotFound) {
NSString *beginstr = [tempStr substringToIndex:range2.location];
beginstr = [beginstr stringByReplacingOccurrencesOfString:@" " withString:@""];
NSArray * arr = [beginstr componentsSeparatedByString:@":"];
if (arr.count == 3) {
NSArray * arr1 = [arr[2] componentsSeparatedByString:@","];
if (arr1.count == 2) {
//将开始时间数组中的时间换化成秒为单位的
CGFloat start = [arr[0] floatValue] * 60*60 + [arr[1] floatValue]*60 + [arr1[0] floatValue] + [arr1[1] floatValue]/1000;
[itemDic setObject:@(start) forKey:@"begin"];
NSString *endstr = [tempStr substringFromIndex:range2.location+range2.length];
endstr = [endstr stringByReplacingOccurrencesOfString:@" " withString:@""];
NSArray * array = [endstr componentsSeparatedByString:@":"];
if (array.count == 3) {
NSArray * arr2 = [array[2] componentsSeparatedByString:@","];
if (arr2.count == 2) {
//将结束时间数组中的时间换化成秒为单位的
CGFloat end = [array[0] floatValue] * 60*60 + [array[1] floatValue]*60 + [arr2[0] floatValue] + [arr2[1] floatValue]/1000;
[itemDic setObject:@(end) forKey:@"end"];
}
}
}
}
}
break;
}
case 2:
[itemDic setObject:tempStr forKey:@"subtitle"];
break;
// case 3: {
// [itemDic setObject:tempStr forKey:@"en"];
// break;
// }
default:
break;
}
i ++;
}else {
// 遇到空行,就添加到数组
i = 0;
NSDictionary *dic = [NSDictionary dictionaryWithDictionary:itemDic];
[tempArr addObject:dic];
[itemDic removeAllObjects];
}
}
}
return tempArr;
}
GitHub链接:
https://github.com/qw9685/srt-.git
另一种方式嵌入字幕,提高性能:https://www.jianshu.com/p/e372a7b98b29
网友评论