背景

在视频处理领域,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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#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