本文为该系列的第二篇,主要讲述如何使用 FFmpeg 分离容器格式(如:mp4、flv)里面的音视频流。在开始之前,我们先了解一下本文涉及到的几个主要类型:
类型 | 描述 |
---|---|
AVInputFormat | 代表输入格式,如:mp4、flv、rtp、hls |
AVOutputFormat | 代表输出格式,如:mp4、flv、rtp、hls |
AVFormatContext | 代表输入/输出文件,主要用于执行封装/解封装操作 |
AVStream | 代表容器格式里面的流,包含音视频相关的参数信息 |
AVPacket | 该类型用于存储压缩过后的音视频数据,对视频来说,每个数据包包含一帧数据,对音频来说,每个数据包包含多个采样数据 |
首先我们需要创建一个 AVFormatContext
对象,然后调用 openInput(_:format:options:)
方法打开输入文件并读取输入文件的头部数据以确定输入格式、时长等信息,该方法调用成功后我们可以通过 AVFormatContext
的一些属性来看下输入文件:
let fmtCtx = AVFormatContext()
try fmtCtx.openInput(CommandLine.arguments[1])
let inputFormat = fmtCtx.inputFormat
print(inputFormat?.name) // 输入格式名称,如:mov,mp4,m4a,3gp,3g2,mj2
print(inputFormat?.longName) // 输入格式描述,如:QuickTime / MOV
print(fmtCtx.metadata) // 元数据
print(fmtCtx.duration) // 时长
print(fmtCtx.size) // 文件尺寸
复制代码
以上两步操作也可以通过下面的方法完成:
let fmtCtx = try AVFormatContext(url: CommandLine.arguments[1])
复制代码
有些容器格式(如:flv)没有头部数据或者头部数据不足以获取足够的信息,因此推荐使用 findStreamInfo(options:)
方法来避免这种情况,该方法会读取并解码一些数据包来查找缺失的信息:
try fmtCtx.findStreamInfo()
复制代码
至此,我们成功的完成了 AVFormatContext
对象的初始化工作,可以开始下一步的工作了。首先,让我们使用 dumpFormat(url:isOutput:)
方法来看下输入文件的相关信息:
// 由于我们要查看的是输入文件的信息,因此 isOutput 需要传 false
fmtCtx.dumpFormat(isOutput: false)
复制代码
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/sun/AV/凡人修仙传.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf58.12.100
description : Packed by Bilibili XCoder v1.0(fixed_gap:False)
Duration: 00:03:47.77, start: 0.000000, bitrate: 3361 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3239 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 118 kb/s (default)
Metadata:
handler_name : SoundHandler
复制代码
接下来是通过 streams
属性获取输入文件的音视频流,并为每个流创建对应的输出文件:
func openFile(stream: AVStream) -> UnsafeMutablePointer<FILE> {
let input = CommandLine.arguments[2]
let output = "\(input[..<(input.firstIndex(of: ".") ?? input.endIndex)])_\(stream.index).\(stream.codecParameters.codecId.name)"
print(output)
guard let file = fopen(output, "wb") else {
fatalError("Failed allocating output stream.")
}
return file
}
// 字典的 key 为流在容器里面的索引,这样做主要是为了方便后续处理
var streamMapping = [Int: UnsafeMutablePointer<FILE>]()
// 我们目前仅处理音视频流,所以此处需过滤掉所有的非音视频流
for istream in fmtCtx.streams where istream.mediaType == .audio || istream.mediaType == .video {
streamMapping[istream.index] = openFile(stream: istream)
}
复制代码
然后是从输入文件读取压缩过的音视频数据包,我们通过循环调用 readFrame(into:)
方法依次读取输入文件里面的每一个数据包并将其写入到传入的 AVPacket
对象里:
let pkt = AVPacket()
while let _ = try? fmtCtx.readFrame(into: pkt) {
// 由于 AVPacket 内部的内存管理方式类似于引用计数,每次调用 `readFrame(into:)` 都会对 pkt 执行 `ref()` 操作,
// 因此,pkt 用过之后要记得执行 `unref()` 以免引起内存泄漏
pkt.unref()
}
复制代码
最后,让我们将读取到的 AVPacket
按照其所属的流写入到对应的输出文件里面:
// `AVPacket.streamIndex` 代表了该数据包对应的流的索引,根据前面建立的映射关系,我们很容易就可以取到该数据包对应的输出文件
if let file = streamMapping[pkt.streamIndex] {
fwrite(pkt.data, 1, pkt.size, file)
}
复制代码
完整代码如下:
import Darwin
import SwiftFFmpeg
func openFile(stream: AVStream) -> UnsafeMutablePointer<FILE> {
let input = CommandLine.arguments[1]
let output = "\(input[..<(input.firstIndex(of: ".") ?? input.endIndex)])_\(stream.index).\(stream.codecParameters.codecId.name)"
print(output)
guard let file = fopen(output, "wb") else {
fatalError("Failed allocating output stream.")
}
return file
}
func splitStream() 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: UnsafeMutablePointer<FILE>]()
for istream in fmtCtx.streams where istream.mediaType == .audio || istream.mediaType == .video {
streamMapping[istream.index] = openFile(stream: istream)
}
let pkt = AVPacket()
while let _ = try? fmtCtx.readFrame(into: pkt) {
if let file = streamMapping[pkt.streamIndex] {
fwrite(pkt.data, 1, pkt.size, file)
}
pkt.unref()
}
_ = streamMapping.values.map(fclose)
}
try splitStream()
复制代码
运行程序你将看到在输入文件的目录下产生了对应的音视频流文件,我们可以使用 ffplay 进行播放,如:ffplay -i input_file
。
由于部分容器格式(如:flv)分离出的音视频流可能无法直接播放,为此提供以下程序,该程序不再将解析出来的数据包直接写入文件,而是将其封装成一种可播放的容器格式,对于封装的详细过程将在后续的文章里为大家讲解,这里暂不做过多描述。
import SwiftFFmpeg
func makeMuxer(stream: AVStream) throws -> (AVFormatContext, AVStream) {
let input = CommandLine.arguments[1]
let output = "\(input[..<(input.firstIndex(of: ".") ?? input.endIndex)])_\(stream.index).\(stream.codecParameters.codecId.name)"
print(output)
let muxer = try AVFormatContext(format: nil, filename: String(output))
guard let ostream = muxer.addStream() else {
fatalError("Failed allocating output stream.")
}
ostream.codecParameters.copy(from: stream.codecParameters)
ostream.codecParameters.codecTag = 0
if !muxer.outputFormat!.flags.contains(.noFile) {
try muxer.openOutput(url: output, flags: .write)
}
return (muxer, ostream)
}
func splitStream() 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: (AVFormatContext, AVStream)]()
for istream in fmtCtx.streams where istream.mediaType == .audio || istream.mediaType == .video {
streamMapping[istream.index] = try makeMuxer(stream: istream)
}
for (_, (muxer, _)) in streamMapping {
try muxer.writeHeader()
}
let pkt = AVPacket()
while let _ = try? fmtCtx.readFrame(into: pkt) {
if let (muxer, ostream) = streamMapping[pkt.streamIndex] {
let istream = fmtCtx.streams[pkt.streamIndex]
pkt.pts = AVMath.rescale(pkt.pts, istream.timebase, ostream.timebase, AVRounding.nearInf.union(.passMinMax))
pkt.dts = AVMath.rescale(pkt.dts, istream.timebase, ostream.timebase, AVRounding.nearInf.union(.passMinMax))
pkt.duration = AVMath.rescale(pkt.duration, istream.timebase, ostream.timebase)
pkt.position = -1
pkt.streamIndex = ostream.index
try muxer.interleavedWriteFrame(pkt)
}
pkt.unref()
}
for (_, (muxer, _)) in streamMapping {
try muxer.writeTrailer()
}
}
try splitStream()
复制代码
至此,关于音视频流分离的介绍告一段落,下一篇文章将讲解如何对编码的音视频数据进行解码。