iOS AVFoundation 音视频编辑

如果需求比较简单,比如只需要修改视频格式或者修剪音视频的长度,使用 AVAssetExportSession 就可以简单的实现。

AVAssetExportSession

用于将 AVAsset 内容根据导出预设条件进行转码,并将导出资源写到磁盘中.

//创建需要导出视频的URL路径(如果指定的路径已经存在会导出失败)
let outputUrl = URL(fileURLWithPath: path)
guard let exprotSession = AVAssetExportSession(asset: asset, presetName:  AVAssetExportPresetPassthrough) else {
    return
}
exprotSession.outputURL = outputUrl //指定导出路径
exprotSession.outputFileType = .mp4 //指定视频格式
exprotSession.shouldOptimizeForNetworkUse = true //网络使用的优化是否开启
exprotSession.timeRange = cropRange //指定剪辑的时间范围
// 异步导出视频
exprotSession.exportAsynchronously {
    let status = exprotSession.status
    switch status {
    case .failed:
        print(exprotSession.error)
    case .cancelled:
        print("cancelled")
    case .completed:
        print(exprotSession.asset)
    default:
        break
    }
}

以上我们可以简单轻松的导出一个我们指定的视频。

但是如果需求复杂一点,需要指定视频的帧数,音视频的比特率,指定音视频的编码格式等,AVAssetExportSession 就无法做到了。这时需要使用 AVAssetReader 和 AVAssetWriter 来配合完成。

现在短视频软件很火,出于服务器性能和容量的考虑,一般会要求上传到服务器的视频的大小,格式要统一。所以指定一些特定的参数是很有必要的,比如比特率这种对视频大小影响很大的参数是一点要指定的。

特定参数

比特率

在数字多媒体领域,比特率是单位时间播视频或音频的数据量。比特率越高,传送的数据越大,还原后的音质、画质就越好,在视频领域,比特率常翻译为码率。比特率是视频质量的最关键因素之一。比特率是音视频大小的关键因素。

dhLmdI.png

可以用这个公示初略来计算视频大小。

帧速率

帧速率也称为FPS(Frames PerSecond)的缩写——帧/秒。是指每秒钟刷新的图片的帧数,也可以理解为图形处理器每秒钟能够刷新几次。每秒钟帧数(FPS)越多,所显示的动作就会越流畅。帧速率越高视频也就越大。

分辨率

每英寸像素是多少,16:9比例分辨bai率常见的有:
3840×2160 (超高清 4K),
2560X1440 (2K),
1920×1080 (1080p全高清),
1600×900,
1366×768 ,
1280×720 (720P 高清),
1024×576

以上3个参数,对视频的质量和大小起了至关重要的作用,考虑到服务器的性能,以及数据传输时间,视频大小不能太大,但是又要保证视频的质量,所以合理控制这些参数很重要。

dhZWIf.jpg

以上是一些推荐参数标准。

项目转换视频要求的参数

参数
视频格式 MP4
编码格式 H.264
尺寸 1280x720
视频帧率 30 帧/秒
视频比特率 3〜5Mbps
编码格式 AAC
音频采样率 48 kHz
音频声道 立体声双声道
音频比特率 128kbps

AVAsset

这是 AVFoundation 处理文件的方式。我们将文件从 URL 中读取到 AVAsset 中,以便 AVFoundation 对他进行操作。

AVAssetReader 和 AVAssetWriter

  • AVAssetReader 用于从 AVAssert 实例中读取音视频样本。
  • AVAssetWriter 用于将音视频数据从多个源写入指定文件格式的单个文件。

AVAssetTrack

资产轨道,一个资产可以分为多个轨道。我们这里只用到两种:一种视频轨道,一种音频轨道。

1.创建 AVAssetReader 和 AVAssetWriter

do {
    // asset 需要获取的音视频的
    self._reader = try AVAssetReader(asset: asset)
} catch {
    DispatchQueue.main.async {
        self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
    }
}
do {
    // outputURL 我们指定输出的视频的路径
    // outputFileType 我们指定的输出的视频类型
    self._writer = try AVAssetWriter(outputURL: outputURL, fileType: outputFileType)
} catch {
    DispatchQueue.main.async {
        self._completionHandler?(.failure(NextLevelSessionExporterError.setupFailure))
    }
}

// 设置读取的视频的时间范围
self._reader?.timeRange = self.timeRange
//网络使用的优化是否开启
self._writer?.shouldOptimizeForNetworkUse = self.optimizeForNetworkUse

2.创建 AVAssetReaderOutput

U7oYqI.png

每个 AVAssetReader 对象只能被关联到一个asset, 但是这个 asset 可能包含多个 track. 因此, 在开始读取之前, 需要配置一个 AVAssetReaderOutput 的子类来设置媒体数据的读取方式. AVAssetReaderOutput 有三个子类可以用来读取asset: AVAssetReaderTrackOutput, AVAssetReaderAudioMixOutput 和 AVAssetReaderVideoCompositionOutput.

2.1 创建一个 AVAssetReaderVideoCompositionOutput 用于读取视频数据

// 获取视频 tracks
let videoTracks = asset.tracks(withMediaType: AVMediaType.video)

guard videoTracks.count > 0 else {
    return
}
// 通过 tracks 创建 Output,Setting 设置为nil
self._videoOutput = AVAssetReaderVideoCompositionOutput(videoTracks: videoTracks, videoSettings: nil)
self._videoOutput?.alwaysCopiesSampleData = false //一般都设置false,这样能提升性能
self._videoOutput?.videoComposition = self.createVideoComposition() //AVVideoComposition 定义不同的视频资源在不同的时间范围内的播放方式。(比如可以设置视频每一帧的刷新时间,视频显示大小范围等)

if let videoOutput = self._videoOutput,
    let reader = self._reader {
    // 把output 添加进 reader里
    if reader.canAdd(videoOutput) {
        reader.add(videoOutput)
    }
}

2.2 设置AVMutableVideoComposition

这里主要设置帧数率和视频尺寸。

internal func createVideoComposition() -> AVMutableVideoComposition {
    let videoComposition = AVMutableVideoComposition()
    
    if let asset = self.asset,
        let videoTrack = asset.tracks(withMediaType: AVMediaType.video).first {
        
        let frameRate: Float = 30
        // 设置视频每一帧的刷新时间
        videoComposition.frameDuration = CMTimeMake(value: 1, timescale: Int32(frameRate))

        if let videoConfiguration = self.videoOutputConfiguration {
            
            let videoWidth = videoConfiguration[AVVideoWidthKey] as? NSNumber
            let videoHeight = videoConfiguration[AVVideoHeightKey] as? NSNumber
            
            let width = videoWidth!.intValue
            let height = videoHeight!.intValue
            
            let targetSize = CGSize(width: width, height: height)
            var naturalSize = videoTrack.naturalSize
            
            var transform = videoTrack.preferredTransform
            
            let rect = CGRect(x: 0, y: 0, width: naturalSize.width, height: naturalSize.height)
            let transformedRect = rect.applying(transform)
        
            transform.tx -= transformedRect.origin.x;
            transform.ty -= transformedRect.origin.y;
            
            let videoAngleInDegrees = atan2(transform.b, transform.a) * 180 / .pi
            if videoAngleInDegrees == 90 || videoAngleInDegrees == -90 {
                let tempWidth = naturalSize.width
                naturalSize.width = naturalSize.height
                naturalSize.height = tempWidth
            }
            // 视频显示时的大小范围
            videoComposition.renderSize = naturalSize
            
            // center the video
            
            var ratio: CGFloat = 0
            let xRatio: CGFloat = targetSize.width / naturalSize.width
            let yRatio: CGFloat = targetSize.height / naturalSize.height
            ratio = min(xRatio, yRatio)
            
            let postWidth = naturalSize.width * ratio
            let postHeight = naturalSize.height * ratio
            let transX = (targetSize.width - postWidth) * 0.5
            let transY = (targetSize.height - postHeight) * 0.5
            
            var matrix = CGAffineTransform(translationX: (transX / xRatio), y: (transY / yRatio))
            matrix = matrix.scaledBy(x: (ratio / xRatio), y: (ratio / yRatio))
            transform = transform.concatenating(matrix)
            
            // make the composition
            
            let compositionInstruction = AVMutableVideoCompositionInstruction()
            compositionInstruction.timeRange = CMTimeRange(start: CMTime.zero, duration: asset.duration)
            
            let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
            layerInstruction.setTransform(transform, at: CMTime.zero)
            
            compositionInstruction.layerInstructions = [layerInstruction]
            videoComposition.instructions = [compositionInstruction]
            
        }
    }
    
    return videoComposition
}

2.3 创建一个 AVAssetReaderAudioMixOutput 用于读取音频数据

let audioTracks = asset.tracks(withMediaType: AVMediaType.audio)

guard audioTracks.count > 0 else {
    self._audioOutput = nil
    return
}
self._audioOutput = AVAssetReaderAudioMixOutput(audioTracks: audioTracks, audioSettings: nil)
self._audioOutput?.alwaysCopiesSampleData = false
self._audioOutput?.audioMix = self.audioMix
if let reader = self._reader,
    let audioOutput = self._audioOutput {
    if reader.canAdd(audioOutput) {
        reader.add(audioOutput)
    }
}

2.4 AVAssetWriter

public convenience init(mediaType: AVMediaType, outputSettings: [String : Any]?)

AVAssetWriter 初始化方法,mediatype 输入媒体的类型,outputsettings ,编码附加到输出的媒体的设置,关AVMediaTypeVideo的信息,请参见AVVideoSettings.h,AVMediaTypeAudio的信息,请参见AVAudioSettings.h

2.4 配置 video input

// 设置一些我们自己指定的视频参数
let compressionDict: [String: Any] = [
    AVVideoAverageBitRateKey: NSNumber(integerLiteral: 4000000),//比特率
    AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel,
    AVVideoMaxKeyFrameIntervalKey: NSNumber(integerLiteral: 30),//视频最大帧数
]
var size = self.resolutionForLocalVideo(assert: asset)
if size!.width > size!.height {
    let p = size!.width / size!.height
    size = CGSize(width: 720 * p , height: 720)
}else {
    let p = size!.height / size!.width
    size = CGSize(width: 720 , height: 720 * p)
}
let videoOutputConfiguration = [
    AVVideoCodecKey: AVVideoCodecH264,//视频编码方式
    AVVideoWidthKey: NSNumber(integerLiteral: Int(size!.width)),//宽度
    AVVideoHeightKey: NSNumber(integerLiteral: Int(size!.height)),//高度
    AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,//缩放显示模式
    AVVideoCompressionPropertiesKey: compressionDict
]

// 根据我们自定义的参数,创建 videoInput
if self._writer?.canApply(outputSettings:videoOutputConfiguration, forMediaType: AVMediaType.video) == true {
    self._videoInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoOutputConfiguration)
    self._videoInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
} else {
    return
}

if let writer = self._writer,
    let videoInput = self._videoInput {
    if writer.canAdd(videoInput) {
        writer.add(videoInput)
    }
}

2.4 配置 audio input

// 设定我们自定义音频的参数
let audioOutputConfiguration = [
    AVFormatIDKey: kAudioFormatMPEG4AAC,//音频编码格式
    AVEncoderBitRateKey: NSNumber(integerLiteral: 128000),//比特率
    AVNumberOfChannelsKey: NSNumber(integerLiteral: 2),//音频通道
    AVSampleRateKey: NSNumber(value: Float(48000))//音频采样率
]
guard let _ = self._audioOutput else {
    return
}

self._audioInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioOutputConfiguration)
self._audioInput?.expectsMediaDataInRealTime = self.expectsMediaDataInRealTime
if let writer = self._writer, let audioInput = self._audioInput {
    if writer.canAdd(audioInput) {
        writer.add(audioInput)
    }
}

2.5 编码数据

最后一步就是编码数据了,通过我们创建 output 拿到数据,然后再通过 input 把数据写入文件中。

self._writer?.startWriting()// 开始写入数据
self._reader?.startReading()// 开始读取数据
videoInput.requestMediaDataWhenReady(on: self._inputQueue, using: {
    while videoInput.isReadyForMoreMediaData {
        guard self._reader?.status == .reading && self._writer?.status == .writing,
            let sampleBuffer = videoOutput.copyNextSampleBuffer() else {
            input.markAsFinished()
            return 
        }
        videoInput.append(sampleBuffer)
    }
})

上面编码的是视频数据,在 while 循环中, 我们通过我们创建的 videoOutput 调用 copyNextSampleBuffer 方法获取的视频数据,然后通过 videoInput 调用 append(_ sampleBuffer: CMSampleBuffer) 方法把数据接收。

音频数据编码和视频一样,只需把上面的 output 和 input 替换成我们创建的 audioOutput 和 audioInput。

上述介绍了使用 AVAssetReader 和 AVAssetWriter 导出我们自定义参数音视频核心流程及代码。详细文档和代码可以查看参考资料。

参考资料

Export

NextLevelSessionExporter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345