FFmpeg学习(二):协议和自定义IO模式

411 阅读7分钟

0.前言

前文我们讨论了FFmpeg中的封装和解封装操作,了解到了ffmpeg支持很多种不同格式的网络流媒体协议,只需要输入对应的url就可以进行操作。本文将讨论FFmpeg是如何实现对不同协议的支持,以及一种常用的自定义IO的多媒体文件处理方式。

1. 介绍

1.1 协议

首先需要明确的是“协议”的概念,准确上来说,FFmpeg中“协议”指的是“承载多媒体容器文件的传输协议”,也就是“传输多媒体文件的方式”,其最终要获得的净数据是“多媒体容器文件”,比如mp4,flv等。
所以ffmpeg支持协议的过程便是,用户输入一种传输协议(的标志),就是我们输入的url,从中提取出我们需要的“多媒体容器文件”。

1.2 AVIO

AVIO实际上是libavformat里面的一个子模块,头文件在“libavformat/avio.h”主要是提供“带缓存的IO输出接口”,我们可以类比C标准库中“stdio.h”的作用。除此之外,AVIO库还支持根据用户输入不同的url,选择对应的传输协议,在此基础之上完成正确的IO操作。我们可以根据下面脑图的梳理,对AVIO库有一个大致的印象。 AVIO.h.png

AVIO支持两种输入方式,一是以URL作输入,FFmpeg内部会识别url的协议类型,底层会使用协议对应的读写操作。一是以自定义参数和读写方法为输入,用户可以提供自定义的buffer的读写接口,也就是我们后续谈论的自定义IO。

1.3 URL模式

我们可以根据下图来理解一下ffmpeg对于URL输入,和协议解析的整体结构设计。
首先,正如前文我们使用解封装时,需要使用avio_open2()打开文件,AVFormatContext的pb成员指向的是一个AVIOContext结构,对应的是一个输入/输出时的IO上下文。
以url作为输入的AVIOContext,此时opaque字段会指向一个URLContext结构体,这个结构体并未存在于FFmpeg公开的头文件中,换言之,协议只用于FFmpeg内部私有。如果我们想添加一个私有的协议,则必须修改FFmpeg的代码。 URLContext实际上是一个所有不同协议的公共上下文,存放着一些都会用到的字段。最主要的是其中的两个成员。
URLContext的prot字段,指向一个URLProtocol对象,根据输入url所用的协议的不同,其具体可能是ff_file_protocol(使用本地文件),ff_rtmp_protocol(RTMP流媒体协议),ff_udp_protocol(UDP协议)等,这些具体的协议对象主要是标明不同协议的具体行为,有着URLProtocol所定义的不同的接口实现。这些协议对象是在FFmpeg编译时创建,并保存在url_protocols这个数组中,当我们输入url时会根据其进行判断和选择。 URLContext的priv_data字段,指向的是不同协议的私有数据上下文,不同的协议对应的均不相同,如ff_file_protocol文件协议对应的上下文为FileContext结构体,URLProtocol结构体中priv_ata_size便是对应的私有Context的大小。

AVIOContext.png

1.4 自定义IO模式

虽然AVIO的URL输入模式可以很方便地支持不同协议的IO操作,但是比如当我们的输入数据已经处于内存中,需要从内存中读入时,这种输入方式便不可用。所幸的是,AVIO也支持自定义的方式来读写数据。
如果我们需要使用到自定义IO模式,我们需要手动创建AVIOContext,并将可能用到的自定义readwriteseek方法,以及输入自定义参数设置给AVIOContext的opaque字段,在后续使用AVFormatContext进行读写时,会回调我们输入的方法。
我们主要是使用avio_alloc_context()来定制我们的IO操作,其接口定义如下:

/*
 * buffer : AVIO使用到的读写缓存区
 * buffer_size : buffer的大小
 * write_flag : buffer是否可写
 * opaque : 自定义输入参数
 * read_packet : read方法函数指针, av_read_frame()调用时会回调该方法
 * write_packet : write方法函数指针, av_write_frame()等调用时会回调
 * seek : seek方法函数指针,当以字节为单位seek相关时会回调
*/
AVIOContext *avio_alloc_context(
                  unsigned char *buffer,
                  int buffer_size,
                  int write_flag,
                  void *opaque,
                  int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int64_t (*seek)(void *opaque, int64_t offset, int whence));

另外,需要说明的是,由于opaque字段并不限定类型类型,所以我们可以传入不同的数据类型,并不限定于是一段内存,来实现对输入输出进行控制。比如,我们可以传入一个socket对象、fifo对象,或一个ssl的socket,这样都是可以的。
可以看到上面URL模式下AVIO的句柄也是在opaque字段,换言之我们也可以通过自定义IO的方式,实现自定义协议的支持,除此之外,类似于本地缓存的需要,也可以用该方式实现。

2. 示例

下面以一个示例来讲解AVIO的自定义IO模式的用法。以下示例已上传至github.com/zzakafool/M… 文件夹:4_BufferedIO
在下面例子中,我们映射了一个视频,将其完全读入到内存中,并通过内存拷贝的方式,自定义设置读取方法,解封装后统计视频里的所有帧数(包括不同的流)。

#include "bufferedIO.h"

extern "C" {
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
}

const int AVIO_BUFFER_SIZE = 4096;

struct InputBufferData {
    uint8_t *ptr = nullptr;
    int size = 0;
    int pos = 0;
};

int buffer_read_packet(void *opaque, uint8_t *buf, int buf_size) {
    InputBufferData *ibd = (InputBufferData *)opaque;
    // 超出则返回eof, 0
    if(ibd->pos >= ibd->size) {
        return 0;
    }

    // 只能读到末尾,防止超出
    int read_len = buf_size;
    if(ibd->pos + read_len >= ibd->size) {
        read_len = ibd->size - ibd->pos;
    }

    memcpy(buf, ibd->ptr + ibd->pos, read_len);
    ibd->pos += read_len;

    return read_len;
}

void bufferedIO(uint8_t *buffer, int size) {
    av_log(NULL, AV_LOG_INFO, "buffer size : %d\n", size);
    // 将输入的文件buffer放入结构体中保存
    InputBufferData ibd = {
        .ptr = buffer,
        .size = size,
    };

    // 创建AVFormatContext
    AVFormatContext *fmtCtx = nullptr;
    fmtCtx = avformat_alloc_context();
    if(fmtCtx == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Allocate avformat context failed");
        return;
    }

    // 创建AVIOContext用到的IO缓存区
    unsigned char* avio_ctx_buffer = (unsigned char *)av_malloc(AVIO_BUFFER_SIZE);
    // 创建AVIOContext,并设置自定义的buffer_read_packet
    AVIOContext *ioCtx = avio_alloc_context(avio_ctx_buffer, AVIO_BUFFER_SIZE, 0, &ibd, buffer_read_packet, NULL, NULL);
    if(ioCtx == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Allocate AVIOContext failed");
        avformat_free_context(fmtCtx);
        return;
    }
    // 设置给AVFormatContext
    fmtCtx->pb = ioCtx;

    // 剩下的就是解封装流程
    if(avformat_open_input(&fmtCtx, NULL, NULL, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "AVFormat open input failed");
        avio_context_free(&ioCtx);
        avformat_free_context(fmtCtx);
        return;
    }

    // 解析流信息
    if(avformat_find_stream_info(fmtCtx, NULL) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Find stream info failed");
        avio_context_free(&ioCtx);
        avformat_close_input(&fmtCtx);
    }

    AVPacket *packet = av_packet_alloc();
    if(packet == nullptr) {
        av_log(NULL, AV_LOG_ERROR, "Allocate Packet failed.\n");
        avio_context_free(&ioCtx);
        avformat_close_input(&fmtCtx);
        return;
    }

    // 统计帧数
    int packet_num = 0;
    while(av_read_frame(fmtCtx, packet) >= 0) {
        ++packet_num;
        // do something for compressed data.
        av_packet_unref(packet);
    }
    av_log(NULL, AV_LOG_INFO, "Total %d packets\n", packet_num);

    av_packet_free(&packet);
    avio_context_free(&ioCtx);
    avformat_close_input(&fmtCtx);
}

main函数:

#include "bufferedIO.h"
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#include <sys/mman.h>

int main() {
    // 打开测试资源
    int fd = open("../res/big_buck_bunny.mp4", O_RDONLY);
    if(fd < 0) {
        std::cerr << "Open file failed" <<std::endl;
        return -1;
    }

    // 获取文件长度
    int len = lseek(fd, 0, SEEK_END);
    if(len == -1) {
        std::cerr << "Get file len failed" <<std::endl;
        return -2;
    }

    // 映射至内存中
    uint8_t* buf = nullptr;
    buf = (uint8_t *)mmap(nullptr, len, PROT_READ, MAP_SHARED, fd, 0);
    if(buf == nullptr) {
        std::cerr << "Mmap file failed" << std::endl;
        return -3;
    }

    bufferedIO(buf, len);

    // 解除映射
    if(munmap(buf, len) < 0) {
        std::cerr << "Munmap file failed" << std::endl;
        return -4;
    }

    return 0;
}

3. 参考资料

  1. 《FFmpeg内存IO模式(内存区作输入或输出)》: 叶余视听 
  2. 《FFMPEG结构体分析:AVIOContext》: 雷霄骅
  3. 《深入理解FFmpeg》 :刘歧等