一、前言
随着直播的兴起,采集本地摄像头和麦克风进行直播推流,也是一个刚需,最简单的做法是直接用ffmpeg命令行采集并推流,这种方式简单粗暴,但是不能实时预览画面,而且不方便加上一些特殊要求。之前就已经打通了音视频文件和视频流的采集,那是不是可以简单点的方式就能直接加入到原有的框架中呢,答案是可以的,经过一段时间的摸索,发现只要新增少量十几行的代码就行,一个是设置avformat_open_input的第三个参数,指定采集本地设备,一个是设置本地设备的文件名,桌面是gdigrab,本地摄像头是dshow,在linux上桌面是x11grab,本地摄像头是v4l2,本地麦克风是alsa。网上类似的代码也很多,这里只讲两点。
第一点是最初在同时打开本地摄像头和麦克风的时候,发现声音总是不正常,尽管深知这种思路肯定是对的,还总在转换声音那边下手,死活都不行。就在即将放弃此方案的时候,找遍了网络找相关资料,发现要通过av_dict_set(options, "audio_buffer_size", "40", 0);设置一个叫audio_buffer_size的参数值,默认这个值很大,导致声音延迟和缓存很大,所以声音画面不同步,把这个值设置小一点,性能相当好,简直完美。
第二点是在使用采集摄像头和麦肯风同步保存到MP4文件或者推流的时候,发现一个现象,随着时间的推移,音视频慢慢的不同步,音频延迟越来越大,一个是的几分钟内还是正常的,时间到了比如十分钟以后,慢慢的延迟越大,后面发现,在调用av_interleaved_write_frame函数写入数据的时候,这个函数默认会缓存,缓存av_write_frame直接立即写入,就再也没有发生过这个问题,连续测试几天稳得一逼。为了尽量提高实时性,目前的方案是直接采集到就解码并编码立即推流,音视频都是,先不做同步,通过audio_buffer_size参数的设置以及改成av_write_frame来写入,能够将实时性做到极致。如果使用ffmpeg命令行,无论如何设置参数,都做不到这个实时性。
公众号:Qt实战,各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发。
公众号:Qt入门和进阶,专门介绍Qt/C++相关知识点学习,帮助Qt开发者更好的深入学习Qt。多位Qt元婴期大神,一步步带你从入门到进阶。
二、效果图
三、体验地址
- 国内站点:https://gitee.com/feiyangqingyun
- 国际站点:https://github.com/feiyangqingyun
- 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 体验地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_push。
- 视频主页:https://space.bilibili.com/687803542
四、功能特点
- 支持各种本地音视频文件和网络音视频文件,格式包括mp3、aac、wav、wma、mp4、mkv、rmvb、wmv、mpg、flv、asf等。
- 支持各种网络音视频流,网络摄像头,协议包括rtsp、rtmp、http等。
- 支持本地摄像头设备推流,可指定分辨率、帧率、格式等。
- 支持本地桌面采集推流,可指定屏幕索引、采集区域、起始坐标、帧率等,也支持指定窗口标题进行采集。
- 可实时切换预览视频文件,可切换音视频文件播放进度,切换到哪里就推流到哪里。预览过程中可以切换静音状态和暂停推流。
- 可指定重新编码推流,任意源头格式可选强转264或265格式。
- 可转换分辨率推流,设置等比例缩放或者指定分辨率进行转换。
- 推流的清晰度、质量、码率都可调,可以节约网络带宽和拉流端的压力。
- 音视频文件自动循环不间断推流。
- 音视频流有自动掉线重连机制,重连成功自动继续推流。
- 支持各种流媒体服务程序,包括但不限于mediamtx、ZLMediaKit、srs、LiveQing、nginx-rtmp、EasyDarwin、ABLMediaServer。
- 通过配置文件自动加载对应流媒体程序的协议和端口,自动生成推流地址和各种协议的拉流地址。可以通过配置文件自己增加流媒体程序。
- 可选rtmp、rtmp格式推流,推流成功后,支持多种格式拉流,包括但不限于rtsp、rtmp、hls、flv、ws-flv、webrtc等。
- 在软件上推流成功后,可以直接单击网页预览,实时预览推流后拉流的画面,多画面网页展示。
- 软件界面上可单击对应按钮,动态添加文件和目录,可手动输入地址。
- 推拉流实时性极高,延迟极低,延迟时间大概在100ms左右。
- 极低CPU资源占用,4路主码流推流只需要占用0.2%CPU。理论上常规普通PC机器推100路毫无压力,主要性能瓶颈在网络。
- 可以推流到外网服务器,然后通过手机、电脑、平板等设备播放对应的视频流。
- 每路推流都可以手动指定唯一标识符(方便拉流/用户无需记忆复杂的地址),没有指定则按照策略随机生成hash值。也支持自动按照指定标识后面加数字的方式递增命名。比如设置标识为字母v,策略为标识递增,则每添加一个对应的推流码命名依次是v1、v2、v3等。
- 根据推流协议自动转码格式,默认策略按照选择的推流协议,比如rtsp支持265而rtmp不支持,如果是265的文件而选择rtmp推流,则自动转码成264格式再推流。
- 音视频同步推流,在拉流和采集的时候就会自动处理好同步,同步后的数据再推流。
- 表格中实时显示每一路推流的分辨率和音视频数据状态,灰色表示没有输入流,黑色表示没有输出流,绿色表示原数据推流,红色表示转码后的数据推流。
- 自动重连视频源,自动重连流媒体服务器,保证启动后,推流地址和打开地址都实时重连,只要恢复后立即连上继续采集和推流。
- 根据不同的流媒体服务器类型,自动生成对应的rtsp、rtmp、hls、flv、ws-flv、webrtc拉流地址,用户可以直接复制该地址到播放器或者网页中预览查看。
- 添加的推流地址等信息自动存储到文件,可以手动打开进行修改,默认启动后自动加载历史记录。
- 可以指定生成的网页文件保存位置,方便作为网站网页发布,可以直接在浏览器中输入网址进行访问,发布后可以直接在局域网其他设备比如手机或者电脑打开对应网址访问。
- 可选是否开机启动、后台运行等。网络推流添加的rtsp地址可勾选是否隐藏地址中的用户信息。
- 自带设备推流模块,自动识别本地设备,包括本地的摄像头和桌面,可以手动选择不同的是视频和音频采集设备进行推流。
- 自带文件点播模块,添加文件后用户可以拉取地址点播,用户端可以任意切换播放进度。支持各种浏览器(谷歌chromium、微软edge、火狐firefox等)、各种播放器(vlc、mpv、ffplay、potplayer、mpchc等)打开请求。
- 文件点播模块实时统计显示每个文件对应的访问数量、总访问数量、不同IP地址访问数量。
- 文件点播模块采用纯QTcpSocket通信,不依赖流媒体服务程序,核心源码不到500行,注释详细,功能完整。
- 支持任意Qt版本(Qt4、Qt5、Qt6),支持任意系统(windows、linux、macos、android、嵌入式linux等)。
五、相关代码
void FFmpegHelper::initOption(AVDictionary **options, const QString &mediaUrl)
{//设置音频采集选项if (mediaUrl.contains("audio=") && !mediaUrl.contains("audio=virtual-audio-capturer")) {av_dict_set(options, "sample_size", "16", 0);av_dict_set(options, "channels", "2", 0);av_dict_set(options, "sample_rate", "44100", 0);av_dict_set(options, "audio_buffer_size", "40", 0);}
}void FFmpegThread::initOption()
{//增加rtp/sdp支持/貌似网络地址带.sdp结尾的这种可以不用加if (mediaType == MediaType_FileLocal && mediaUrl.endsWith(".sdp")) {av_dict_set(&options, "protocol_whitelist", "file,rtp,udp", 0);}//设置秘钥相关FFmpegHelper::initDecryption(&options, this->property("cryptoKey").toByteArray());//设置缓存大小/通信协议FFmpegHelper::initOption(&options, caching, transport);//设置分辨率/帧率/桌面采集偏移值等参数if (mediaType == MediaType_Device || mediaType == MediaType_Screen) {FFmpegHelper::initOption(&options, mediaUrl);FFmpegHelper::initOption(&options, bufferSize, frameRate);if (mediaType == MediaType_Screen) {int offsetX = this->property("offsetX").toInt();int offsetY = this->property("offsetY").toInt();FFmpegHelper::initOption(&options, offsetX, offsetY, mediaUrl);}}
}bool FFmpegThread::initInput()
{//本地摄像头/桌面录屏/linux系统可以打开cheese程序查看本地摄像头(如果是在虚拟机中需要设置usb选项3.1)if (mediaType == MediaType_Device) {ifmt = av_find_input_format(mediaUrl.startsWith("audio=") ? Device_Audio : Device_Video);} else if (mediaType == MediaType_Screen) {ifmt = av_find_input_format(Device_Screen);}//实例化格式处理上下文formatCtx = avformat_alloc_context();//设置超时回调(有些不存在的地址或者网络不好的情况下要卡很久)formatCtx->interrupt_callback.callback = FFmpegThreadHelper::openAndReadCallBack;formatCtx->interrupt_callback.opaque = this;//可以强制指定本地摄像头采集解码为mjpeg或者h264/找遍了网络原来是这样设置才起作用if (mediaType == MediaType_Device) {audioPlayer->setProperty("microphone", true);FFmpegHelper::setVideoCodecName(formatCtx, videoCodecName);}//取出最终的播放地址QString url = VideoHelper::getPlayUrl(VideoCore_FFmpeg, mediaType, mediaUrl);//桌面采集指定窗口标题需要转换才能支持中文/ffmpeg5开始才修复了这个问题QByteArray urlData = VideoHelper::getUrlData(mediaType, url, FFMPEG_VERSION_MAJOR < 5);//打开输入(通过标志位控制回调那边做超时判断)tryOpen = true;int result = -1;if (mediaType == MediaType_WebSocket) {formatCtx->pb = webObj->getCtx();if (!formatCtx->pb) {return false;}//打开失败则重新连接result = avformat_open_input(&formatCtx, NULL, NULL, &options);if (result < 0) {QMetaObject::invokeMethod(webObj, "reopen");}} else {result = avformat_open_input(&formatCtx, urlData.constData(), ifmt, &options);}tryOpen = false;if (result < 0) {debug(result, "打开地址", "");return false;}//根据自己项目需要开启下面部分代码加快视频流打开速度//开启后由于值太小可能会出现部分视频流获取不到分辨率if (decodeType == DecodeType_Fastest && mediaType == MediaType_Rtsp) {FFmpegHelper::initRtspFast(formatCtx);}//获取流信息result = avformat_find_stream_info(formatCtx, NULL);if (result < 0) {debug(result, "找流失败", "");return false;}//封装格式formatName = formatCtx->iformat->name;//校验硬解码FFmpegThreadHelper::checkHardware(formatName, hardware);//获取文件时长(这里获取到的是秒)double length = (double)formatCtx->duration / AV_TIME_BASE;//如果是本地文件而且没有时长则用最原始方法读取时长//有部分设备导出的视频文件读取出来时长不对也可以用此方法读取if (mediaType == MediaType_FileLocal && duration <= 0) {if (this->property("getDurationByFrame").toBool()) {length = FFmpegUtil::getDuration(mediaUrl);}}duration = length * 1000;this->checkMediaType();//发送文件时长信号if (getIsFile()) {emit receiveDuration(duration > 0 ? duration : 0);}//获取音视频轨道信息(一般有一个音频或者一个视频/ts节目文件可能有多个)FFmpegHelper::getTracks(formatCtx, audioTracks, videoTracks);emit receiveTrack(audioTracks, videoTracks);QString msg = QString("格式: %1 时长: %2 秒 加速: %3").arg(formatName).arg(duration / 1000).arg(hardware);debug(0, "媒体信息", msg);return true;
}bool FFmpegThread::initVideo()
{//找到视频流索引videoIndex = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);if (videoIndex < 0) {//有些没有视频流所以这里不用返回videoIndex = -1;debug(0, "无视频流", "");} else {//如果手动指定了轨道则取指定的(节目流有多个轨道可以指定某个)if (videoTrack >= 0 && videoTracks.contains(videoTrack)) {videoIndex = videoTrack;}//取出流获取对应的信息创建解码器int result = -1;AVStream *videoStream = formatCtx->streams[videoIndex];//如果主动设置过旋转角度则将旋转信息设置到流信息中以便保存那边也应用(不需要保存也旋转可以注释)if (rotate != -1) {FFmpegHelper::setRotate(videoStream, rotate);}//先获取旋转角度(如果有旋转角度则不能用硬件加速)this->getRotate();if (rotate != 0) {hardware = "none";}//查找视频解码器(如果上面av_find_best_stream第五个参数传了则这里不需要)AVCodecID codecId = FFmpegHelper::getCodecId(videoStream);if (codecId == AV_CODEC_ID_NONE) {debug(result, "无视解码", "");return false;}//初始化解码器FFmpegThreadHelper::initVideoCodec(&videoCodec, codecId, videoCodecName, hardware);//创建视频解码器上下文videoCodecCtx = avcodec_alloc_context3(NULL);if (!videoCodecCtx) {debug(result, "创建视解", "");return false;}//将视频流的参数拷贝给视频解码器上下文/以便能够按照对应流参数进行解码result = FFmpegHelper::copyContext(videoCodecCtx, videoStream, false);if (result < 0) {debug(result, "视频参数", "");return false;}//初始化硬件加速(也可以叫硬解码/如果当前格式不支持硬解则立即切换到软解码)if (hardware != "none" && !FFmpegThreadHelper::initHardware(this, videoCodec, videoCodecCtx, hardware)) {hardware = "none";videoCodec = avcodec_find_decoder(codecId);}if (!videoCodec) {return false;}//设置低延迟和加速解码等参数(设置max_lowres的话很可能画面采用最小的分辨率)if (!getIsFile()) {//videoCodecCtx->lowres = videoCodec->max_lowres;videoCodecCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;videoCodecCtx->flags2 |= AV_CODEC_FLAG2_FAST;}//打开视频解码器result = avcodec_open2(videoCodecCtx, videoCodec, NULL);if (result < 0) {debug(result, "打开视解", "");return false;}if (videoCodecCtx->pix_fmt == AV_PIX_FMT_NONE) {debug(0, "格式为空", "");return false;}//获取分辨率大小FFmpegHelper::getResolution(videoStream, videoWidth, videoHeight);//如果没有获取到宽高则返回if (videoWidth == 0 || videoHeight == 0) {debug(0, "无分辨率", "");return false;}//记录首帧开始时间和解码器名称videoFirstPts = videoStream->start_time;videoCodecName = videoCodec->name;frameRate = FFmpegHelper::getFrameRate(videoStream, formatName);QString msg = QString("索引: %1 解码: %2 帧率: %3 宽高: %4x%5 角度: %6").arg(videoIndex).arg(videoCodecName).arg(frameRate).arg(videoWidth).arg(videoHeight).arg(rotate);debug(0, "视频信息", msg);//FFmpegUtil::getExtraData(videoCodecCtx);}return true;
}