1.简介

流媒体是使用了流式传输的多媒体应用技术。如下是维基百科关于流媒体概念的定义

流媒体 (streaming media) 是指将一连串的媒体数据压缩后,经过网络分段发送数据,在网络上即时传输影音以供观赏的一种技术与过程,此技术使得数据包得以像流水一样发送;如果不使用此技术,就必须在使用前下载整个媒体文件。

1.1 ffmepeg影音处理的层次

FFmpeg 中对影音数据的处理,可以划分为协议层、容器层、编码层与原始数据层四个层次:

协议层:提供网络协议收发功能,可以接收或推送含封装格式的媒体流。协议层由 libavformat 库及第三方库(如 librtmp)提供支持。

容器层:处理各种封装格式。容器层由 libavformat 库提供支持。

编码层:处理音视频编码及解码。编码层由各种丰富的编解码器(libavcodec 库及第三方编解码库(如 libx264))提供支持。

原始数据层:处理未编码的原始音视频帧。原始数据层由各种丰富的音视频滤镜(libavfilter 库)提供支持。

注:收流与推流的功能,属于协议层的处理

1.2流媒体系统中的角色

流媒体系统是一个比较复杂的系统,简单来说涉及三个角色:流媒体服务器、推流客户端和收流客户端。推流客户端是内容生产者,收流客户端是内容消费者

ffmpeg流媒体处理(收流与推流)_ffmpeg

1.3 收流与推流

如果输入是网络流,输出是本地文件,则实现的是收流功能,将网络流存储为本地文件

如果输入是本地文件,输出是网络流,则实现的是推流功能,将本地文件推送到网络

如果输入是网络流,输出也是网络流,则实现的是转流功能,将一个流媒体服务器上的流推送到另一个流媒体服务器,’

源码

#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
#include <stdbool.h>

#define DEBUG_PRINT 1

#if DEBUG_PRINT
#define LOG(fmt, ...) printf(fmt "\n", ##__VA_ARGS__)
#else
#define LOG(fmt, ...)
#endif

int main(int argc, char **argv) 
{
	AVOutputFormat *ofmt = NULL;
	AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
	AVPacket pkt;

	const char *in_filename, *out_filename;

	int ret, i;
	int stream_index = 0;
	int *stream_mapping = NULL;
	int stream_mapping_size = 0;

	if (argc < 3)
	{
		printf("usage: %s input output\n"
			"API example program to remux a media file with libavformat and libavcodec.\n"
			"The output format is guessed according to the file extension.\n"
			"\n", argv[0]);
		return 1;
	}

	in_filename = argv[1];
	out_filename = argv[2];

	// 打开输入文件
	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) 
	{
		LOG("Could not open input file '%s'", in_filename);
		//goto end;
	}
	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) 
	{
		LOG("Failed to retrieve input stream information");
		//goto end;
	}
	av_dump_format(ifmt_ctx, 0, in_filename, 0);

	// 确定输出格式
	bool push_stream = false;
	const char *ofmt_name = NULL;
	if (strstr(out_filename, "rtmp://") != NULL) 
	{
		push_stream = true;
		ofmt_name = "flv";
	}
	else if (strstr(out_filename, "udp://") != NULL) 
	{
		push_stream = true;
		ofmt_name = "mpegts";
	}
	else 
	{
		push_stream = false;
		ofmt_name = NULL;
	}

	if ((ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename)) < 0)
	{
		LOG("Could not create output context\n");
		goto end;
	}
	stream_mapping_size = ifmt_ctx->nb_streams;
	stream_mapping = (int*)av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
	if (!stream_mapping) {
		ret = AVERROR(ENOMEM);
		goto end;
	}

	ofmt = ofmt_ctx->oformat;
	AVRational frame_rate;
	double duration;

	for (i = 0; i < ifmt_ctx->nb_streams; i++)
	{
		AVStream *out_stream;
		AVStream *in_stream = ifmt_ctx->streams[i];
		AVCodecParameters *in_codecpar = in_stream->codecpar;

		if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
			in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
			in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) 
		{
			stream_mapping[i] = -1;
			continue;
		}
		if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) 
		{
		
			double duration = 0.0;
			frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL);

			if (frame_rate.num > 0 && frame_rate.den > 0) 
			{
				AVRational duration_q = { frame_rate.den, frame_rate.num };
				duration = av_q2d(duration_q);
			}
			else 
			{
				// 可选:设置默认帧率,例如 25fps
				duration = 0.04;
			}
			
		}
		stream_mapping[i] = stream_index++;

		out_stream = avformat_new_stream(ofmt_ctx, NULL);
		if (!out_stream) 
		{
			LOG("Failed allocating output stream\n");
			ret = AVERROR_UNKNOWN;
			goto end;
		}

		ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
		if (ret < 0) 
		{
			LOG("Failed to copy codec parameters\n");
			goto end;
		}
		out_stream->codecpar->codec_tag = 0;
	}

	av_dump_format(ofmt_ctx, 0, out_filename, 1);
	if (!(ofmt->flags & AVFMT_NOFILE))
	{
		if ((ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE)) < 0) 
		{
			LOG("Could not open output file '%s'", out_filename);
			goto end;
		}
	}

	if ((ret = avformat_write_header(ofmt_ctx, NULL)) < 0) 
	{
		LOG("Error occurred when opening output file\n");
		goto end;
	}

	while (1) {
		ret = av_read_frame(ifmt_ctx, &pkt);
		if (ret < 0)
			break;
		if (pkt.stream_index >= stream_mapping_size || stream_mapping[pkt.stream_index] < 0) {
			av_packet_unref(&pkt);
			continue;
		}
		int codec_type = ifmt_ctx->streams[pkt.stream_index]->codecpar->codec_type;
		if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO)) 
		{
			//av_usleep((int64_t)(duration * AV_TIME_BASE));
		}

		pkt.stream_index = stream_mapping[pkt.stream_index];
		AVStream *in_stream = ifmt_ctx->streams[pkt.stream_index];
		AVStream *out_stream = ofmt_ctx->streams[pkt.stream_index];

		av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base);
		pkt.pos = -1;

		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0) {
			LOG("Error muxing packet\n");
			break;
		}
		av_packet_unref(&pkt);
	}
	av_write_trailer(ofmt_ctx);

end:
	avformat_close_input(&ifmt_ctx);

	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_closep(&ofmt_ctx->pb);
	avformat_free_context(ofmt_ctx);
	av_freep(&stream_mapping);

	if (ret < 0 && ret != AVERROR_EOF)
		//printf("Error occurred: %s\n", av_err2str(ret));

	return ret < 0 ? 1 : 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.
  • 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.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.

2.1收流

收流的代码与打开普通文件的代码没有区别,打开输入时,FFmpeg 能识别流协议及封装格式,根据相应的协议层代码来接收流,收到流数据去掉协议层后得到的数据和普通文件内容是一样的,后续的处理流程也就一样了。

2.2推流

推流有两个需要注意的地方。

一是需要根据输出流协议显式指定输出 URL 的封装格式:

bool push_stream = false;
    char *ofmt_name = NULL;
    if (strstr(out_filename, "rtmp://") != NULL) {
        push_stream = true;
        ofmt_name = "flv";
    }
    else if (strstr(out_filename, "udp://") != NULL) {
        push_stream = true;
        ofmt_name = "mpegts";
    }
    else {
        push_stream = false;
        ofmt_name = NULL;
    }
    avformat_alloc_output_context2(&ofmt_ctx, NULL, ofmt_name, out_filename);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

这里只写了两种。rtmp 推流必须推送 flv 封装格式,udp 推流必须推送 mpegts 封装格式,其他情况就当作是输出普通文件。这里使用 push_stream 变量来标志是否使用推流功能,这个标志后面会用到。

二是要注意推流的速度,不能一股脑将收到的数据全推出去,这样流媒体服务器承受不住。可以按视频播放速度(帧率)来推流。因此每推送一个视频帧,要延时一个视频帧的时长。音频流的数据量很小,可以不必关心此问题。

在打开输入 URL 时,获取视频帧的持续时长: 

AVRational frame_rate;
    double duration;
    if (push_stream && (in_codecpar->codec_type == AVMEDIA_TYPE_VIDEO)) 
    {
        frame_rate = av_guess_frame_rate(ifmt_ctx, in_stream, NULL);
        duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在 av_read_frame() 之后,av_interleaved_write_frame() 之前增加延时,延时时长就是一个视频帧的持续时长:

int codec_type = in_stream->codecpar->codec_type;
    if (push_stream && (codec_type == AVMEDIA_TYPE_VIDEO))
    {
        av_usleep((int64_t)(duration*AV_TIME_BASE));
    }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.