赞
踩
之前就写过这个方案,当时做的是ffmpeg内核版本,由于ffmpeg内核解析都是代码实现,所以无缝切换非常完美,看不到丝毫的中间切换过程,看起来就像是在一个通道画面中。其实这种切换只能说是取巧办法,最佳的办法应该是公用一个openglwidget窗体,解码线程那边开两个,第二个解码线程打开后,解码到了数据开始,再将第一个解码线程停掉,或者先停掉第一个解码线程的信号槽关联即可。这样就真正的底层实现的无感切换。但是这种方式不适用于其他视频内核,比如vlc内核这种,如果使用的句柄模式,就不好弄了。
在轮询视频的时候,通常都是需要将之前的视频全部关闭,然后打开下一组视频,在这个切换的过程中,如果是按照常规的做法,比如先关闭再打开新的视频,肯定会出现空白黑屏之类的过度空白区间,如何避免这个问题实现无感知的无缝切换,是个需要稍微懂点脑筋的问题,有一个比较好的做法就是,准备双倍的通道或者后台解码线程,在收到需要切换指令的时候,先后台打开解码线程,直到打开完成能够正常出图像,此时再去关闭之前的解码线程,这样就相当于无缝对接上了,不会说是中间等待打开解码到能正常出图像的过程空白。但是这样又存在一个问题,那就是轮询间隔时间不是非常准确,会有个1-2s的偏差,基本上这个偏差也能接受就是,所以如果是60s的轮询间隔,你可以在58s的时候就开始打开另外一组的解码线程,2s后全部正常出图像后就关联到视频控件上显示,把之前的解码线程关闭并销毁。这样处理下来,既保证了切换过程无感知,又保证了不会过度开销占用CPU,也就切换的期间刚好有双倍的解码线程,只要内存足够,这点性能牺牲还是值得的。
公众号:Qt实战,各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发。
公众号:Qt入门和进阶,专门介绍Qt/C++相关知识点学习,帮助Qt开发者更好的深入学习Qt。多位Qt元婴期大神,一步步带你从入门到进阶,走上财务自由之路。
bool FFmpegSaveHelper::rtmp_pcm = false; QStringList FFmpegSaveHelper::vnames_file = QStringList() << "h264" << "hevc"; QStringList FFmpegSaveHelper::anames_pcm = QStringList() << "pcm_mulaw" << "pcm_alaw" << "pcm_s16be"; QStringList FFmpegSaveHelper::anames_file = QStringList() << "aac" << "mp2" << "mp3" << "ac3" << anames_pcm; QStringList FFmpegSaveHelper::anames_rtmp = QStringList() << "aac" << "mp3"; QStringList FFmpegSaveHelper::anames_rtsp = QStringList() << "aac" << "mp3" << anames_pcm; void FFmpegSaveHelper::checkEncode(FFmpegSave *thread, const QString &videoCodecName, const QString &audioCodecName, bool &videoEncode, bool &audioEncode, EncodeAudio &encodeAudio, bool &needAudio) { //推流和录制要区分判断(推流更严格/主要限定在流媒体服务器端) bool notSupportVideo = false; bool notSupportAudio = false; SaveMode saveMode = thread->getSaveMode(); QString mediaUrl = thread->property("mediaUrl").toString(); if (saveMode == SaveMode_File) { notSupportVideo = !vnames_file.contains(videoCodecName); notSupportAudio = !anames_file.contains(audioCodecName); } else { //具体需要根据实际需求进行调整 if (saveMode == SaveMode_Rtmp) { notSupportVideo = (videoCodecName != "h264"); notSupportAudio = !anames_rtmp.contains(audioCodecName); } else if (saveMode == SaveMode_Rtsp) { notSupportVideo = !vnames_file.contains(videoCodecName); notSupportAudio = !anames_rtsp.contains(audioCodecName); } //特定格式过滤 if (mediaUrl.endsWith(".m3u8")) { notSupportAudio = true; } } if (notSupportVideo) { thread->debug(0, "视频格式", QString("警告: %1").arg(videoCodecName)); videoEncode = true; } if (notSupportAudio) { thread->debug(0, "音频格式", QString("警告: %1").arg(audioCodecName)); audioEncode = true; } //0. 因为还没有搞定万能转换/所以暂时做下面的限制 //1. 保存文件模式下纯音频统一编码成pcma //2. 保存文件模式下视音频且启用了转码则禁用音频 //3. 推流RTMP模式下启用了转码则禁用音频 //4. 推流RTSP模式下纯音频且启用了转码则编码成pcma //5. 推流RTSP模式下启用了转码则禁用音频 //6. 纯音频aac格式在推流的时候可选转码/有些流媒体程序必须要求转码才能用 bool encodeAac = false; bool onlySaveAudio = thread->getOnlySaveAudio(); bool onlyAac = (onlySaveAudio && audioCodecName == "aac"); if (encodeAudio == EncodeAudio_Auto) { if (saveMode == SaveMode_File) { if (onlySaveAudio || audioCodecName == "pcm_s16le") { encodeAudio = EncodeAudio_Pcma; } else if (audioEncode) { needAudio = false; } } else if (saveMode == SaveMode_Rtmp) { if (audioEncode) { needAudio = false; } else if (onlyAac && encodeAac) { encodeAudio = EncodeAudio_Aac; } } else if (saveMode == SaveMode_Rtsp) { if (audioEncode) { encodeAudio = EncodeAudio_Pcma; } else if (onlyAac && encodeAac) { encodeAudio = EncodeAudio_Pcma; } } } //如果设置过需要检查B帧/有B帧推流需要转码/否则一卡卡 if (!videoEncode && !onlySaveAudio && saveMode != SaveMode_File) { bool checkB = thread->property("checkB").toBool(); bool isFile = thread->property("isFile").toBool(); if (checkB && isFile && FFmpegUtil::hasB(mediaUrl)) { videoEncode = true; } } //部分流媒体服务支持推pcma和pcmu if (rtmp_pcm && saveMode == SaveMode_Rtmp && anames_pcm.contains(audioCodecName)) { needAudio = true; encodeAudio = EncodeAudio_Pcma; } //音频需要强转则必须设置启用音频编码 if (encodeAudio != EncodeAudio_Auto) { audioEncode = true; } } const char *FFmpegSaveHelper::getFormat(AVDictionary **options, QString &fileName, bool mov, const QString &flag) { //默认是mp4/mov更具兼容性比如音频支持pcma等 const char *format = mov ? "mov" : "mp4"; if (fileName.startsWith("rtmp://")) { format = "flv"; } else if (fileName.startsWith("rtsp://")) { format = "rtsp"; av_dict_set(options, "stimeout", "3000000", 0); av_dict_set(options, "rtsp_transport", "tcp", 0); } else if (fileName.startsWith("udp://")) { format = "mpegts"; } else { QByteArray temp; if (!flag.isEmpty()) { temp = flag.toUtf8(); format = temp.constData(); QString suffix = fileName.split(".").last(); fileName.replace(suffix, flag); } } return format; } bool FFmpegSave::initStream() { //如果存在秘钥则启用加密 AVDictionary *options = NULL; FFmpegHelper::initEncryption(&options, this->property("cryptoKey").toByteArray()); QString flag; if (getOnlySaveAudio() && encodeAudio != EncodeAudio_Aac) { flag = "wav"; } //既可以是保存到文件也可以是推流(对应格式要区分) bool mov = audioCodecName.startsWith("pcm_"); const char *format = FFmpegSaveHelper::getFormat(&options, fileName, mov, flag); //开辟一个格式上下文用来处理视频流输出(末尾url不填则rtsp推流失败) QByteArray fileData = fileName.toUtf8(); const char *url = fileData.data(); int result = avformat_alloc_output_context2(&formatCtx, NULL, format, url); if (result < 0) { debug(result, "创建格式", ""); return false; } //创建输出视频流 if (!this->initVideoStream()) { goto end; } //创建输出音频流 if (!this->initAudioStream()) { goto end; } //打开输出文件 if (!(formatCtx->oformat->flags & AVFMT_NOFILE)) { //记录开始时间并设置回调用于超时判断 startTime = av_gettime(); formatCtx->interrupt_callback.callback = FFmpegSaveHelper::openAndWriteCallBack; formatCtx->interrupt_callback.opaque = this; tryOpen = true; result = avio_open2(&formatCtx->pb, url, AVIO_FLAG_WRITE, &formatCtx->interrupt_callback, NULL); tryOpen = false; if (result < 0) { debug(result, "打开输出", ""); goto end; } } //写入文件开始符 result = avformat_write_header(formatCtx, &options); if (result < 0) { debug(result, "写文件头", ""); goto end; } writeHeader = true; debug(0, "打开输出", QString("格式: %1").arg(format)); return true; end: //关闭释放并清理文件 this->close(); this->deleteFile(fileName); return false; } bool FFmpegSave::initVideoStream() { if (needVideo) { videoIndexOut = 0; AVStream *stream = avformat_new_stream(formatCtx, NULL); if (!stream) { return false; } //设置旋转角度(没有编码的数据是源头带有旋转角度的/编码后的是正常旋转好的) if (!videoEncode) { FFmpegHelper::setRotate(stream, rotate); } //复制解码器上下文参数(不编码从源头流拷贝/编码从设置的编码器拷贝) int result = -1; if (videoEncode) { stream->r_frame_rate = videoCodecCtx->framerate; result = FFmpegHelper::copyContext(videoCodecCtx, stream, true); } else { result = FFmpegHelper::copyContext(videoStreamIn, stream); } if (result < 0) { debug(result, "复制参数", ""); return false; } } return true; } bool FFmpegSave::initAudioStream() { if (needAudio) { audioIndexOut = (videoIndexOut == 0 ? 1 : 0); AVStream *stream = avformat_new_stream(formatCtx, NULL); if (!stream) { return false; } //复制解码器上下文参数(不编码从源头流拷贝/编码从设置的编码器拷贝) int result = -1; if (audioEncode) { result = FFmpegHelper::copyContext(audioCodecCtx, stream, true); } else { result = FFmpegHelper::copyContext(audioStreamIn, stream); } if (result < 0) { debug(result, "复制参数", ""); return false; } } return true; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。