科普一下:
还有的别一个流媒体服务器:EasyDarwin(win上方便快捷),好想看到别的推rtsp的流用这个比较多(这官网里面ffmpeg推流时还加了一个参数 -rtsp_transport tcp(udp) 看后续能不能用的上)。
这里有比较多的关于ffmpeg的rtsp、rtmp参数,以及一些常用的视频流转化、处理命令。
ZLMediaKit这个是一个项目,支持rtsp、rtmp推流,类似于easydarwin、srs这种,这个页面有ffmpeg的各种推流命令。
srs、easydarwin、srs的对比,地址。
SRS是一个简单高效的实时视频服务器,支持RTMP/WebRTC/HLS/HTTP-FLV/SRT/GB28181。 # 所以好像不能推rtsp,一般的实现使用obs读取rtsp流,再用obs推rtmp
安装使用的两种方式:
直接使用官方提供的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
使用源码安装:
推流,一共三种方式:
ffmpeg推流:示例
ffmpeg -re -i ./doc/source.flv -c copy -f flv -y rtmp://localhost/live/123
# flv格式ffmpeg -re -i 123.mp4 -c:v libx264 -c:a aac -f flv -y rtmp://192.168.125.128/live/456
# mp4格式
ffmpeg -i "rtsp地址" -c:v libx264 -c:a aac -f flv rtmp://192.168.125.128/live/456
# 源是rtsp就不要加-re了,会很卡说明:
一定要-re,-re是限制ffmpeg的上传帧率,就按照视频本身的帧率来处理,否则ffmpeg会按照自身能力进行最大帧率的上传,就会导致流媒体服务器处理能力受限或者拉流端处理能力有限导致各类问题;
第一种是要flv的格式,-vcodec copy -acodec copy 限于摄像机提供的就为H.264+AAC的码流;
若不是flv(即第二种的mp4这些),则将-vcodec copy
改为-vcodec libx264
,-acodec copy
改为-acodec aac
,这样就可以比如h.265的mp4视频了,而不仅仅是flv格式。
rtsp流地址:要看这个rtsp摄像头的编码格式,(以受电弓rtsp摄像头为例(两种编码格式))
是==H.265X==(这就是h.264): ffmpeg -i “rtsp地址” -c copy -f flv “rtmp://192.168.125.128/live/123”
- 这里的H.265X其实就是h264了,效果是一样的,所以可以直接推流。
是==H.265==(也是HEVC)编码,而FLV格式不支持HEVC编码,就可能会得到相关编码错误: “[flv @ 000002ca5d2a2f80] Video codec hevc not compatible with flv Could not write header for output file #0 (incorrect codec parameters ?): Function not implemented Error initializing output stream 0:1 –”
所以就要将视频流转码为H.264编码再推流到RTMP服务器上,用“-c:v libx264 -c:a aac”(用这吧,好记一些)或是“-vcodec libx264 -acodec aac”这种写法是一个意思。
所以可以去VMS这个软件里修改这个rtsp的编码格式,是可以改成H265X,就可以直接推流。
OBS推流:示例 (这个要推流成功,就一定要先打开1935的rtmp端口):
推多路流:
推流(tee协议输出多路流):==ffmpeg -re -i myvideo.mp4 -vcodec libx264 -acodec aac -map 0 -f flv “tee:rtmp://127.0.0.1/live/123 | rtmp://127.0.0.1/live/456”== |
srs的配置文件:示例
用srs自带的配置文件推流好像速度更快,看这官方文档里面配置的写法。
拉流:
Tips:
web上播放是不支持rtsp的,所以要转成rtmp
-stream_loop -1
,stream_loop设置的值代表循环的次数,其中-1代表无限次。(加在 -i 前面)ffmpeg转封装格式说明:
需要知道 源容器 和 目标容器 的可容纳的编码格式,编码格式如果相互兼容,可以用-c copy
拷贝原有的stream,如“ffmpeg -i input.mp4 -c copy -f flv output.flv”,编码格式如果不兼容,需要转化成目标文件支持的编码,如“ffmpeg -i input_ac3.mp4 -vcodec copy -acodec aac -f flv output.flv”,与上面的对应起来。
常规的从文件转换HLS直播时:
ffmpeg -re -i input.mp4 -c copy -f hls -bsf:v h264_mp4toannexb output.m3u8
# -bsf:v h264_mp4toannexb 作用是将MP4中的H.264数据转换成H.264 AnnexB标准编码,AnnexB标准的编码常见于实时传输流中。如果源文件为FLV、TS等可以作为直播传输流的视频,则不需要这个参数。
ffmpeg推流上传HLS相关的M3U8以及TS文件 Nginx配置webdav模块
ffmpeg -re -i input.mp4 -c copy -f hls -hls_time 3 -hls_list_size 0 -method PUT -t 30 http://127.0.0.1/test/output.m3u8
最后关于推流,chatgpt还推荐了一些参数,没试过,放这里作为一个灵感吧:
-preset veryfast
:这个参数表示使用非常快的编码速度,但是可能会牺牲一些编码质量。如果需要更高的编码质量可以尝试使用slow
或者medium
等更慢的preset。-tune zerolatency
:这个参数表示使用零延迟的编码模式,可以减少编码延迟,但是可能会牺牲一些编码质量。如果需要更高的编码质量可以尝试不使用这个参数。-b:v 2500k
:这个参数表示视频流的比特率为2500k,即视频流的平均码率为2500k。码率越高,视频的质量也就越高,但是同时也会占用更多的带宽。-bufsize 2500k
:这个参数表示视频流的缓冲区大小为2500k。缓冲区越大,可以减少视频丢帧的可能性,但同时也会增加编码延迟。如果推流过程中出现了丢帧的情况可以适当增加缓冲区大小。-s 1280x720
:应该是设置分辨率。-rtsp_transport tcp
:好像默认是用的udp,可改成tcp本地摄像头推流的一个参考:以下 Logi C270 HD WebCam 是通过查看列表的命令行获得的名称
查看本机USB摄像头列表: ffmpeg -list_devices true -f dshow -i dummy
播放本机USB播放摄像头, ffplay -f dshow -i video=”Logi C270 HD WebCam”
本机USB摄像头+转码+推流到RTSP服务器(rtp over tcp) ffmpeg -f dshow -i video=”Logi C270 HD WebCam” -fflags nobuffer -max_delay 1 -threads 5 -profile:v high -preset superfast -tune zerolatency -an -c:v h264 -crf 25 -s 1280*720 -f rtsp -bf 0 -g 5 -f rtsp rtsp://127.0.0.1/live/test
我自己本地摄像头推流成功是用这个命令:==ffmpeg -f dshow -i video=”Logi C270 HD WebCam” -r 20 -f flv -y rtmp://192.168.125.128/live/123== # -r 是这是帧率,如果推流不成功,尝试设置音视频推流格式
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
这里就是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;
}
说明:
以上代码运行时,有两个警告:“Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly” “Encoder did not produce proper pts, making some up.”
用flv视频肯定没啥问题,但是用rtsp作为数据源,或者一些mp4格式的视频,可能会遇到这个错误:
[flv @ 000002562679e9c0] Video codec hevc not compatible with flv
这是因为==有rtsp的编码格式是h.265(要注意用VMS去查看)==,要用成 -c:v libx264 -c:a aac 这种(哪怕rtsp没声音,也要这这样指定aac的),具体就可以看下面这个2.3.2。
下面代码基本通用推流到rtsp、rtmp服务器。==一切延迟对比的画面是按python的画面为标准==(python画面本来就有点延迟,比ffmpeg直接读取展示慢一点,但也还是比较实时)(因为代码中指定推流的编码格式为libx264,所以rtsp源为h264、h265的都可以)(这个当源为视频时会报错,暂时只能拉流媒体的源)
==rtsp==服务器用的“EasyDarwin”:两种方式
==rtmp==服务器用的“srs”:三种方式
推流还可以尝试就用srs自带的.conf配置文件来实现(1.2中有),看一看延迟(以后用到再测吧)。
这个是关注的B站北小蔡的代码,讲的是延迟很低,它主要也是针对推流到rtsp服务器,代码也写的比较完善,里面有很多关于ffmpeg的推拉流的代码可以参考。(原始代码里可能因为ffmpeg版本问题,有细微的修改)。(真的投入使用时要再审核一下内存泄露问题,注意内存释放,要多测试)
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
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
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;
}
}
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);
==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给的话一定不要带点.)
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;
}
一个真实设备video0,一个虚拟设备video1。将video0的数据读出写入到video1,然后应用去读取video1的数据,看到这个操作,突然浮想联翩。手机上如果装了这个设备,是不是可以在和别人视频的时候,播放本地文件。是不是可以抖音上传假视频。还想去了之前看过的一个私活,摄像头读取本地文件显示。
pyvirtualcam: python的虚拟摄像头库(pip安装),windows上搭配obs的vitrual cream来使用。
简单来说,就是用opencv读取真实摄像头或者视频文件,然后用这个库传递给obs的virtual cream,然后进行显示出来
import pyvirtualcam
import numpy as np
# 点进去看,这里一个参数默认是PixelFormat.RGB
with pyvirtualcam.Camera(width=1280, height=720, fps=20) as cam:
print(f'Using virtual camera: {cam.device}')
frame = np.zeros((cam.height, cam.width, 3), np.uint8) # RGB
while True:
# frame[:] = cam.frames_sent % 255 # grayscale animation
frame[:, :, 0] = cam.frames_sent % 255 # 红色渐变(所以0是red)
cam.send(frame)
cam.sleep_until_next_frame()
# 这是pyvirtualcam中的代码,它里面包含了很多example,直接拿来改
```
pyvirtualcam.Camera创建时还有两个比较重要的参数:
from pyvirtualcam import PixelFormat
import platform
device_val = None
os = platform.system()
if os == "Linux":
device_val = "/dev/video2"
with pyvirtualcam.Camera(width=1280, height=720, fps=20, fmt=PixelFormat.BGR, device=device_val, print_fps=20) as cam:
然后linux上就不是用obs的vitual cream,而是用这个项目v4l2loopback。
然后在pyvirtualcam推荐了一个相似的项目:UnityCapture。
以上是用的obs自带的虚拟摄像头,看教程很多说,比较QQ这些用不了,就要下载额外的虚拟摄像头(一般注册四个),看这个教程吧,但是我跟着走了,QQ设置起,虚拟摄像头能开启,但还是没有画面,不知道为啥。
现在已经有了编译好的二进制文件了,可以直接安装使用,下面时从源码编译: