使用 FFmpeg 实现 TS 流非文件输出


背景

在视频处理领域,MPEG-2 传输流(TS 流)是一种广泛应用的标准格式,常用于广播电视和流媒体传输。FFmpeg 作为强大的开源音视频处理工具,提供了丰富的 API 来处理 TS 流。通常,我们会将处理后的 TS 流直接输出到文件,但在某些场景下,如实时流媒体传输,我们需要将 TS 流输出到网络或者自定义的缓冲区中,这需要实现自定义 I/O 操作。

实现

FFmpeg 的 AVIOContext 是实现自定义 I/O 操作的关键。AVIOContext 提供了一个抽象的 I/O 层,允许用户自定义读写操作,从而将数据输出到非文件目标,如网络套接字、内存缓冲区等。我们可以通过 avio_alloc_context 函数创建一个自定义的 AVIOContext,并指定读写回调函数,在回调函数中实现自己的 I/O 逻辑。

以下是一个使用 FFmpeg 写 TS 流且不直接输出到文件的 C++ 代码示例:

ts_writer.h

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
}

class TSWriter
{
public:
    TSWriter() = default;
    int open(AVCodecParameters *codecParameters);
    int close();
    int write(AVPacket *pkt);

private:
    static int write_packet(void *opaque, uint8_t *buf, int buf_size);

private:
    AVFormatContext *m_formatContext = nullptr;
    AVIOContext *m_ioContext = nullptr;
    AVStream *m_videoStream = nullptr;
};

ts_writer.cpp

#include "ts_writer.h"
#include <iostream>
#include <cstring>

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

constexpr int IO_BUFFER_SIZE = 32768;

int TSWriter::open(AVCodecParameters *codecParameters) {
    // 分配输出格式上下文
    int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "mpegts", nullptr);
    if (ret < 0) {
        std::cerr << "Failed to allocate format context: " << av_err2str(ret) << std::endl;
        return -1;
    }

    // 分配 I/O 缓冲区
    uint8_t* buffer = static_cast<uint8_t*>(av_malloc(IO_BUFFER_SIZE));
    if (!buffer) {
        std::cerr << "Failed to allocate I/O buffer" << std::endl;
        avformat_free_context(m_formatContext);
        m_formatContext = nullptr;
        return -1;
    }

    // 分配 I/O 上下文
    m_ioContext = avio_alloc_context(buffer, IO_BUFFER_SIZE, AVIO_FLAG_WRITE, this, nullptr, TSWriter::write_packet, nullptr);
    if (!m_ioContext) {
        std::cerr << "Failed to allocate I/O context" << std::endl;
        av_free(buffer);
        avformat_free_context(m_formatContext);
        m_formatContext = nullptr;
        return -1;
    }

    m_formatContext->pb = m_ioContext;

    // 创建视频流
    if (!m_videoStream) {
        m_videoStream = avformat_new_stream(m_formatContext, nullptr);
        if (!m_videoStream) {
            std::cerr << "Failed to create new video stream" << std::endl;
            avio_context_free(&m_ioContext);
            avformat_free_context(m_formatContext);
            m_formatContext = nullptr;
            return -1;
        }

        // 复制编解码器参数
        ret = avcodec_parameters_copy(m_videoStream->codecpar, codecParameters);
        if (ret < 0) {
            std::cerr << "Failed to copy codec parameters: " << av_err2str(ret) << std::endl;
            avio_context_free(&m_ioContext);
            avformat_free_context(m_formatContext);
            m_formatContext = nullptr;
            return -1;
        }

        m_videoStream->codecpar->codec_tag = 0;

        // 写入文件头
        ret = avformat_write_header(m_formatContext, nullptr);
        if (ret < 0) {
            std::cerr << "Failed to write header: " << av_err2str(ret) << std::endl;
            avio_context_free(&m_ioContext);
            avformat_free_context(m_formatContext);
            m_formatContext = nullptr;
            return -1;
        }
    }
    return 0;
}

int TSWriter::close() {
    if (m_formatContext) {
        // 写入文件尾
        int ret = av_write_trailer(m_formatContext);
        if (ret < 0) {
            std::cerr << "Failed to write trailer: " << av_err2str(ret) << std::endl;
        }
        // 释放格式上下文
        avformat_free_context(m_formatContext);
        m_formatContext = nullptr;
    }

    if (m_ioContext) {
        // 释放 I/O 上下文
        avio_context_free(&m_ioContext);
        m_ioContext = nullptr;
    }

    return 0;
}

int TSWriter::write(AVPacket* packet) {
    if (!m_formatContext || !packet) {
        return -1;
    }

    // 写入数据包
    int ret = av_interleaved_write_frame(m_formatContext, packet);
    if (ret < 0) {
        std::cerr << "Error writing packet: " << av_err2str(ret) << std::endl;
        return -1;
    }

    return 0;
}

int TSWriter::write_packet(void* opaque, uint8_t* buf, int buf_size) {
    // 自定义TS Writer 实现
    std::cout << "Writing " << buf_size << " bytes to output" << std::endl;
    return buf_size;
}

关键流程:

  • 分配输出格式上下文:使用 avformat_alloc_output_context2 分配 MPEG-2 TS 格式的输出上下文。
  • 分配 I/O 缓冲区:使用 av_malloc 分配指定大小的 I/O 缓冲区。
  • 分配 I/O 上下文:使用 avio_alloc_context 创建自定义 I/O 上下文,指定写回调函数 TSWriter::write_packet。

write_packet方法就可以拿到ts流内存了,这里可以自行选择写入文件或者上传。

总结

通过自定义 I/O 上下文和写回调函数,可灵活控制 TS 流数据的输出,而不局限于文件输出。

demo地址:ts_demo

Category

Tags