FFmpeg4入门系列教程25:本地文件推流

索引地址:系列教程索引地址

上一篇:FFmpeg4入门系列教程24:搭建UDP/TCP/HTTP(S)/RTP/RTMP/RTSP推流服务器

本地文件推流的流程和FFmpeg4入门系列教程13:h264编码为mp4流程是一样的。

转换流程图为:

flow

注意:

  • 原视频有几条流,推送的输出流就有几条流
  • 本操作不涉及视频编解码,只是视频时间戳转换
  • 与第13篇不同的是,13是本地文件,本篇是流地址。

本文测试用软件为FFmpeg+Easydarwin

输入输出文件

1
2
3
const char *inFilename = "/home/jackey/Videos/test.mp4"; //输入URL
const char *outFilename = "rtsp://localhost/test"; //输出URL
const char *ofmtName = "rtsp";//输出格式;

在第13篇中,输出格式是根据文件名自动判断的,此处的输出文件为流地址,无法判断输出视频格式,需要我们手动指定。

参数设置

根据Easydarwin-GitHub说明,推流地址为:

1
2
3
ffmpeg -re -i C:\Users\Administrator\Videos\test.mkv -rtsp_transport tcp -vcodec h264 -f rtsp rtsp://localhost/test

ffmpeg -re -i C:\Users\Administrator\Videos\test.mkv -rtsp_transport udp -vcodec h264 -f rtsp rtsp://localhost/test

我们要把命令行参数修改为代码

1
2
3
4
AVDictionary *dict = NULL;
av_dict_set(&dict, "rtsp_transport", "tcp", 0);
av_dict_set(&dict, "vcodec", "h264", 0);
av_dict_set(&dict, "f", "rtsp", 0);

输入打开

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
// 1. 打开输入
// 1.1 打开输入文件,获取封装格式相关信息
if ((ret = avformat_open_input(&ifmtCtx, inFilename, 0, &dict)) < 0)
{
printf("can't open input file: %s\n", inFilename);
break;
}

// 1.2 解码一段数据,获取流相关信息
if ((ret = avformat_find_stream_info(ifmtCtx, 0)) < 0)
{
printf("failed to retrieve input stream information\n");
break;
}

// 1.3 获取输入ctx
for (i = 0; i < ifmtCtx->nb_streams; ++i)
{
if (ifmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoIndex = i;
break;
}
}

av_dump_format(ifmtCtx, 0, inFilename, 0);

输出打开

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
// 2. 打开输出
// 2.1 分配输出ctx
avformat_alloc_output_context2(&ofmtCtx, NULL, ofmtName, outFilename);
if (!ofmtCtx)
{
printf("can't create output context\n");
break;
}

for (i = 0; i < ifmtCtx->nb_streams; ++i)
{
// 2.2 基于输入流创建输出流
AVStream *inStream = ifmtCtx->streams[i];
AVStream *outStream = avformat_new_stream(ofmtCtx, NULL);
if (!outStream)
{
printf("failed to allocate output stream\n");
break;
}

// 2.3 将当前输入流中的参数拷贝到输出流中
ret = avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
if (ret < 0)
{
printf("failed to copy codec parameters\n");
break;
}
}

av_dump_format(ofmtCtx, 0, outFilename, 1);

if (!(ofmtCtx->oformat->flags & AVFMT_NOFILE))
{
// 2.4 创建并初始化一个AVIOContext, 用以访问URL(outFilename)指定的资源
ret = avio_open(&ofmtCtx->pb, outFilename, AVIO_FLAG_WRITE);
if (ret < 0)
{
printf("can't open output URL: %s.%d\n", outFilename, ret);
break;
}
}
}

数据处理

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
// 3. 数据处理
// 3.1 写输出文件
ret = avformat_write_header(ofmtCtx, NULL);
if (ret < 0)
{
printf("Error accourred when opening output file\n");
break;
}

startTime = av_gettime();

while (1)
{
AVStream *inStream, *outStream;

// 3.2 从输出流读取一个packet
ret = av_read_frame(ifmtCtx, &pkt);
if (ret < 0)
{
break;
}
//Important:Delay
if (pkt.stream_index == videoIndex)
{
AVRational time_base = ifmtCtx->streams[videoIndex]->time_base;
AVRational time_base_q = {1, AV_TIME_BASE};
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - startTime;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
}

inStream = ifmtCtx->streams[pkt.stream_index];
outStream = ofmtCtx->streams[pkt.stream_index];

/* copy packet */
// 3.3 更新packet中的pts和dts
// 关于AVStream.time_base(容器中的time_base)的说明:
// 输入:输入流中含有time_base,在avformat_find_stream_info()中可取到每个流中的time_base
// 输出:avformat_write_header()会根据输出的封装格式确定每个流的time_base并写入文件中
// AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式AVStream.time_base不同
// 所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
av_packet_rescale_ts(&pkt, inStream->time_base, outStream->time_base);
pkt.pos = -1;

if (pkt.stream_index == videoIndex)
{
printf("send %8d video frames to output URL\n", frameIndex);
frameIndex++;
}

// 3.4 将packet写入输出
ret = av_interleaved_write_frame(ofmtCtx, &pkt);
if (ret < 0)
{
printf("Error muxing packet\n");
break;
}
av_packet_unref(&pkt);
}

// 3.5 写输出文件尾
av_write_trailer(ofmtCtx);

效果为:

result

问题

因为之前在搞RTSP视频推流封面的东西,所以测试用的示例也是使用这个方法。

但是在实际推流过程中,如果你的原视频是包含字幕流的,用这种方法是推不了流的。经过查找,如果要实现字幕流推送,需要修改FFmpeg源码,这个坑先留着。

下一篇:FFmpeg4入门系列教程26:捕获摄像头编码h264并推流