美文网首页FFmpegiOS-FFmpeg音视频开发FFmpeg
FFmpeg笔记:01 - 分离音视频流

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

作者: sun__x | 来源:发表于2019-01-20 20:54 被阅读5次

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

    然后我们需要通过 AVFormatContext 读取更多的数据以便准确的定位输入文件的各个流(主要针对那些缺少头部信息的格式,如:flv,像 mp4 格式在上一步就已经获取到了流信息),通过调用 findStreamInfo(options:) 方法可以完成这一步:

    try fmtCtx.findStreamInfo()
    

    至此,我们已经成功的获取到了输入文件的各种音视频流,可以开始下一步的工作了。首先,让我们使用 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
    

    接下来是为每个流创建对应的输出文件:

    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()
    

    运行程序你将看到在输入文件的目录下产生了对应的音视频流文件,我们可以使用 fflay 进行播放,如: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()
    

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

    相关文章

      网友评论

        本文标题:FFmpeg笔记:01 - 分离音视频流

        本文链接:https://www.haomeiwen.com/subject/xpsfjqtx.html