本文为该系列的第三篇,主要讲述如何使用 FFmpeg 解码音视频流。在开始之前,我们先了解一下本文涉及到的几个主要类型:
类型 | 描述 |
---|---|
AVCodecParameters | 代表流的参数信息 |
AVCodec | 代表编解码器,如:h264、vp9、png、aac |
AVCodecContext | 用于执行实际的编解码操作 |
AVFrame | 该类型用于存储解码过后的音视频数据,对视频来说,每个数据包包含一帧数据,对音频来说,每个数据包包含多个采样数据 |
首先,如前文所述,我们需要为输入文件创建一个 AVFormatContext
对象:
let fmtCtx = try AVFormatContext(url: CommandLine.arguments[1])
try fmtCtx.findStreamInfo()
然后使用已获取到的流信息查找对应的解码器并创建 AVCodecContext
对象用于后续的解码操作:
guard let codec = AVCodec.findDecoderById(stream.codecParameters.codecId) else {
fatalError("The codec '\(stream.codecParameters.codecId)' is not supported.")
}
let decoder = AVCodecContext(codec: codec)
将流的参数信息传递给解码器并通过调用 openCodec(_:options:)
方法来初始化解码器:
decoder.setParameters(stream.codecParameters)
try decoder.openCodec()
至此,准备工作已经完成,现在正式开始我们的解码工作。首先是从输入文件读取压缩过的音视频数据包并将其写入到传入的 AVPacket
对象里:
let pkt = AVPacket()
while let _ = try? fmtCtx.readFrame(into: pkt) {
// 由于 AVPacket 内部的内存管理方式类似于引用计数,每次调用 `readFrame(into:)` 都会对 pkt 执行 `ref()` 操作,
// 因此,pkt 用过之后要记得执行 `unref()` 以免引起内存泄漏
pkt.unref()
}
然后调用解码器的 sendPacket(_:)
方法将编码的数据包发送给解码器进行解码:
try decoder.sendPacket(pkt)
最后通过调用解码器的 receiveFrame(_:)
方法来将解码后的数据保存到传入的 AVFrame
对象里并按照类型将解码后的数据写到文件里。由于一个 AVPacket
可能对应多个 AVFrame
(主要针对音频数据),因此我们需要循环调用:
let frame = AVFrame()
while true {
do {
try decoder.receiveFrame(frame)
} catch let err as AVError where err == .tryAgain || err == .eof {
break
}
if decoder.mediaType == .video {
// 对于数据以平面模式存储的(如:YUV420P),只保存第一组数据(对 YUV420P 格式来讲,只保存 Y)
let linesize = Int(frame.linesize[0])
for i in 0..<frame.height {
fwrite(frame.data[0]!.advanced(by: i * linesize), 1, frame.width, file)
}
} else {
// 当前写法只针对以平面模式存储的数据
let bps = decoder.sampleFormat.bytesPerSample
for i in 0..<frame.sampleCount {
for j in 0..<frame.channelCount {
fwrite(frame.data[j]!.advanced(by: bps * i), 1, bps, file)
}
}
}
// 由于 AVFrame 内部的内存管理方式类似于引用计数,每次调用 `receiveFrame(_:)` 都会对 frame 执行 `ref()` 操作,
// 因此,frame 用过之后要记得执行 `unref()` 以免引起内存泄漏
frame.unref()
}
完整代码如下:
import Darwin
import SwiftFFmpeg
func openFile(stream: AVStream) -> UnsafeMutablePointer<FILE> {
let input = CommandLine.arguments[1]
let suffix = stream.mediaType == .video ? "rawvideo" : "rawaudio"
let output = "\(input[..<(input.firstIndex(of: ".") ?? input.endIndex)])_\(stream.index).\(suffix)"
print(output)
guard let file = fopen(output, "wb") else {
fatalError("Failed allocating output stream.")
}
return file
}
func makeDecoder(stream: AVStream) throws -> AVCodecContext {
guard let codec = AVCodec.findDecoderById(stream.codecParameters.codecId) else {
fatalError("The codec '\(stream.codecParameters.codecId)' is not supported.")
}
let decoder = AVCodecContext(codec: codec)
decoder.setParameters(stream.codecParameters)
try decoder.openCodec()
return decoder
}
func decode(decoder: AVCodecContext, pkt: AVPacket?, frame: AVFrame, file: UnsafeMutablePointer<FILE>) throws {
try decoder.sendPacket(pkt)
while true {
do {
try decoder.receiveFrame(frame)
} catch let err as AVError where err == .tryAgain || err == .eof {
break
}
if decoder.mediaType == .video {
// 对于数据以平面模式存储的(如:YUV420P),只保存第一组数据(对 YUV420P 格式来讲,只保存 Y)
let linesize = Int(frame.linesize[0])
for i in 0..<frame.height {
fwrite(frame.data[0]!.advanced(by: i * linesize), 1, frame.width, file)
}
} else {
// 当前写法只针对以平面模式存储的数据
let bps = decoder.sampleFormat.bytesPerSample
for i in 0..<frame.sampleCount {
for j in 0..<frame.channelCount {
fwrite(frame.data[j]!.advanced(by: bps * i), 1, bps, file)
}
}
}
// 由于 AVFrame 内部的内存管理方式类似于引用计数,每次调用 `receiveFrame(_:)` 都会对 frame 执行 `ref()` 操作,
// 因此,frame 用过之后要记得执行 `unref()` 以免引起内存泄漏
frame.unref()
}
}
func decodeVideo() throws {
if CommandLine.argc < 2 {
print("Usage: \(CommandLine.arguments[0]) input_file")
return
}
let fmtCtx = try AVFormatContext(url: CommandLine.arguments[1])
try fmtCtx.findStreamInfo()
fmtCtx.dumpFormat(isOutput: false)
var streamMapping = [Int: (AVCodecContext, UnsafeMutablePointer<FILE>)]()
for istream in fmtCtx.streams where istream.mediaType == .audio || istream.mediaType == .video {
let decoder = try makeDecoder(stream: istream)
let file = openFile(stream: istream)
streamMapping[istream.index] = (decoder, file)
}
let pkt = AVPacket()
let frame = AVFrame()
while let _ = try? fmtCtx.readFrame(into: pkt) {
if let (decoder, file) = streamMapping[pkt.streamIndex] {
try decode(decoder: decoder, pkt: pkt, frame: frame, file: file)
}
pkt.unref()
}
// 刷新解码器内部的缓冲区
try streamMapping.values.forEach { decoder, file in
try decode(decoder: decoder, pkt: nil, frame: frame, file: file)
}
}
try decodeVideo()
运行程序你将看到在输入文件的目录下产生了对应的音视频解码后的文件,我们可以使用 ffplay 进行播放,如:
# 视频(像素格式及尺寸参数请按照实际情况传入)
ffplay -f rawvideo -pixel_format gray -video_size 1920x1080 input_file
# 音频(采样格式、声道数量及采样率请按照实际情况传入)
ffplay -f fltp -ac 2 -ar 48000 input_file
至此,关于音视频解码的介绍告一段落,下一篇文章将讲解如何利用硬件加快解码速度。
网友评论