note

科普一下:


还有的别一个流媒体服务器:EasyDarwin(win上方便快捷),好想看到别的推rtsp的流用这个比较多(这官网里面ffmpeg推流时还加了一个参数 -rtsp_transport tcp(udp) 看后续能不能用的上)。

这里有比较多的关于ffmpeg的rtsp、rtmp参数,以及一些常用的视频流转化、处理命令。

ZLMediaKit这个是一个项目,支持rtsp、rtmp推流,类似于easydarwin、srs这种,这个页面有ffmpeg的各种推流命令。

srs、easydarwin、srs的对比,地址

推拉流延迟的一些问题相关的博客:地址1地址2


一、SRS流媒体服务器

​ SRS是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181。 # 所以好像不能推rtsp,一般的实现使用obs读取rtsp流,再用obs推rtmp

1.1. 安装

安装使用的两种方式:

  1. 直接使用官方提供的docker镜像,直接一键开启,方便快捷:

    docker run –rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080
    registry.cn-hangzhou.aliyuncs.com/ossrs/srs:4 ./objs/srs -c conf/srs.conf

  2. 使用源码安装:

    • 如果使用源码安装,直接看它github,非常便捷;
    • 这次是安装在centos_harbor上的,每次开机需要进到里面:
      • cd /opt/srs/trunk
      • 启动服务:./objs/srs -c conf/srs.conf
      • 查看状态:./etc/init.d/srs status
      • 查看日志:tail -n 30 -f ./objs/srs.log
        • 然后我只是只执行了make,没有执行make install ,intall会将其安装在/usr/loacl/srs这个目录里面。
    • 然后看这官方文档,将其设置为开机自启的方法。
    • 这个运行后,会启动8080端口,是一个网页端的管理界面,但是进去后,啥推流的信息都看不到。

1.2. 推流

推流,一共三种方式:



1.3. 拉流及注意事项

拉流:


Tips:


ffmpeg转封装格式说明:


最后关于推流,chatgpt还推荐了一些参数,没试过,放这里作为一个灵感吧:

二、代码推流

2.1. 推本地摄像头

本地摄像头推流的一个参考:以下 Logi C270 HD WebCam 是通过查看列表的命令行获得的名称

我自己本地摄像头推流成功是用这个命令:==ffmpeg -f dshow -i video=”Logi C270 HD WebCam” -r 20 -f flv -y rtmp://192.168.125.128/live/123== # -r 是这是帧率,如果推流不成功,尝试设置音视频推流格式

2.2. python推流

​ python里面的一个推流,记录一下吧,感觉不是很好,最后都是调用ffmpeg,那不如直接ffmpeg命令行操作:(里面也算有==python调用外部程序==,其它用到的时候做个参考)

import cv2
import subprocess

# RTMP服务器地址
rtmp = r'rtmp://192.168.125.128/live/123'   # 后面的123自己随意起的,可改成其它的,如123321/456等等
# 读取视频并获取属性
cap = cv2.VideoCapture(0)
size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
sizeStr = str(size[0]) + 'x' + str(size[1])

# 以后为了方便写,这样写列表太麻烦,可以这样(下面是列出本地所有设备)
command_list = "ffmpeg -list_devices true -f dshow -i dummy".split()

command = ['ffmpeg',
           '-y', '-an',
           '-f', 'rawvideo',
           '-vcodec', 'rawvideo',
           '-pix_fmt', 'bgr24',
           '-s', sizeStr,
           '-r', '25',
           '-i', '-',
           '-c:v', 'libx264',
           '-pix_fmt', 'yuv420p',
           '-preset', 'ultrafast',
           '-f', 'flv',
           rtmp]
pipe = subprocess.Popen(command, shell=False, stdin=subprocess.PIPE)
while cap.isOpened():
    success, frame = cap.read()
    if success:
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
        pipe.stdin.write(frame.tostring())
cap.release()
pipe.terminate()

拉取流:ffplay rtmp:192.168.125.128/live/123

2.3. c++推流

2.3.1 rtsp推rtmp(转封装)

这里就是ffmpeg里面的格式转封装,没有涉及到重新编码,所以packet都只用了一个。

要推流成rtmp就是flv的格式。

​ c++用代码拉取rtsp的流然后推成rtmp的(win下4.几的版本是ok的,ffmpeg version N-107626-g1368b5a725-20220801):这个博客里还有很多ffmpeg采集声音、画面的相关代码。 ​ 以下代码肯定能运行,前提条件是rtsp的编码格式为h.264(或是受电弓的摄像头的H.265X格式,可用VMS软件进行修改),因为以下代码的推流编码格式是直接copy源流的,推成rtmp的要libx264,所若是rtsp原流是h265的,这代码就会出错,提示HEVC的错误。

#include <iostream>
#include <thread>

extern "C" {
#include <libavformat/avformat.h>
#pragma comment(lib, "avformat.lib")

#include <libavcodec/avcodec.h>
#pragma comment(lib, "avcodec.lib")

#include <libavutil/avutil.h>
#pragma comment(lib, "avutil.lib")
}

#define my_av_gettime() \
	std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::system_clock::now().time_since_epoch()).count()

char av_error[AV_ERROR_MAX_STRING_SIZE] = { 0 };
#define av_err2str(errnum) \
    av_make_error_string(av_error, AV_ERROR_MAX_STRING_SIZE, errnum)

// 这个推视频也是ok的
const char* INFILE = "C:\\Users\\Administrator\\Videos\\source.200kbps.768x320.flv";
const char* RTMP = "rtmp://192.168.125.128/live/66";
const char* RTSP = "rtsp://192.168.108.134:554/user=admin&password=&channel=1&stream=1.sdp?";

//const char* in_filename = "C:\\Users\\Administrator\\Desktop\\05\\h264_aac.mp4";
const char* in_filename = "rtsp://192.168.108.135:554/user=admin&password=&channel=1&stream=1.sdp?";
const char* out_filename = "rtmp://192.168.108.218/live/11";

int rtsp2rtmp() {	
    // 输入输出各对应一个AVFormatContext,以后自己用还是写 avformat_alloc_context 函数去分配吧
	AVFormatContext *ifmt_ctx = nullptr;
	AVFormatContext *ofmt_ctx = nullptr;
    
    const AVOutputFormat *ofmt = nullptr;
	// 注意这里:AVPacket *pkt = nullptr; 到了下面av_read_frame函数一定会运行报错,
	// 要么下面这样去使用指针,要么用 AVPacket pkt; 这中实例化对象去写。
	AVPacket *pkt = av_packet_alloc();
    
    int ret = 0;
	int video_index = -1;
	int frame_index = 0;
	int64_t start_time = 0;
	// 这个参数是为了另一种写法
	int64_t Last_pts = AV_NOPTS_VALUE;

	ret = avformat_network_init();  // 初始化网络组件
	if (ret != 0) {
		std::cout << av_err2str(ret) << std::endl;
		goto END;
	}

	// 输入(写NULL、nullptr、0都是一个意思)
	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, nullptr, 0)) < 0) {
		av_log(NULL, AV_LOG_ERROR, "Could not open input file.\n");
		goto end;
	}
	if ((ret = avformat_find_stream_info(ifmt_ctx, nullptr)) < 0) {
		av_log(nullptr, AV_LOG_ERROR, "Failed to retrive input stream information");
		goto end;
	}

	/* 旧版本一般这样去找视频流,新版本也是兼容的,但可能一些代码参数有区别,下面是新版等价的
	for (int i = 0; i < ifmt_ctx->nb_streams; ++i) {
		// 旧的版本里可能是 ->codec->codec_type
		if (ifmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
			video_index = i;
			break;
		}
	}
	*/
    ret = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "av_find_best_stream error.");
		goto end;
	}
	video_index = ret;

	// 打印一下输入信息
	av_dump_format(ifmt_ctx, 0, in_filename, 0);

	// 开始输出("flv"参数可以用这替代:av_guess_format(nullptr, "123.flv", nullptr),重要的是后缀(所以给flv就行),让ffmpeg去推断我们要得格式是哪种的,有需要还可以放 456.mp4 之类的。)
	avformat_alloc_output_context2(&ofmt_ctx, nullptr, "flv", out_filename);   // RTMP
	// avformat_alloc_output_context2(&ofmt_ctx, nullptr, "mpegts", out_filename);   // UDP
	if (!ofmt_ctx) {
		ret = AVERROR_UNKNOWN;
		av_log(NULL, AV_LOG_ERROR, "Could not create output context\n");
		goto end;
	}
	ofmt = ofmt_ctx->oformat;

	// 根据输入流创建输出流 (这就把所有codec参数都存进了 ofmt_ctx )
	for (int i = 0; i < ifmt_ctx->nb_streams; ++i) {
		AVStream *in_stream = ifmt_ctx->streams[i];
		AVStream *out_stream = avformat_new_stream(ofmt_ctx, nullptr);
		if (!out_stream) {
			av_log(NULL, AV_LOG_ERROR, "Failed allocating output stream\n");
			goto end;
		}
		// 复制 AVCodecContext 的设置(老版本的API可能是 avcodec_copy_context )
		ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
		if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Failed to copy context from input to output stream codec");
			goto end;
		}
		// 标记不需要重新编解码 
		out_stream->codecpar->codec_tag = 0;
	}

	// 打印一下输出流的格式 (读里面参数就会懂,最后一个参数:是输出流就给1,不是就给0)
	av_dump_format(ofmt_ctx, 0, out_filename, 1);
    
    
   // 打开输出URL
	if (!(ofmt->flags & AVFMT_NOFILE)) {
		ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
		if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Could not open output URL");
			goto end;
		}
	}
	// 写文件头
	ret = avformat_write_header(ofmt_ctx, nullptr);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "Error occurred when opening output URL\n");
		goto end;
	}
	
    // 下面仅做了输入、输出时间的处理代码,相当于只是做封装转换,未设计到编解码。
	// start_time = av_gettime() 返回从 Unix 纪元(1970 年 1 月 1 日 00:00:00 UTC)以来的时间(以微秒为单位),已被弃用
	start_time = my_av_gettime();
	while (1) {
		AVStream *in_stream, *out_stream;
		// 获取一个AVPacket
		ret = av_read_frame(ifmt_ctx, pkt);
		if (ret < 0) break;

		//FIX:No PTS (Example: Raw H.264)
		//Simple Write PTS, 本来没有pts的话,就自己去写,这样后面代码使用就有了
		if (pkt->pts == AV_NOPTS_VALUE) {
			// write PTS  (注意:时间基 ->time_base 与帧率 ->r_frame_rate 应该是互为倒数)
			AVRational time_base1 = ifmt_ctx->streams[video_index]->time_base;
			// duration between 2 frames(us) 两帧之间的时间间隔,微妙
			// AV_TIME_BASE为宏1000000的定义,这么多微妙刚好为1s,以前常是用1000ms,来除以帧率,得到每两帧的时间间隔
			// r_frame_rate结果是 AVRational 对象,所以再用 av_q2d 函数来算出帧率
			int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ifmt_ctx->streams[video_index]->r_frame_rate);
			pkt->pts = (double)(frame_index * calc_duration) / (double)(av_q2d(time_base1) * AV_TIME_BASE);
			pkt->dts = pkt->pts;
			pkt->duration = (double)calc_duration / (double)(av_q2d(time_base1) * AV_TIME_BASE);
		}

		// 很重要的:Delay(根据帧率去算的,不然本地视频一下就很快完了)
		if (pkt->stream_index == video_index) {
			// 方式一:(还是就用这吧)
			AVRational time_base = ifmt_ctx->streams[video_index]->time_base;
			AVRational time_base_q = { 1, AV_TIME_BASE };  // AV_TIME_BASE_Q
			int64_t pts_time = av_rescale_q(pkt->dts, time_base, time_base_q);
			int64_t now_time = my_av_gettime() - start_time;
			if (pts_time > now_time) {
				std::this_thread::sleep_for(std::chrono::microseconds(pts_time - now_time));
			}

			// 方式二:(思路是来自上一小节的 视频filter,但好像是有点问题,那里是用的 frame->pts)
			// 我这用 pkt->pts 还是 pkt->dts 都能跑,但画面总感觉有些不对,放这里作为了解吧
			/*
			if (Last_pts != AV_NOPTS_VALUE) {
				AVRational time_base = ifmt_ctx->streams[video_index]->time_base;
				int64_t delay = av_rescale_q(pkt->pts - Last_pts, time_base, AVRational{ 1, AV_TIME_BASE });
				if (delay > 0 && delay < 1000000)    // 1000000 us 也就是1s
					std::this_thread::sleep_for(std::chrono::microseconds(delay));
			}
			Last_pts = pkt->pts;
			*/
		}

		in_stream = ifmt_ctx->streams[pkt->stream_index];
		out_stream = ofmt_ctx->streams[pkt->stream_index];
		/* copy packet:这里是没用重新编解码的 */
		// Convert PTS/DTS
		// av_rescale_q_rnd:是将时间戳从前者时间基转换到后者时间基,可看:https://zhuanlan.zhihu.com/p/468346396
		enum AVRounding rnd = static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
		pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base, rnd);
		pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base, rnd);
		pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base);
		pkt->pos = -1;

		if (pkt->stream_index == video_index) {
			std::cout << "Send " << frame_index++ << "video frames to output URL\n";
		}

		// 音视频交织写入,如果只写视频,可能就是 av_write_frame(ofmt_ctx, pkt);
		ret = av_interleaved_write_frame(ofmt_ctx, pkt);
		if (ret < 0) {
			av_log(NULL, AV_LOG_ERROR, "Error muxing packet\n");
			break;
		}

		// 一帧用完后一定要 unref
		av_packet_unref(pkt);

	}

	// 写文件尾
	av_write_trailer(ofmt_ctx);
end:
	// 这里没用 avformat_alloc_context 去创建 ifmt_ctx,就可以不去释放,前面那种写法也并非是唯一
	if (!ifmt_ctx) {
		avformat_close_input(&ifmt_ctx);
	}

	if (!pkt) {
		av_packet_free(&pkt);
	}

	// 关闭输出
	if (ofmt_ctx && ofmt && !(ofmt->flags & AVFMT_NOFILE)) {
		avio_close(ofmt_ctx->pb);
		avformat_free_context(ofmt_ctx);
	}

	avformat_network_deinit();

	// 这种goto end的写法,这里是必要的,中途遇错来到这里,就好提示并退出
	if (ret < 0 && ret != AVERROR_EOF) {
		std::cerr << "Error occurred: " << av_err2str(ret) << std::endl;
		ret = -1;
	}
    
    return ret;
}

int main() {
	rtsp2rtmp();
	system("pause");
	return 0;
}

说明:

2.3.2 rtsp推rtsp/rtmp(重编码)

​ 下面代码基本通用推流到rtsp、rtmp服务器。==一切延迟对比的画面是按python的画面为标准==(python画面本来就有点延迟,比ffmpeg直接读取展示慢一点,但也还是比较实时)(因为代码中指定推流的编码格式为libx264,所以rtsp源为h264、h265的都可以)(这个当源为视频时会报错,暂时只能拉流媒体的源)



​ 这个是关注的B站北小蔡的代码,讲的是延迟很低,它主要也是针对推流到rtsp服务器,代码也写的比较完善,里面有很多关于ffmpeg的推拉流的代码可以参考。(原始代码里可能因为ffmpeg版本问题,有细微的修改)。(真的投入使用时要再审核一下内存泄露问题,注意内存释放,要多测试)

  1. Log.h:==这里的写法、用法很值得借鉴使用==

    #ifndef Log_H
    #define LOG_H
    //  __FILE__ 获取源文件的相对路径和名字
    //  __LINE__ 获取该行代码在文件中的行号
    //  __func__ 或 __FUNCTION__ 获取函数名
    #define LOGI(format, ...) fprintf(stderr, "[INFO]%s [%s:%d %s()] " format "\n", "_",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
    #define LOGE(format, ...)  fprintf(stderr,"[ERROR]%s [%s:%d %s()] " format "\n","_",__FILE__,__LINE__,__func__ ,##__VA_ARGS__)
       
    #endif // !Log_H
    
  2. StreamPusher.h (vs中的Study项目中,ffmpegStreamPusher中就是这个代码)

    #ifndef STREAMPUSHER_H
    #define STREAMPUSHER_H
       
    #include <stdio.h>
    #include <string>
    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    }
       
    class StreamPusher {
    public:
    	explicit StreamPusher(const char* srcUrl, const char* dstUrl, int dstVideoWidth, int dstVideoHeight, int dstVideoFps);
    	StreamPusher() = delete;
    	~StreamPusher();
    public:
    	void start();
    private:
    	bool connectSrc();
    	void closeConnectSrc();
    	bool connectDst();
    	void closeConnectDst();
       
    private:
    	std::string mSrcUrl;
    	std::string mDstUrl;
       
    	// 源头
    	AVFormatContext *mSrcFmtCtx = nullptr;
    	AVCodecContext  *mSrcVideoCodecCtx = nullptr;
    	AVStream        *mSrcVideoStream = nullptr;
    	int              mSrcVideoIndex = -1;
    	// 以下拉流时更新
    	int       mSrcVideoFps = 25;
    	int       mSrcVideoWidth = 1920;
    	int       mSrcVideoHeight = 1080;
    	int       mSrcVideoChannel = 3;
       
    	//目的
    	AVFormatContext *mDstFmtCtx = nullptr;
    	AVCodecContext  *mDstVideoCodecCtx = nullptr;
    	AVStream        *mDstVideoStream = nullptr;
    	int              mDstVideoIndex = -1;
    	int       mDstVideoFps = 25;//转码后默认fps
    	int       mDstVideoWidth = 1920;//转码后默认分辨率宽
    	int       mDstVideoHeight = 1080;//转码后默认分辨率高
    	int       mDstVideoChannel = 3;
    };
    #endif // !STREAMPUSHER_H
    
  3. StreamPusher.cpp (里面有很多ffmpeg的api函数,以后做相关的可以来参考着看)

    #include <iostream>
    #include "StreamPusher.h"
    #include "Log.h"
    #include <thread>
    extern "C" {
    #include <libavutil/pixfmt.h>
    #include <libswscale/swscale.h>
    #include <libavutil/imgutils.h>
    #include <libavutil/opt.h>
    #include <libavutil/time.h>
    }
       
    #pragma comment(lib, "avcodec.lib")
    #pragma comment(lib, "avformat.lib")
    #pragma comment(lib, "avutil.lib")
    #pragma comment(lib, "swscale.lib")
       
       
    StreamPusher::StreamPusher(const char* srcUrl, const char* dstUrl, int dstVideoWidth, int dstVideoHeight, int dstVideoFps) :
    	mSrcUrl(srcUrl), mDstUrl(dstUrl), mDstVideoWidth(dstVideoWidth), mDstVideoHeight(dstVideoHeight), mDstVideoFps(dstVideoFps) {
    	LOGI("StreamPusher::StreamPusher");
    }
    StreamPusher::~StreamPusher() {
    	LOGI("StreamPusher::~StreamPusher");
    }
       
    void StreamPusher::start() {
    	LOGI("StreamPusher::start begin");
       
    	bool conn = this->connectSrc();
        // 下面这里用 do{...} while(0) 来代替,代码看起来好一点,这嵌套太多层了
    	if (conn) {
    		conn = this->connectDst();
    		if (conn) {
    			// 初始化参数
    			AVFrame* srcFrame = av_frame_alloc();// pkt->解码->frame
    			AVFrame* dstFrame = av_frame_alloc();
       
    			dstFrame->width = mDstVideoWidth;
    			dstFrame->height = mDstVideoHeight;
    			dstFrame->format = mDstVideoCodecCtx->pix_fmt;
    			int dstFrame_buff_size = av_image_get_buffer_size(mDstVideoCodecCtx->pix_fmt, mDstVideoWidth, mDstVideoHeight, 1);
    			uint8_t* dstFrame_buff = (uint8_t*)av_malloc(dstFrame_buff_size);
    			av_image_fill_arrays(dstFrame->data, dstFrame->linesize, dstFrame_buff,
    				mDstVideoCodecCtx->pix_fmt, mDstVideoWidth, mDstVideoHeight, 1);
       
    			SwsContext* sws_ctx_src2dst = sws_getContext(mSrcVideoWidth, mSrcVideoHeight,
    				mSrcVideoCodecCtx->pix_fmt,
    				mDstVideoWidth, mDstVideoHeight,
    				mDstVideoCodecCtx->pix_fmt,
    				SWS_BICUBIC, nullptr, nullptr, nullptr);
       
    			AVPacket srcPkt;//拉流时获取的未解码帧
    			AVPacket* dstPkt = av_packet_alloc();// 推流时编码后的帧
    			int continuity_read_error_count = 0;// 连续读错误数量
    			int continuity_write_error_count = 0;// 连续写错误数量
    			int ret = -1;
    			int64_t frameCount = 0;
    			while (true) {//不中断会继续执行
    				if (av_read_frame(mSrcFmtCtx, &srcPkt) >= 0) {
    					continuity_read_error_count = 0;
    					if (srcPkt.stream_index == mSrcVideoIndex) {
    						// 读取pkt->解码->编码->推流
    						ret = avcodec_send_packet(mSrcVideoCodecCtx, &srcPkt);
    						if (ret == 0) {
    							ret = avcodec_receive_frame(mSrcVideoCodecCtx, srcFrame);
    							if (ret == 0) {
    								frameCount++;
    								//解码成功->修改分辨率->修改编码
       
    								// frame(yuv420p) 转 frame_bgr
    								sws_scale(sws_ctx_src2dst,
    									srcFrame->data, srcFrame->linesize, 0, mSrcVideoHeight,
    									dstFrame->data, dstFrame->linesize);
       
    								//开始编码 start
    								dstFrame->pts = dstFrame->pkt_dts = av_rescale_q_rnd(frameCount, mDstVideoCodecCtx->time_base, mDstVideoStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       								
    								/*
    								原来代码中使用的是 dstFrame->pkt_duration =
    								报错:'AVFrame::pkt_duration': 被声明已否决。 chatgpt解答:表示使用了已经被弃用的 AVFrame::pkt_duration 字段。在新的 FFmpeg 版本中,推荐使用 AVFrame::pkt_duration2 或者 AVFrame::best_effort_timestamp 字段
    								*/
    								dstFrame->best_effort_timestamp = av_rescale_q_rnd(1, mDstVideoCodecCtx->time_base, mDstVideoStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
       
    								dstFrame->pkt_pos = frameCount;
       
    								ret = avcodec_send_frame(mDstVideoCodecCtx, dstFrame);
    								if (ret >= 0) {
    									ret = avcodec_receive_packet(mDstVideoCodecCtx, dstPkt);
    									if (ret >= 0) {
    										// 推流 start
    										dstPkt->stream_index = mDstVideoIndex;
    										ret = av_interleaved_write_frame(mDstFmtCtx, dstPkt);
    										if (ret < 0) {//推流失败
    											LOGI("av_interleaved_write_frame error: ret=%d", ret);
    											++continuity_write_error_count;
    											if (continuity_write_error_count > 5) {//连续5次推流失败,则断开连接
    												LOGI("av_interleaved_write_frame error: continuity_write_error_count=%d,dstUrl=%s", continuity_write_error_count, mDstUrl.data());
    												break;
    											}
    										}
    										else {
    											continuity_write_error_count = 0;
    										}
    										// 推流 end
    									}
    									else {
    										LOGI("avcodec_receive_packet error: ret=%d", ret);
    									}
    								}
    								else {
    									LOGI("avcodec_send_frame error: ret=%d", ret);
    								}
    								//开始编码 end
    							}
    							else {
    								LOGI("avcodec_receive_frame error: ret=%d", ret);
    							}
    						}
    						else {
    							LOGI("avcodec_send_packet error: ret=%d", ret);
    						}
       
    						//                          std::this_thread::sleep_for(std::chrono::milliseconds(1));
    					}
    					else {
    						//av_free_packet(&pkt);//过时
    						av_packet_unref(&srcPkt);
    					}
    				}
    				else {
    					//av_free_packet(&pkt);//过时
    					av_packet_unref(&srcPkt);
    					++continuity_read_error_count;
    					if (continuity_read_error_count > 5) {//连续5次拉流失败,则断开连接
    						LOGI("av_read_frame error: continuity_read_error_count=%d,srcUrl=%s", continuity_read_error_count, mSrcUrl.data());
    						break;
    					}
    					else {
    						std::this_thread::sleep_for(std::chrono::milliseconds(100));
    					}
    				}
    			}
       
    			// 销毁
    			av_frame_free(&srcFrame);
    			//av_frame_unref(srcFrame);
    			srcFrame = NULL;
       
    			av_frame_free(&dstFrame);
    			//av_frame_unref(dstFrame);
    			dstFrame = NULL;
       
    			av_free(dstFrame_buff);
    			dstFrame_buff = NULL;
       
    			sws_freeContext(sws_ctx_src2dst);
    			sws_ctx_src2dst = NULL;
    		}
    	}
    	this->closeConnectDst();
    	this->closeConnectSrc();
       
    	LOGI("StreamPusher::start end");
    }
       
    bool StreamPusher::connectSrc() {
       
    	mSrcFmtCtx = avformat_alloc_context();
       
    	AVDictionary* fmt_options = NULL;
    	av_dict_set(&fmt_options, "rtsp_transport", "tcp", 0); //设置rtsp底层网络协议 tcp or udp
    	av_dict_set(&fmt_options, "stimeout", "3000000", 0);   //设置rtsp连接超时(单位 us)
    	av_dict_set(&fmt_options, "rw_timeout", "3000000", 0); //设置rtmp/http-flv连接超时(单位 us)
    	av_dict_set(&fmt_options, "fflags", "discardcorrupt", 0);
    	//av_dict_set(&fmt_options, "timeout", "3000000", 0);//设置udp/http超时(单位 us)
       
    	int ret = avformat_open_input(&mSrcFmtCtx, mSrcUrl.data(), NULL, &fmt_options);
       
    	if (ret != 0) {
    		LOGI("avformat_open_input error: srcUrl=%s", mSrcUrl.data());
    		return false;
    	}
       
    	if (avformat_find_stream_info(mSrcFmtCtx, NULL) < 0) {
    		LOGI("avformat_find_stream_info error: srcUrl=%s", mSrcUrl.data());
    		return false;
    	}
       
    	// video start
    	for (int i = 0; i < mSrcFmtCtx->nb_streams; i++) {
    		if (mSrcFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    			mSrcVideoIndex = i;
    			break;
    		}
    	}
    	//mSrcVideoIndex = av_find_best_stream(mFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
       
    	if (mSrcVideoIndex > -1) {
    		AVCodecParameters* videoCodecPar = mSrcFmtCtx->streams[mSrcVideoIndex]->codecpar;
    		const AVCodec* videoCodec = avcodec_find_decoder(videoCodecPar->codec_id);
    		if (!videoCodec) {
    			LOGI("avcodec_find_decoder error: srcUrl=%s", mSrcUrl.data());
    			return false;
    		}
       
    		mSrcVideoCodecCtx = avcodec_alloc_context3(videoCodec);
    		if (avcodec_parameters_to_context(mSrcVideoCodecCtx, videoCodecPar) != 0) {
    			LOGI("avcodec_parameters_to_context error: srcUrl=%s", mSrcUrl.data());
    			return false;
    		}
    		if (avcodec_open2(mSrcVideoCodecCtx, videoCodec, nullptr) < 0) {
    			LOGI("avcodec_open2 error: srcUrl=%s", mSrcUrl.data());
    			return false;
    		}
       
    		mSrcVideoCodecCtx->thread_count = 1;
    		mSrcVideoStream = mSrcFmtCtx->streams[mSrcVideoIndex];
       
    		if (0 == mSrcVideoStream->avg_frame_rate.den) {
    			LOGI("avg_frame_rate.den = 0 error: srcUrl=%s", mSrcUrl.data());
    			mSrcVideoFps = 25;
    		}
    		else {
    			mSrcVideoFps = mSrcVideoStream->avg_frame_rate.num / mSrcVideoStream->avg_frame_rate.den;
    		}
       
    		mSrcVideoWidth = mSrcVideoCodecCtx->width;
    		mSrcVideoHeight = mSrcVideoCodecCtx->height;
       
    	}
    	else {
    		LOGI("There is no video stream in the video: srcUrl=%s", mSrcUrl.data());
    		return false;
    	}
    	// video end;
    	return true;
    }
    void StreamPusher::closeConnectSrc() {
    	std::this_thread::sleep_for(std::chrono::milliseconds(1));
       
    	if (mSrcVideoCodecCtx) {
    		avcodec_close(mSrcVideoCodecCtx);
    		avcodec_free_context(&mSrcVideoCodecCtx);
    		mSrcVideoCodecCtx = nullptr;
    	}
       
    	if (mSrcFmtCtx) {
    		// 拉流不需要释放start
    		//if (mFmtCtx && !(mFmtCtx->oformat->flags & AVFMT_NOFILE)) {
    		//    avio_close(mFmtCtx->pb);
    		//}
    		// 拉流不需要释放end
    		avformat_close_input(&mSrcFmtCtx);
    		avformat_free_context(mSrcFmtCtx);
    		mSrcFmtCtx = nullptr;
    	}
    }
    bool StreamPusher::connectDst() {
    	// 这里直接给 rtsp 是可以的,跟命令行推流时给 -f rtsp 应该是一个意思,rtmp放这里会报错,然后推流到rtmp服务器时是用的 -f flv,所以推理到rtmp时把这“rtsp”改成“flv”即可。具体参数解释可以看 "2.4 ffmpeg中的一些API"
    	if (avformat_alloc_output_context2(&mDstFmtCtx, NULL, "rtsp", mDstUrl.data()) < 0) {
    		LOGI("avformat_alloc_output_context2 error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
       
    	// init video start
    	const AVCodec* videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
    	if (!videoCodec) {
    		LOGI("avcodec_find_encoder error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
    	mDstVideoCodecCtx = avcodec_alloc_context3(videoCodec);
    	if (!mDstVideoCodecCtx) {
    		LOGI("avcodec_alloc_context3 error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
    	//int bit_rate = 300 * 1024 * 8;  //压缩后每秒视频的bit位大小 300kB
    	int bit_rate = 4096000;
    	// CBR:Constant BitRate - 固定比特率
    	mDstVideoCodecCtx->flags |= AV_CODEC_FLAG_QSCALE;
    	mDstVideoCodecCtx->bit_rate = bit_rate;
    	mDstVideoCodecCtx->rc_min_rate = bit_rate;
    	mDstVideoCodecCtx->rc_max_rate = bit_rate;
    	mDstVideoCodecCtx->bit_rate_tolerance = bit_rate;
       
    	//VBR
        //  mDstVideoCodecCtx->flags |= AV_CODEC_FLAG_QSCALE;
        //  mDstVideoCodecCtx->rc_min_rate = bit_rate / 2;
        //  mDstVideoCodecCtx->rc_max_rate = bit_rate / 2 + bit_rate;
        //  mDstVideoCodecCtx->bit_rate = bit_rate;
       
    	//ABR:Average Bitrate - 平均码率
        // mDstVideoCodecCtx->bit_rate = bit_rate;
       
    	mDstVideoCodecCtx->codec_id = videoCodec->id;
    	mDstVideoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;// 不支持AV_PIX_FMT_BGR24直接进行编码
    	mDstVideoCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    	mDstVideoCodecCtx->width = mDstVideoWidth;
    	mDstVideoCodecCtx->height = mDstVideoHeight;
    	mDstVideoCodecCtx->time_base = { 1,mDstVideoFps };
    	//  mDstVideoCodecCtx->framerate = { mDstVideoFps, 1 };
    	mDstVideoCodecCtx->gop_size = 5;
    	mDstVideoCodecCtx->max_b_frames = 0;
    	mDstVideoCodecCtx->thread_count = 1;
    	mDstVideoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;   //添加PPS、SPS
    	AVDictionary* video_codec_options = NULL;
       
    	//H.264
    	if (mDstVideoCodecCtx->codec_id == AV_CODEC_ID_H264) {
    		//            av_dict_set(&video_codec_options, "profile", "main", 0);
    		av_dict_set(&video_codec_options, "preset", "superfast", 0);
    		av_dict_set(&video_codec_options, "tune", "zerolatency", 0);
    	}
    	//H.265
    	if (mDstVideoCodecCtx->codec_id == AV_CODEC_ID_H265) {
    		av_dict_set(&video_codec_options, "preset", "ultrafast", 0);
    		av_dict_set(&video_codec_options, "tune", "zero-latency", 0);
    	}
    	if (avcodec_open2(mDstVideoCodecCtx, videoCodec, &video_codec_options) < 0) {
    		LOGI("avcodec_open2 error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
    	mDstVideoStream = avformat_new_stream(mDstFmtCtx, videoCodec);
    	if (!mDstVideoStream) {
    		LOGI("avformat_new_stream error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
    	mDstVideoStream->id = mDstFmtCtx->nb_streams - 1;
    	// stream的time_base参数非常重要,它表示将现实中的一秒钟分为多少个时间基, 在下面调用avformat_write_header时自动完成
    	avcodec_parameters_from_context(mDstVideoStream->codecpar, mDstVideoCodecCtx);
    	mDstVideoIndex = mDstVideoStream->id;
    	// init video end
       
    	av_dump_format(mDstFmtCtx, 0, mDstUrl.data(), 1);
       
    	// open output url
    	if (!(mDstFmtCtx->oformat->flags & AVFMT_NOFILE)) {
    		if (avio_open(&mDstFmtCtx->pb, mDstUrl.data(), AVIO_FLAG_WRITE) < 0) {
    			LOGI("avio_open error: dstUrl=%s", mDstUrl.data());
    			return false;
    		}
    	}
       
    	AVDictionary* fmt_options = NULL;
    	//av_dict_set(&fmt_options, "bufsize", "1024", 0);
    	av_dict_set(&fmt_options, "rw_timeout", "30000000", 0); //设置rtmp/http-flv连接超时(单位 us)
    	av_dict_set(&fmt_options, "stimeout", "30000000", 0);   //设置rtsp连接超时(单位 us)
    	av_dict_set(&fmt_options, "rtsp_transport", "tcp", 0);
    	//        av_dict_set(&fmt_options, "fflags", "discardcorrupt", 0);
       
    		//av_dict_set(&fmt_options, "muxdelay", "0.1", 0);
    		//av_dict_set(&fmt_options, "tune", "zerolatency", 0);
       
    	mDstFmtCtx->video_codec_id = mDstFmtCtx->oformat->video_codec;
       
    	if (avformat_write_header(mDstFmtCtx, &fmt_options) < 0) { // 调用该函数会将所有stream的time_base,自动设置一个值,通常是1/90000或1/1000,这表示一秒钟表示的时间基长度
    		LOGI("avformat_write_header error: dstUrl=%s", mDstUrl.data());
    		return false;
    	}
    	return true;
    }
    void StreamPusher::closeConnectDst() {
    	std::this_thread::sleep_for(std::chrono::milliseconds(1));
    	if (mDstFmtCtx) {
    		// 推流需要释放start
    		if (mDstFmtCtx && !(mDstFmtCtx->oformat->flags & AVFMT_NOFILE)) {
    			avio_close(mDstFmtCtx->pb);
    		}
    		// 推流需要释放end
    		avformat_free_context(mDstFmtCtx);
    		mDstFmtCtx = nullptr;
    	}
       
    	if (mDstVideoCodecCtx) {
    		if (mDstVideoCodecCtx->extradata) {
    			av_free(mDstVideoCodecCtx->extradata);
    			mDstVideoCodecCtx->extradata = NULL;
    		}
       
    		avcodec_close(mDstVideoCodecCtx);
    		avcodec_free_context(&mDstVideoCodecCtx);
    		mDstVideoCodecCtx = nullptr;
    	}
    }
    
  4. main.cpp

    ​ //ffmpeg RTSP推流 https://www.jianshu.com/p/a9c7b08be46e

    ​ //ffmpeg 推流参数 https://blog.csdn.net/qq_173 # 参考吧,内容好像一般

    #include <iostream>
    #include "StreamPusher.h"
    int main(int argc, char* argv[]) {
    	srand((int)time(NULL));
    	const char* srcUrl = "rtsp://192.168.108.134:554/user=admin&password=&channel=1&stream=1.sdp?";
       	
        // 推到rtsp服务器:bool StreamPusher::connectDst(){}此函数第一行里给“rtsp”
        const char* dstUrl = "rtsp://localhost/test";
        // 推到rtmp服务器:connectDst() 函数第一行里改成“flv”
        const char* dstUrl = "rtmp://192.168.125.128/live/456";
           
    	int dstVideoFps = 20;
    	int dstVideoWidth = 800;
    	int dstVideoHeight = 448;
    	StreamPusher pusher(srcUrl, dstUrl, dstVideoWidth, dstVideoHeight, dstVideoFps);
    	pusher.start();
    	return 0;
    }
    

CmakeLists.txt的一个示例:原来自带的,自己还没试过

cmake_minimum_required(VERSION 3.1.3)

project(BXC_StreamPusher LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 11)

if("${CMAKE_BUILD_TYPE}" STREQUAL "")
  set(CMAKE_BUILD_TYPE "Debug")
endif()

message(STATUS "编译类型: ${CMAKE_BUILD_TYPE}")
message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")

set(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g2 -ggdb")
set(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall")

set(INCLUDE_DIR /usr/local/include)
set(LIB_DIR /usr/local/lib)

#set(INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/linux/include)
#set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/linux/lib)

include_directories(${INCLUDE_DIR})
link_directories(${LIB_DIR})

add_executable(BXC_StreamPusher
        StreamPusher/StreamPusher.cpp
        StreamPusher/main.cpp
        )

target_link_libraries(BXC_StreamPusher avformat avcodec avutil swscale swresample pthread)

​ 对于上面“StreamPusher.cpp”中的 “if (conn)”用了太多的if else嵌套了,我用do whie(0)进行了改写,尽量用一个层级,有if失败的地方就是尽早continue,但是代码我还没测过,不知道有没有问题,先放这里吧:

do 
{	
	if (!conn) {
		std::cerr << "源流媒体链接失败..." << std::endl;
		break;
	}
	conn = this->connectDst();
	if (!conn) {
		std::cerr << "推流服务器链接失败..." << std::endl;
		break;
	}

	// 初始化参数
	AVFrame* srcFrame = av_frame_alloc(); // pkt->解码->frame
	AVFrame* dstFrame = av_frame_alloc();

	dstFrame->width = mDstVideoWidth;
	dstFrame->height = mDstVideoHeight;
	dstFrame->format = mDstVideoCodecCtx->pix_fmt;  // AV_PIX_FMT_YUV420P;
	int dstFrame_buff_size = av_image_get_buffer_size(mDstVideoCodecCtx->pix_fmt, mDstVideoWidth, mDstVideoHeight, 1);
	uint8_t* dstFrame_buff = (uint8_t*)av_malloc(dstFrame_buff_size);
	av_image_fill_arrays(dstFrame->data, dstFrame->linesize, dstFrame_buff,
		mDstVideoCodecCtx->pix_fmt, mDstVideoWidth, mDstVideoHeight, 1);

	SwsContext* sws_ctx_src2dst = sws_getContext(mSrcVideoWidth, mSrcVideoHeight,
		mSrcVideoCodecCtx->pix_fmt,
		mDstVideoWidth, mDstVideoHeight,
		mDstVideoCodecCtx->pix_fmt,
		SWS_BICUBIC, nullptr, nullptr, nullptr);

	AVPacket srcPkt; // 拉流时获取的未解码帧
	AVPacket* dstPkt = av_packet_alloc(); // 推流时编码后的帧
	int continuity_read_error_count = 0; // 连续读错误数量
	int continuity_write_error_count = 0; // 连续写错误数量
	int ret = -1;
	int64_t frameCount = 0;

	while (true) {
		if (av_read_frame(mSrcFmtCtx, &srcPkt) >= 0) {
			continuity_read_error_count = 0;

			if (srcPkt.stream_index != mSrcVideoIndex) {
				//av_free_packet(&pkt);//过时
				av_packet_unref(&srcPkt);
				continue;
			}
			// 读取pkt->解码->编码->推流
			ret = avcodec_send_packet(mSrcVideoCodecCtx, &srcPkt);
			if (ret != 0) { 
				LOGI("avcodec_send_packet error: ret=%d", ret); 
				continue;
			}
			ret = avcodec_receive_frame(mSrcVideoCodecCtx, srcFrame);
			if (ret != 0) {
				LOGI("avcodec_receive_frame error: ret=%d", ret);
				continue;
			}

			frameCount++;
			// 解码成功->修改分辨率->修改编码

			// frame(yuv420p) 转 frame_bgr
			sws_scale(sws_ctx_src2dst,
				srcFrame->data, srcFrame->linesize, 0, mSrcVideoHeight,
				dstFrame->data, dstFrame->linesize);

			//开始编码 start
			dstFrame->pts = dstFrame->pkt_dts = av_rescale_q_rnd(frameCount, mDstVideoCodecCtx->time_base, mDstVideoStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));

			
			// 原来代码中使用的是 dstFrame->pkt_duration =
			// 报错:'AVFrame::pkt_duration': 被声明已否决。 chatgpt解答:表示使用了已经被弃用的 AVFrame::pkt_duration 字段。在新的 FFmpeg 版本中,推荐使用 AVFrame::pkt_duration2 或者 AVFrame::best_effort_timestamp 字段
			dstFrame->best_effort_timestamp = av_rescale_q_rnd(1, mDstVideoCodecCtx->time_base, mDstVideoStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));

			dstFrame->pkt_pos = frameCount;
			ret = avcodec_send_frame(mDstVideoCodecCtx, dstFrame);
			if (ret < 0) {
				LOGI("avcodec_send_frame error: ret=%d", ret);
				continue;
			}

			ret = avcodec_receive_packet(mDstVideoCodecCtx, dstPkt);
			if (ret < 0) {
				LOGI("avcodec_receive_packet error: ret=%d", ret);
				continue;
			}

			// 推流 start
			dstPkt->stream_index = mDstVideoIndex;
			ret = av_interleaved_write_frame(mDstFmtCtx, dstPkt);
			if (ret >= 0) {   // 推流成功
				continuity_write_error_count = 0;
				continue;
			}
			// 下面就是推流失败
			LOGI("av_interleaved_write_frame error: ret=%d", ret);
			++continuity_write_error_count;
			if (continuity_write_error_count > 5) {// 连续5次推流失败,则断开连接
				LOGI("av_interleaved_write_frame error: continuity_write_error_count=%d,dstUrl=%s", continuity_write_error_count, mDstUrl.data());
				break;
			}

		}
		else {
			// av_free_packet(&pkt);//过时
			av_packet_unref(&srcPkt);
			++continuity_read_error_count;
			if (continuity_read_error_count > 5) {// 连续5次拉流失败,则断开连接
				LOGI("av_read_frame error: continuity_read_error_count=%d,srcUrl=%s", continuity_read_error_count, mSrcUrl.data());
				break;
			}
			else {
				std::this_thread::sleep_for(std::chrono::milliseconds(100));
			}
		}
	}

	// 销毁
	av_frame_free(&srcFrame);
	//av_frame_unref(srcFrame);
	srcFrame = NULL;

	av_frame_free(&dstFrame);
	//av_frame_unref(dstFrame);
	dstFrame = NULL;

	av_free(dstFrame_buff);
	dstFrame_buff = NULL;

	sws_freeContext(sws_ctx_src2dst);
	sws_ctx_src2dst = NULL;
} while (0);

2.4. ffmpeg中一些API

  1. 报错:’AVFrame::pkt_duration’: 被声明已否决: chatgpt解答:表示使用了已经被弃用的 AVFrame::pkt_duration 字段。在新的 FFmpeg 版本中,推荐使用 AVFrame::pkt_duration2 或者 AVFrame::best_effort_timestamp 字段,2.3.2中的代码也是将 “dstFrame->pkt_duration =” 改成了 “dstFrame->best_effort_timestamp”就OK了。
  2. avformat_alloc_output_context2()参数解释:(来自chatgpt) 用于创建一个输出格式上下文,并分配所需的内存空间。示例:avformat_alloc_output_context2(&mDstFmtCtx, NULL, “rtsp”, “rtsp://localhost/123”)
    • AVFormatContext **ctx:输出格式上下文指针的指针。如果成功,将会分配内存,并将指针赋值给这个参数。
    • AVOutputFormat *oformat:要使用的输出格式。如果为 NULL,则自动选择合适的输出格式。
    • const char *format_name:要使用的输出格式名称。(==主要就是这个参数==,就是上面示例的”rtsp”位置,直接给“rtmp”就是错的,rtmp的要给flv),这个参数让FFmpeg 将尝试创建一个特定格式的输出文件,常见格式如下:
      • ==mp4==:MPEG-4 格式 (.mp4 文件扩展名) ==avi==:AVI 格式 (.avi 文件扩展名) ==flv==:Flash 视频格式 (.flv 文件扩展名) ==mov==:QuickTime 格式 (.mov 文件扩展名) ==mpeg==:MPEG-1 和 MPEG-2 格式 (.mpg 文件扩展名) ==matroska==:Matroska 格式 (.mkv 文件扩展名) ==webm==:WebM 格式 (.webm 文件扩展名) ==rtsp==: 这是我加的,也是ok的,但“rtmp”不行,要用”flv”替代

      • 如果为 NULL,则自动选择合适的输出格式。

      • 如果指定了特定的 format_name,但是 FFmpeg 库中没有对应的输出格式,则此函数将返回错误,并且无法创建输出文件。所以不确定要使用哪种输出格式,可以使用 FFmpeg 提供的 av_guess_format 函数猜测文件格式。例如 AVOutputFormat *output_format = ==av_guess_format(NULL, “output_file.mp4”, NULL)==; # 这将根据文件扩展名猜测文件格式,或许就可以直接给 .flv (注意av_guess_format这里一定要带点.,而上面avformat_alloc_output_context2给的话一定不要带点.)

        • 如果是guess的.flv格式,得到的结果:都是枚举值 output_format->audio_codec # 86017 代表 AV_CODEC_ID_MP3 output_format->video_codec # 21 代表 AV_CODEC_ID_FLV1
    • const char *filename:要推送的文件名或 URL。如果为 NULL,则不会创建输出文件。
    • int buffer_size:内部缓冲区大小。如果为 0,则使用默认值。
    • AVIOContext *opaque:自定义 IO 上下文。如果为 NULL,则使用默认的 IO 上下文。
    • int flags:一组标志位,用于控制输出格式上下文的行为。可以使用 AVFMT_NOFILE 表示不创建输出文件,AVFMT_GLOBALHEADER 表示在文件头部包含全局参数等。

2.5. 按Q停止推流

ffmpeg在命令行推流时,按Q是可以停止的,下面这是问的chatgpt的回答,还没测试过,先放这里吧。

#include <stdio.h>
#include <signal.h>
#include <libavformat/avformat.h>

static int received_sigterm = 0;
static int show_help = 0;

static void sigterm_handler(int sig)
{
    if (sig == SIGINT || sig == SIGTERM)
        received_sigterm = sig;
}

static void siginfo_handler(int sig)
{
    if (sig == SIGINFO) {
        show_help = 1;
    }
}

static void show_help_options(void)
{
    printf("FFmpeg push stream demo\n"
           "Usage: ./push_stream input_file stream_url\n"
           "Press \'q\' to stop pushing stream\n"
           "Press \'?\' to show help options\n");
}

int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("Missing arguments\n");
        show_help_options();
        return 1;
    }

    signal(SIGINT, sigterm_handler);
    signal(SIGTERM, sigterm_handler);
    signal(SIGINFO, siginfo_handler);

    av_register_all();

    AVFormatContext *input_ctx = NULL;
    AVInputFormat *input_fmt = NULL;
    int ret = avformat_open_input(&input_ctx, argv[1], input_fmt, NULL);
    if (ret < 0) {
        printf("Failed to open input file '%s'\n", argv[1]);
        return 1;
    }

    AVFormatContext *output_ctx = NULL;
    AVOutputFormat *output_fmt = av_guess_format("flv", argv[2], NULL);
    ret = avformat_alloc_output_context2(&output_ctx, output_fmt, NULL, argv[2]);
    if (ret < 0) {
        printf("Failed to allocate output context\n");
        return 1;
    }

    ret = avio_open(&output_ctx->pb, argv[2], AVIO_FLAG_WRITE);
    if (ret < 0) {
        printf("Failed to open output file '%s'\n", argv[2]);
        return 1;
    }

    ret = avformat_write_header(output_ctx, NULL);
    if (ret < 0) {
        printf("Failed to write header\n");
        return 1;

}

AVPacket pkt;
av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;

while (!received_sigterm) {
    ret = av_read_frame(input_ctx, &pkt);
    if (ret < 0)
        break;

    pkt.stream_index = 0;  // assuming only one stream

    ret = av_interleaved_write_frame(output_ctx, &pkt);
    if (ret < 0) {
        printf("Error writing packet to output stream\n");
        break;
    }

    av_packet_unref(&pkt);

    if (show_help) {
        show_help = 0;
        show_help_options();
    }
}

av_write_trailer(output_ctx);

avio_close(output_ctx->pb);
avformat_free_context(output_ctx);

avformat_close_input(&input_ctx);
avformat_free_context(input_ctx);

return 0;
}

三、virtual camera

​ 一个真实设备video0,一个虚拟设备video1。将video0的数据读出写入到video1,然后应用去读取video1的数据,看到这个操作,突然浮想联翩。手机上如果装了这个设备,是不是可以在和别人视频的时候,播放本地文件。是不是可以抖音上传假视频。还想去了之前看过的一个私活,摄像头读取本地文件显示。

然后linux上就不是用obs的vitual cream,而是用这个项目v4l2loopback

然后在pyvirtualcam推荐了一个相似的项目:UnityCapture


以上是用的obs自带的虚拟摄像头,看教程很多说,比较QQ这些用不了,就要下载额外的虚拟摄像头(一般注册四个),看这个教程吧,但是我跟着走了,QQ设置起,虚拟摄像头能开启,但还是没有画面,不知道为啥。

四、window下SRS的编译

现在已经有了编译好的二进制文件了,可以直接安装使用,下面时从源码编译: