如果需求比较简单,比如只需要修改视频格式或者修剪音视频的长度,使用 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 来配合完成。
现在短视频软件很火,出于服务器性能和容量的考虑,一般会要求上传到服务器的视频的大小,格式要统一。所以指定一些特定的参数是很有必要的,比如比特率这种对视频大小影响很大的参数是一点要指定的。
特定参数
比特率
在数字多媒体领域,比特率是单位时间播视频或音频的数据量。比特率越高,传送的数据越大,还原后的音质、画质就越好,在视频领域,比特率常翻译为码率。比特率是视频质量的最关键因素之一。比特率是音视频大小的关键因素。
可以用这个公示初略来计算视频大小。
帧速率
帧速率也称为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个参数,对视频的质量和大小起了至关重要的作用,考虑到服务器的性能,以及数据传输时间,视频大小不能太大,但是又要保证视频的质量,所以合理控制这些参数很重要。
以上是一些推荐参数标准。
项目转换视频要求的参数
参数 | 值 |
---|---|
视频格式 | 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
每个 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 导出我们自定义参数音视频核心流程及代码。详细文档和代码可以查看参考资料。