一:前言
FFmpeg 是一个非常强大的多媒体框架,它可以用来处理视频和音频数据。它包括了命令行工具 ffmpeg
、ffplay
、ffprobe
等,以及一套可以用来开发多媒体应用的库(libavcodec、libavformat、libavutil、libswscale 等)。FFmpeg 支持多种音视频格式的转换、编码、解码、流处理等操作。
RTSP(Real Time Streaming Protocol)是一种网络控制协议,用于建立和控制媒体流的会话。它常用于流媒体应用,比如视频监控系统。RTSP 允许客户端控制媒体流,如播放、暂停、快进等,并且可以支持多种流媒体传输协议,如 RTP(Real-time Transport Protocol)。
二:功能分析
功能要求:支持多路流媒体信号同时进行解码和保存也就是录播功能。
功能分析:要想实现多路流媒体信号同时进行解码功能的实现,首先会想到一个问题,那就是同步性,对于多路并非运行而言,其多路从事的其实是一个工作,那就是通过avformat_open_input()打开对应的流文件,然后再对这个流文件进行处理。
功能实现:更具功能分析可以得到一个实现方案,那就是使用线程并发性的特点,利用多线程互不干扰同时进行的特点来实现多路的流媒体并发解码。下面的示例为两路流并发运行的例子,多路流同样的道理有多少路采用多少线程。思路清晰,条理清晰,直接上具体实现功能。
三:功能实现
单路拉流解码流程
1:打开输入流文件
使用ffmpeg的接口函数:
cpp
avformat_alloc_context()
创建一个存储多媒体容器格式信息的结构体。然后用:avformat_open_input()打开流文件。
cpp
int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);
其中的第一个参数就是上面函数的返回值,第二个参数就是流地址的RUL,第三个参数为NULL,第四个参数的作用为设置一些解复用器特定的选项,rtsp的默认传输是UDP,可以通过这个参数加上av_dict_set()函数来修改为 TCP协议。
2:寻找媒体信息
cpp
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
第一个参数为上面的创建结构体的函数返回值,第二个为NULL。
然后访问里面的流媒体信息。
fctx->nb_streams;//有几个流媒体
fctx->streams[0];//一般为视频流媒体
fctx->streams[1];//音频
3:创建上下文
cpp
avcodec_alloc_context3(NULL)
cpp
int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *params);
拷贝流媒体中的数据。
fctx->streams[video_index]->codecpar
4:寻找解码器
cpp
AVCodec *avcodec_find_decoder(enum AVCodecID id);
通过ID来寻找对应的解码器。
5:绑定上下文
打开编码器;avcodec_open2();
cpp
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
6:编码并保存
接口函数:
cpp
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *pkt);
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
分别的功能为:
向输入流中读取一个数据包
向解码器发送一个压缩数据包
从解码器中接收解码后的帧。
基本解码功能就结束了,要注意资源的回收和释放
av_frame_unref();
释放 AVFrame
结构体中的所有引用的资源
av_packet_unref();
用于释放 AVPacket
结构体中的所有资源。
两路拉流功能实现
两路是用俩线程分别实现俩路解码的过程,解码流程大体不变,不一样的点就是加入两个线程。具体代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavdevice/avdevice.h>
#include <pthread.h>
// 定义视频流索引
int video_index1 = -1;
int video_index2 = -1;
AVFormatContext *fctx1 = NULL;
AVFormatContext *fctx2 = NULL;
// 线程处理函数
void *decode_stream(void *arg)
{
AVFormatContext *format_ctx = (AVFormatContext *)arg;
AVCodecContext *codec_ctx = NULL;
AVCodec *codec = NULL;
AVPacket packet;
AVFrame *frame = av_frame_alloc();
int ret;
FILE *file = NULL;
// 寻找视频流
int video_stream_index = video_index1;
if (format_ctx == fctx2)
{
video_stream_index = video_index2;
file = fopen("2.yuv", "w+");
} else
{
file = fopen("1.yuv", "w+");
}
if (!file)
{
printf("创建YUV文件失败\n");
return NULL;
}
// 获取解码器上下文
codec_ctx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(codec_ctx, format_ctx->streams[video_stream_index]->codecpar);
// 寻找解码器
codec = avcodec_find_decoder(codec_ctx->codec_id);
if (codec == NULL)
{
printf("未找到解码器\n");
fclose(file);
return NULL;
}
// 打开解码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0)
{
printf("无法打开解码器\n");
fclose(file);
return NULL;
}
// 读取数据并解码
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
while (av_read_frame(format_ctx, &packet) >= 0)
{
if (packet.stream_index == video_stream_index)
{
ret = avcodec_send_packet(codec_ctx, &packet);
if (ret < 0)
{
printf("发送数据到解码器失败\n");
break;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
} else if (ret < 0)
{
printf("解码失败\n");
break;
}
// 写入YUV数据
fwrite(frame->data[0], 1, frame->linesize[0] * frame->height, file);
fwrite(frame->data[1], 1, frame->linesize[1] * frame->height / 2, file);
fwrite(frame->data[2], 1, frame->linesize[2] * frame->height / 2, file);
av_frame_unref(frame);
}
av_packet_unref(&packet);
}
}
// 释放资源
if (frame)
{
av_frame_free(&frame);
}
if (codec_ctx)
{
avcodec_free_context(&codec_ctx);
}
if (file)
{
fclose(file);
}
return NULL;
}
int main()
{
avformat_network_init();
AVDictionary *options1 = NULL;
AVDictionary *options2 = NULL;
//定义两路流媒体URL
char filepath1[] = "rtsp://192.168.xx.xx/xx/xx";//根据自己的URL设置
char filepath2[] = "rtsp://xxx.xxx.xx.xx.";
av_dict_set(&options1, "buffer_size", "1080000", 0);
av_dict_set(&options1, "rtsp_transport", "tcp", 0);
av_dict_set(&options1, "stimeout", "5000000", 0);
av_dict_set(&options1, "max_delay", "500000", 0);
av_dict_set(&options2, "buffer_size", "720000", 0);
av_dict_set(&options2, "rtsp_transport", "tcp", 0);
av_dict_set(&options2, "stimeout", "5000000", 0);
av_dict_set(&options2, "max_delay", "500000", 0);
//1:打开输入流文件
fctx1 = avformat_alloc_context();
fctx2 = avformat_alloc_context();
if (avformat_open_input(&fctx1, filepath1, NULL, &options1) < 0)
{
printf("第一路流文件打开失败\n");
return -1;
}
if (avformat_open_input(&fctx2, filepath2, NULL, &options2) < 0)
{
printf("第二路流文件打开失败\n");
avformat_close_input(&fctx1);
avformat_free_context(fctx1);
return -1;
}
if (avformat_find_stream_info(fctx1, NULL) < 0)
{
printf("第一路流文件非流媒体文件\n");
avformat_close_input(&fctx1);
avformat_close_input(&fctx2);
avformat_free_context(fctx1);
avformat_free_context(fctx2);
return -1;
}
if (avformat_find_stream_info(fctx2, NULL) < 0)
{
printf("第二路流文件非流媒体文件\n");
avformat_close_input(&fctx1);
avformat_close_input(&fctx2);
avformat_free_context(fctx1);
avformat_free_context(fctx2);
return -1;
}
//寻找对应的视频流索引
for (int i = 0; i < fctx1->nb_streams; i++)
{
if (fctx1->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_index1 = i;
break;
}
}
for (int i = 0; i < fctx2->nb_streams; i++)
{
if (fctx2->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_index2 = i;
break;
}
}
//创建线程
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, decode_stream, fctx1);
pthread_create(&tid2, NULL, decode_stream, fctx2);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
avformat_close_input(&fctx1);
avformat_close_input(&fctx2);
avformat_free_context(fctx1);
avformat_free_context(fctx2);
return 0;
}