FFmpeg笔记:01 - 分离音视频流

本文为该系列的第二篇,主要讲述如何使用 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()
复制代码

至此,关于音视频流分离的介绍告一段落,下一篇文章将讲解如何对编码的音视频数据进行解码。

转载于:https://juejin.im/post/5c447e8cf265da616c65e1a9

你可能感兴趣的:(FFmpeg笔记:01 - 分离音视频流)