一、前言
本功能最初也是有一些人提过类似的需求,就是能不能将本地的音视频文件,通过纯Qt程序推流出去,然后用户可以直接在网页上播放,也可以用各种播放器播放,然后还可以任意切换播放进度,其实说白了就是个文件服务器,用户通过网络地址访问以后,告诉对方当前是媒体文件就会自动播放,是其他文件则可以开启下载,很多视频网站最初也是按照这个思路来设计,当然缺点很明显,那就是无法防止用户下载,毕竟这个本来就是当做文件发给用户的,无所谓保密的需求,话说现在的无论哪一种视频网站,只要能播放,用户就能通过各种手段录制下来的,也是无法规避这个问题。
无论网络协议如何发展,都离不开最底层的两种协议,tcp/udp通信,http也是建立在这两种协议基础上,然后又在http基础上衍生了众多的协议,总之,最基础的tcp/udp几十年都没变过,现在音视频发展这么迅速,衍生的各种rtmp/rtsp/hsl/webrtc啥的,最终底层还是基于tcp/udp通信,明白了这个道理,文件推流理论上基于tcp就可以实现。音视频文件在普通文件服务的基础上还多了个范围的参数Accept-Ranges: bytes/Content-Range,就是用户单击了进度后告诉服务这边当前要切换到哪个字节位置,这样就可以任意跳转播放进度。
二、效果图
三、体验地址
- 国内站点: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。
四、相关代码
void FilePushClient::readData()
{
//GET /后缀 HTTP/1.1
//Host: 192.168.0.110:6908
//Connection: keep-alive (一般网页请求是keep-alive/其他都是close)
//Upgrade-Insecure-Requests: 1
//User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
//Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
//Accept-Encoding: gzip, deflate
//Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
QByteArray data = tcpSocket->readAll();
buffer.append(data);
//超过了长度说明数据是垃圾数据
if (buffer.size() > 1000) {
buffer.clear();
return;
}
//末尾必须是两个回车换行
if (!buffer.endsWith("\r\n\r\n")) {
return;
}
//解析请求(解析失败则不用继续)
QHttpParser parser;
bool ok = parser.parserRequest(buffer);
emit receiveData(buffer);
buffer.clear();
if (!ok) {
quit();
return;
}
//不是对应的方法
if (parser.method() != "GET") {
quit();
return;
}
//取出后缀地址(如果是请求图标则不用继续)
QString url = parser.url();
if (!url.startsWith("/") || url.startsWith("/favicon.ico")) {
return;
}
//根据请求中的hash值查找文件
QString hash = url.mid(1, url.length());
FilePushServer *server = (FilePushServer *)this->parent();
QString fileName = server->findFile(hash);
if (!this->setFile(fileName)) {
quit();
return;
}
QString range = parser.headerValue("Range");
if (range.isEmpty()) {
this->writeData200(0);
return;
}
//Range: bytes=0- / bytes=-1024 / bytes=0-1024
QStringList list = range.split("=");
if (list.count() != 2) {
quit();
return;
}
//取出进度范围
range = list[1];
range.replace(" ", "");
list.clear();
list = range.split("-");
//测试下来发现基本上都是 x- 的情况
qint64 startPos, endPos;
if (range.startsWith("-")) {
endPos = fileSize - 1;
startPos = endPos - list.at(0).toInt();
} else if (range.endsWith("-")) {
startPos = list.at(0).toInt();
endPos = fileSize - 1;
} else {
startPos = list.at(0).toInt();
endPos = list.at(1).toInt();
}
this->writeData206(startPos, endPos);
}
void FilePushClient::writeData(qint64 bytes)
{
writeByteCount -= bytes;
if (tcpSocket && writeByteCount > 0) {
qint64 size = 512 * 1024;
size = tcpSocket->write(file->read(size));
//qDebug() << TIMEMS << "writeData" << size;
}
}
QByteArray FilePushClient::getHeadData(const QString &flag, qint64 startPos, qint64 endPos, qint64 bufferSize)
{
QStringList list;
list << flag;
list << "Server: QQ_517216493 WX_feiyangqingyun";
list << "Cache-control: no-cache";
list << "Pragma: no-cache";
list << "Connection: close";
list << "Accept-Ranges: bytes";
list << "Access-Control-Allow-Origin: *";
list << QString("Content-Type: %1").arg(fileType);
list << QString("Content-Length: %1").arg(bufferSize);
if (playMode == 1) {
list << QString("Content-Disposition: attachment;filename=%1").arg(fileName);
}
list << QString("Content-Range: bytes %1-%2/%3").arg(startPos).arg(endPos).arg(fileSize);
//末尾必须加个回车换行
list << "\r\n";
QString data = list.join("\r\n");
return data.toUtf8();
}
void FilePushClient::writeData200(qint64 startPos)
{
if (!file->isOpen()) {
return;
}
if (startPos >= fileSize) {
return;
}
//文件切换到对应位置
file->seek(startPos);
qint64 endPos = fileSize - 1;
qint64 bufferSize = fileSize - startPos;
QByteArray data = getHeadData("HTTP/1.1 200 OK", startPos, endPos, bufferSize);
//计算并发送数据
writeByteCount = data.size() + (fileSize - startPos);
tcpSocket->write(data);
emit sendData(data);
//qDebug() << TIMEMS << "writeData200";
}
void FilePushClient::writeData206(qint64 startPos, qint64 endPos)
{
if (!file->isOpen()) {
return;
}
if (startPos >= fileSize || startPos >= endPos) {
return;
}
//文件切换到对应位置
file->seek(startPos);
qint64 bufferSize = endPos - startPos + 1;
QByteArray data = getHeadData("HTTP/1.1 206 Partial Content", startPos, endPos, bufferSize);
//计算并发送数据
writeByteCount = data.size() + (fileSize - startPos);
tcpSocket->write(data);
emit sendData(data);
//qDebug() << TIMEMS << "writeData206";
}
五、功能特点
5.1 文件推流
- 指定网卡和监听端口,接收网络请求推送音视频等各种文件。
- 实时统计显示每个文件对应的访问数量、总访问数量、不同IP地址访问数量。
- 可指定多种模式,0-直接播放、1-下载播放。
- 实时打印显示各种收发请求和应答数据。
- 每个文件对应MD5加密的唯一标识符,用于请求地址后缀区分访问哪个文件。
- 支持各种浏览器(谷歌chromium/微软edge/火狐firefox等)、各种播放器(vlc/mpv/ffplay/potplayer/mpchc等)打开请求。
- 播放过程中可以任意切换播放进度,支持倍速播放。
- 需要推流的文件名称历史记录自动存储和打开加载应用。
- 切换文件获取访问地址,自动拷贝地址到剪切板方便直接粘贴测试使用。
- 极低CPU占用,128路1080P同时推流不到1%CPU占用,异步发送数据机制。
- 纯QTcpSocket通信,不依赖流媒体服务程序,核心源码不到500行,注释详细,功能完整。
- 支持Qt4/Qt5/Qt6任意版本,支持任意系统(windows/linux/macos/android/嵌入式linux等)。
5.2 网络推流
- 支持各种本地视频文件和网络视频文件。
- 支持各种网络视频流,网络摄像头,协议包括rtsp、rtmp、http。
- 支持将本地摄像头设备推流,可指定分辨率和帧率等。
- 支持将本地桌面推流,可指定屏幕区域和帧率等。
- 自动启动流媒体服务程序,默认mediamtx(原rtsp-simple-server),可选用srs、EasyDarwin、LiveQing、ZLMediaKit等。
- 可实时切换预览视频文件。
- 推流的清晰度和质量可调。
- 可动态添加文件、目录、地址。
- 视频文件自动循环推流,如果视频源是视频流,在掉线后会自动重连。
- 网络视频流自动重连,重连成功自动继续推流。
- 网络视频流实时性极高,延迟极低,延迟时间大概在100ms左右。
- 推流后除了用rtmp地址访问以外,还支持直接hls/webrtc访问,可以直接浏览器打开看实时画面。
- 支持Qt4/Qt5/Qt6任意版本,支持任意系统(windows/linux/macos/android/嵌入式linux等)。