使用 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