最近需要做一個功能,給視頻添加字幕,關(guān)鍵還是OC的代碼,愁啊。
這種沒做過的東西就只能面對搜索引擎編程了。
各種搜索和嘗試,最后弄了兩天給弄出來了,唯一不足的就是字幕的清晰度不夠,也暫時不知道是不是視頻合成質(zhì)量的問題。
簡單說一下整個流程的思路及關(guān)鍵點:
1. 創(chuàng)建AVMutableComposition 和 AVMutableCompositionTrack
AVMutableComposition *mix = [AVMutableComposition composition];
// 視頻軌道
AVMutableCompositionTrack *videoTrack = [mix addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
//音頻軌道
AVMutableCompositionTrack *audioTrack = [mix addMutableTrackWithMediaType:AVMediaTypeAudio
preferredTrackID:kCMPersistentTrackID_Invalid];
2. 把資源放進(jìn)Track
[videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:videoTrack.asset.duration error:nil];
[AudioTrack insertTimeRange:CMTimeRangeFromTimeToTime(kCMTimeZero, videoTrack.asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];
3.創(chuàng)建導(dǎo)出
self.exporter = [[AVAssetExportSession alloc] initWithAsset:mix presetName:AVAssetExportPreset3840x2160];
4. AVMutableVideoComposition 和 AVMutableVideoCompositionInstruction
//AVMutableVideoComposition:管理所有視頻軌道,可以決定最終視頻的尺寸,裁剪需要在這里進(jìn)行
AVMutableVideoComposition *mainCompositionInst = [AVMutableVideoComposition videoComposition];
// AVMutableVideoCompositionInstruction 視頻軌道中的一個視頻,可以縮放、旋轉(zhuǎn)等
AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
5.添加字幕或水印
// videoLayer是視頻layer,parentLayer是最主要的,videoLayer和字幕,貼紙的layer都要加在該layer上
CALayer *parentLayer = [CALayer layer];
CALayer *videoLayer = [CALayer layer];
...
[parentLayer addSublayer: 字幕layer];
因為主要就是要添加字幕,所以這里的邏輯我會重點說一下。
字幕其實就是放在視頻layer層上的文字(label),但是它們是隱藏的,并且在指定時間出現(xiàn)又在指定時間消失。也就是說要給這個layer加上一個動畫組,第一個動畫是將透明度變?yōu)?,第二個動畫將透明度變?yōu)?
將字幕文件解析后,將動畫組的beginTime設(shè)為開始時間,duration設(shè)為持續(xù)時間,同時將消失動畫的beginTime設(shè)為持續(xù)時間,duration設(shè)為0.1
6. AVVideoCompositionCoreAnimationTool 以及 exporter的賦值
mainCompositionInst.animationTool = [AVVideoCompositionCoreAnimationTool
videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
/// 設(shè)置字幕 layer層
self.exporter.videoComposition = mainCompositionInst;
7.導(dǎo)出
[self.exporter exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
});
}];
感謝愛我你就抱抱我——ios 通過AVFoundation給視頻添加字幕的這篇文章,不然我對添加字幕毫無頭緒。雖然文章里面沒有完整的字幕動畫代碼...
完整代碼如下,這里面的代碼是有實際業(yè)務(wù)需求的代碼,所以會有一些變動。
另外添加字幕label的時候我碰到了一個特別奇怪的問題。如果只添加一個label.layer,沒有問題,如果放入for循環(huán)中循環(huán)添加,一個都加不上去。嘗試半天不知道為什么,最后靈機(jī)一動,加了一句添加label到view上,結(jié)果就可以了。
然后由于label添加的字幕清晰度不行,暫時也不知道是視頻合成的原因還是說自己分辨率造成的。所以換成了CATextLayer,比label清晰了那么一丟丟。但是發(fā)現(xiàn)我設(shè)置了textLayer.contentsScale = [UIScreen mainScreen].scale;反而更不清晰了,所以就屏蔽了這個contentsScale
/**
@param videos 需要被合成的音頻文件
@param audios 需要被合成的視頻文件
*/
- (void)compositionVideos:(NSArray <NSURL *>*)videos audios:(NSArray <NSURL *>*)audios
{
[SVProgressHUD setDefaultMaskType:(SVProgressHUDMaskTypeClear)];
[SVProgressHUD showWithStatus:@"正在合成..."];
NSMutableArray *videosAsset = [NSMutableArray arrayWithCapacity:0];
NSMutableArray *audiosAsset = [NSMutableArray arrayWithCapacity:0];
for (NSURL *videoPath in videos) {
AVAsset *asset = [AVAsset assetWithURL:videoPath];
NSLog(@"%@",asset);
[videosAsset insertObject:asset atIndex:videosAsset.count];
}
for (NSURL *audioPath in audios) {
AVAsset *asset = [AVAsset assetWithURL:audioPath];
[audiosAsset insertObject:asset atIndex:audiosAsset.count];
}
// 1 - 創(chuàng)建 AVMutableComposition 對象. 對象將保存AVMutableCompositionTrack實例.
AVMutableComposition *mix = [AVMutableComposition composition];
// 2 - 視頻軌道
AVMutableCompositionTrack *videoTrack = [mix addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
[videosAsset enumerateObjectsUsingBlock:^(AVAsset *asset, NSUInteger idx, BOOL *stop) {
[videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] atTime:videoTrack.asset.duration error:nil];
}];
NSMutableArray *audioTrackes = [NSMutableArray arrayWithCapacity:0];
[audiosAsset enumerateObjectsUsingBlock:^(AVAsset *asset, NSUInteger idx, BOOL *stop) {
AVMutableCompositionTrack *AudioTrack = [mix addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:(int32_t)idx];
[AudioTrack insertTimeRange:CMTimeRangeFromTimeToTime(kCMTimeZero, videoTrack.asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0] atTime:kCMTimeZero error:nil];
[audioTrackes addObject:AudioTrack];
}];
// 4 - 獲取路徑
NSURL *url = [EditAudioVideo exporterVideoPath];
// 5 - 創(chuàng)建導(dǎo)出
self.exporter = [[AVAssetExportSession alloc] initWithAsset:mix presetName:AVAssetExportPreset3840x2160];
//修改背景音樂的音量start
AVMutableAudioMix *videoAudioMixTools = [AVMutableAudioMix audioMix];
//獲取音頻軌道
NSMutableArray *inputParameters = [NSMutableArray arrayWithCapacity:0];
[audiosAsset enumerateObjectsUsingBlock:^(AVAsset *asset, NSUInteger idx, BOOL *stop) {
AVMutableAudioMixInputParameters *audioParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:audioTrackes[idx]];
[audioParameters setVolumeRampFromStartVolume:1.0 toEndVolume:1.0 timeRange:CMTimeRangeMake(kCMTimeZero, CMTimeAdd(kCMTimeZero, asset.duration))];
[audioParameters setTrackID:(int32_t)idx];
[inputParameters addObject:audioParameters];
}];
videoAudioMixTools.inputParameters = inputParameters;
//3.1 AVMutableVideoCompositionInstruction 視頻軌道中的一個視頻,可以縮放、旋轉(zhuǎn)等
AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, videoTrack.asset.duration);
/// 3.2 AVMutableVideoCompositionLayerInstruction 一個視頻軌道,包含了這個軌道上的所有視頻素材
/// 必須依靠它
AVMutableVideoCompositionLayerInstruction *videolayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
//1 創(chuàng)建AVAsset實例 AVAsset包含了video的所有信息 self.videoUrl輸入視頻的路徑
AVAssetTrack *videoAssetTrack = [[videoTrack.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
UIImageOrientation videoAssetOrientation_ = UIImageOrientationUp;
BOOL isVideoAssetPortrait_ = NO;
CGAffineTransform videoTransform = videoAssetTrack.preferredTransform;
if (videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0) {
videoAssetOrientation_ = UIImageOrientationRight;
isVideoAssetPortrait_ = YES;
}
if (videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0) {
videoAssetOrientation_ = UIImageOrientationLeft;
isVideoAssetPortrait_ = YES;
}
if (videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0) {
videoAssetOrientation_ = UIImageOrientationUp;
}
if (videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0) {
videoAssetOrientation_ = UIImageOrientationDown;
}
[videolayerInstruction setTransform:videoAssetTrack.preferredTransform atTime:kCMTimeZero];
[videolayerInstruction setOpacity:0.0 atTime:videoTrack.asset.duration];
mainInstruction.layerInstructions = [NSArray arrayWithObjects:videolayerInstruction,nil];
//AVMutableVideoComposition:管理所有視頻軌道,可以決定最終視頻的尺寸,裁剪需要在這里進(jìn)行
AVMutableVideoComposition *mainCompositionInst = [AVMutableVideoComposition videoComposition];
CGSize naturalSize;
if(isVideoAssetPortrait_){
naturalSize = CGSizeMake(videoTrack.naturalSize.height, videoTrack.naturalSize.width);
} else {
naturalSize = videoTrack.naturalSize;
}
/// 設(shè)置高寬
float renderWidth, renderHeight;
renderWidth = naturalSize.width;
renderHeight = naturalSize.height;
mainCompositionInst.renderSize = CGSizeMake(renderWidth, renderHeight);
mainCompositionInst.instructions = [NSArray arrayWithObject:mainInstruction];
mainCompositionInst.frameDuration = CMTimeMake(1, 30);
/// 加水印 加字幕
// videoLayer是視頻layer,parentLayer是最主要的,videoLayer和字幕,貼紙的layer都要加在該layer上
CALayer *parentLayer = [CALayer layer];
CALayer *videoLayer = [CALayer layer];
parentLayer.frame = CGRectMake(0, 0, renderWidth, renderHeight);
videoLayer.frame = CGRectMake(0, 0, renderWidth, renderHeight);
[parentLayer addSublayer:videoLayer];
for (SubTitleManager *obj in [SubTitleManager manager].subTitles) {
// UILabel * titleLabel = [[UILabel alloc]initWithFrame: CGRectMake(8, 8, kScreenW - 16, 38)];
// titleLabel.attributedText = [[SubTitleManager manager] getSRTSubtitleWithCurrentObj:obj];
// titleLabel.minimumScaleFactor = 9/14;
// titleLabel.adjustsFontSizeToFitWidth = true;
// titleLabel.numberOfLines = 3;
// titleLabel.shadowOffset = CGSizeMake(0, -1);
// titleLabel.textAlignment = NSTextAlignmentCenter;
// titleLabel.layer.opacity = 0;
CATextLayer * textLayer = [CATextLayer layer];
textLayer.string = [[SubTitleManager manager] getSRTSubtitleWithCurrentObj:obj];
textLayer.frame = CGRectMake(8, 8, kScreenW - 16, 38);
textLayer.wrapped = YES;
// textLayer.contentsScale = [UIScreen mainScreen].scale;
textLayer.alignmentMode = kCAAlignmentCenter;
textLayer.truncationMode = kCATruncationNone;
textLayer.opacity = 0;
// 這個是透明度動畫主要是使在插入的才顯示,其它時候都是不顯示的
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1];
opacityAnim.toValue = [NSNumber numberWithFloat:1];
opacityAnim.removedOnCompletion = NO;
CABasicAnimation *opacityDismissAnim = [CABasicAnimation animationWithKeyPath:@"opacity"];
opacityDismissAnim.fromValue = [NSNumber numberWithFloat:1];
opacityDismissAnim.toValue = [NSNumber numberWithFloat:0];
opacityDismissAnim.removedOnCompletion = NO;
opacityDismissAnim.beginTime = obj.endTime - obj.startTime;
opacityDismissAnim.duration = 0.1;
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = [NSArray arrayWithObjects:opacityAnim, opacityDismissAnim, nil];
if (obj.startTime == 0) {
groupAnimation.beginTime = 0.01;
} else {
groupAnimation.beginTime = obj.startTime;
}
groupAnimation.duration = obj.endTime - obj.startTime;
[textLayer addAnimation:groupAnimation forKey:@"groupAnimation"];
[parentLayer addSublayer: textLayer];
}
mainCompositionInst.animationTool = [AVVideoCompositionCoreAnimationTool
videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
self.exporter.outputURL = url;
self.exporter.outputFileType = AVFileTypeQuickTimeMovie;
/// 賦值音頻
self.exporter.audioMix = videoAudioMixTools;
self.exporter.shouldOptimizeForNetworkUse = YES;
/// 設(shè)置字幕 layer層
self.exporter.videoComposition = mainCompositionInst;
__weak typeof(self) wSelf = self;
[self.exporter exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
[SVProgressHUD showWithStatus:@"錄制合成完成"];
[wSelf exportDidFinish:wSelf.exporter];
});
}];
}