最近多了个新需求,需要倍速推流,推流界的扛把子obs也有倍速推流功能,最高支持到两倍速。这里所说的倍速,当然只限定在文件,只有文件才可能有倍速功能,因为也只有文件才能倍速解码播放。实时视频流是不可能倍速的,因为没有时长,有时长的才可以按照播放进度来。是否是文件也不能通过是不是本地文件等来判断,以为很多http/rtsp/m3u8等也可能是文件,具体最终的判断依据应该是有没有时长,能不能获取到时长,能获取到的就说明是文件。
倍速推流和倍速播放功能相通,在ffmpeg做音视频解码常识中,有个pts和dts就是用来控制显示时间和解码时间的,如果这两个值除以2就说明时间少了一半,就是2倍速,乘以2就表示时间多了2倍,就是0.5倍速,基本上的运算公式就是 packet.pts = packet.pts/speed,其中这个speed速度参数是float类型。倍速播放的时候其实就是将收到的packet的pts/dts更改后,再送入解码,而推流其实就是保存,保存到rtsp地址就是将数据推流到rtsp,所以将这个值经过同样的运算发出去,就形成了倍速推流。
二、效果图三、体验地址国内站点:https://gitee.com/feiyangqingyun国际站点:https://github.com/feiyangqingyun四、功能特点支持各种本地音视频文件和网络音视频文件,格式包括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等)。五、相关代码 #include "frmspeedpush.h"#include "ui_frmspeedpush.h"#include "qthelper.h"#include "videoutil.h"frmSpeedPush::frmSpeedPush(QWidget *parent) : QWidget(parent), ui(new Ui::frmSpeedPush){ ui->setupUi(this); this->initForm(); this->initConfig();}frmSpeedPush::~frmSpeedPush(){ AppConfig::SpeedPushStart = (ui->btnStart->text() == "停止推流"); AppConfig::writeConfig(); delete ui;}void frmSpeedPush::initForm(){ VideoPara videoPara = ui->videoWidget->getVideoPara(); videoPara.videoCore = VideoCore_FFmpeg; ui->videoWidget->setVideoPara(videoPara); VideoPara para = ui->videoWidget->getVideoPara(); para.playRepeat = true; ui->videoWidget->setVideoPara(para); connect(ui->videoWidget, SIGNAL(sig_receivePlayStart(int)), this, SLOT(receivePlayStart(int))); connect(ui->videoWidget, SIGNAL(sig_receivePlayFinsh()), this, SLOT(receivePlayFinsh()));}void frmSpeedPush::initConfig(){ VideoUtil::loadMediaUrl(ui->cboxMediaUrl, AppConfig::SpeedMediaUrl, 0x40); connect(ui->cboxMediaUrl->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(saveConfig())); ui->txtPushUrl->setText(AppConfig::SpeedPushUrl); connect(ui->txtPushUrl, SIGNAL(textChanged(QString)), this, SLOT(saveConfig())); VideoUtil::loadSpeed(ui->cboxSpeed); ui->cboxSpeed->setCurrentIndex(ui->cboxSpeed->findData(AppConfig::SpeedPushValue)); connect(ui->cboxSpeed, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig())); ui->ckMuted->setChecked(AppConfig::SpeedPushMuted); connect(ui->ckMuted, SIGNAL(stateChanged(int)), this, SLOT(saveConfig())); if (AppConfig::SpeedPushStart) { on_btnStart_clicked(); }}void frmSpeedPush::saveConfig(){ AppConfig::SpeedMediaUrl = ui->cboxMediaUrl->currentText().trimmed(); AppConfig::SpeedPushUrl = ui->txtPushUrl->text().trimmed(); AppConfig::SpeedPushValue = ui->cboxSpeed->itemData(ui->cboxSpeed->currentIndex()).toFloat(); AppConfig::SpeedPushMuted = ui->ckMuted->isChecked(); AppConfig::writeConfig();}void frmSpeedPush::receivePlayStart(int time){ VideoThread *thread = ui->videoWidget->getVideoThread(); thread->setMuted(AppConfig::SpeedPushMuted); thread->setSpeed(AppConfig::SpeedPushValue); thread->setEncodeSpeed(AppConfig::SpeedPushValue); thread->recordStart(AppConfig::SpeedPushUrl); connect(thread, SIGNAL(receivePosition(qint64)), this, SLOT(receivePosition(qint64))); ui->sliderPosition->setRange(0, thread->getDuration()); ui->btnStart->setText("停止推流"); ui->cboxSpeed->setEnabled(false);}void frmSpeedPush::receivePlayFinsh(){ ui->sliderPosition->setRange(0, 0); ui->btnStart->setText("启动推流"); ui->cboxSpeed->setEnabled(true);}void frmSpeedPush::receivePosition(qint64 position){ ui->sliderPosition->setValue(position);}void frmSpeedPush::on_btnStart_clicked(){ if (ui->btnStart->text() == "启动推流") { ui->videoWidget->open(AppConfig::SpeedMediaUrl); } else { ui->videoWidget->stop(); }}void frmSpeedPush::on_sliderPosition_clicked(){ int value = ui->sliderPosition->value(); on_sliderPosition_sliderMoved(value);}void frmSpeedPush::on_sliderPosition_sliderMoved(int value){ ui->videoWidget->setPosition(value);}void frmSpeedPush::on_ckMuted_stateChanged(int arg1){ ui->videoWidget->setMuted(ui->ckMuted->isChecked());}void FFmpegSave::writePacket2(AVPacket *packet){ //非音视频流不用处理 int index = packet->stream_index; if (index != videoIndexOut && index != audioIndexOut) { return; } //转发数据包(可以设置仅仅转发数据包不用继续) if (sendPacket) { emit receivePacket(FFmpegHelper::creatPacket(packet)); if (onlySendPacket) { return; } } //封装格式 https://blog.csdn.net/weixin_44520287/article/details/113435440 https://xilixili.net/2018/08/20/ffmpeg-got-raw-h264/ //测试发现部分文件如果是非编码保存也写入了/可能部分播放器不支持保存后的文件播放/比如安卓上 if (index == videoIndexOut) { FFmpegSaveHelper::writeBsf(packet, videoStreamIn, bsfCtx); } if (saveVideoType == SaveVideoType_Stream) { //只需要写入视频数据 if (index == videoIndexOut) { file.write((char *)packet->data, packet->size); } } else if (saveVideoType == SaveVideoType_Mp4) { //取出输入输出流的时间基 AVStream *streamIn = (index == videoIndexOut ? videoStreamIn : audioStreamIn); AVStream *streamOut = formatCtx->streams[index]; AVRational timeBaseIn = streamIn->time_base; AVRational timeBaseOut = streamOut->time_base; //转换时间基准 if (index == videoIndexOut) { FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, videoCount, frameRate); FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut); } else if (index == audioIndexOut) { if (audioEncode) { FFmpegSaveHelper::rescalePacket(packet, audioDuration); } else { FFmpegSaveHelper::rescalePacket(packet, timeBaseIn, timeBaseOut, audioDuration); } } //打印对应的信息方便查看/videoIndexOut/audioIndexOut if (index == -1) { qDebug() << TIMEMS << flag << index << packet->pts << packet->dts << packet->duration; } //倍速调整时间戳 if (encodeSpeed != 1) { packet->pts = packet->pts / encodeSpeed; packet->dts = packet->dts / encodeSpeed; } //写入一帧数据/如果用 av_interleaved_write_frame 默认会缓存/可能导致音频越来越慢 int result = av_write_frame(formatCtx, packet); if (result < 0) { errorCount++; debug(result, QString("写%1包").arg(index == audioIndexOut ? "音频" : "视频"), ""); } else { errorCount = 0; } //推流超过错误次数需要重连 if (errorCount >= 5 && saveMode != SaveMode_File) { isOk = false; errorCount = 0; emit receiveSaveError(VideoError_Save); } }}