不久前的一个☀️晴朗的早晨,我坐在办公室里一边🔥疯狂敲打键盘,看似努力工作实则是和我的小伙伴们💬聊得不亦乐乎,一边享受着第一口热豆浆☕。突然,身后传来了一声咳嗽声,🙀紧接着一个声音传过来了:"Achuy,跟我来会议室一趟".
😰😰我心头一跳,没想到老大会亲自来我工位找我,还看到我摸鱼了😬。怀着忐忑不安的心情,跟随老大一路走到会议室。
"坐",老大一脸严肃的坐在另一边的位置上与我对望😳。是要警告我不要在上班一直聊天吧?还是说看到我最近的周报,对我近期的工作进度不满意?我头脑瞬间涌现出纷繁的思绪.
"Achuy呀,在整个开发组里面,云课堂客户端一直是你负责,最近产品提出一些新的想法,老板听后觉得这个想法满足用户的需求,并提供良好的用户体验。还能够跟上市场趋势📈,并能以合理的价格和高质量的性能吸引消费者的关注💰,所以我想跟你谈谈一些新增需求"。
听到这里我点了点头,我的内心渐渐平静,犹如大海的波涛渐渐平息🌊🌊,恢复了宁静与平和😌。此刻,所有的焦虑与不安都得到了释放。"嗯还好,需求而已,不是讲摸鱼的事情一切好说",我内心想到😁。
接着老大继续说到:"目前的云课堂客户端主要是用到janus和srs进行直播和互动,现在我们需要做到的就是直接获取硬件组那边录播摄像头的rtsp流,能够实时录制、直播和互动📹🎥".
听到这个需求,我内心拒绝🙅♂️🚫,但嘴上却说:"相当于就是云课堂客户端对接硬件录播那边的流,这样能够直接获取录播的rtsp流,这意味着用户可以随时随地进行实时录制,直播和互动。无论是家庭聚会、企业会议还是教学活动,都能轻松记录下来,分享给更多的人,这样后面也能软件+硬件捆绑在一起直接俄一套卖了,挺好的呀👌".
"嗯,是的,目前需要你先试验一下,在electron如何播放rtsp流,大概需要多久时间?",老大的嘴角浮现出一丝丝的微笑😏。
我心头一紧😬,想到我最近时间的摸鱼,还被老大发现了,暗自下定决心,要快速做出一番成绩👊💪。硬着头皮说:"2天..."🗓️🕑。
回到工位上,开始到各大搜索网站上搜各种能在electron上播放rtsp流的方法🕵️♂️👨💻。
electron应用本质上就是网页套壳的播放应用,所以这个问题本质上就是如何在网页上播放rtsp流。
网页播放rtsp流常见操作
- 安装vlc插件
- rtsp转rtmp然后使用videojs通过flash播放rtmp
- 后端通过ffmpeg实时转码将rtsp流转成video标签能播放的形式
第一套方案已经不太好实现了,现今的浏览器对于vlc插件几乎都不再支持了。
第二套方案flash在2020年也将被chrome停止支持
第三套方案是需求是直接获取录播的rtsp流,所以也不能使用了。
怎么办,第一天就这么过去了...
第二天到了,今天的天气仿佛被灰蒙蒙的云层所笼罩,阳光被遮蔽在厚重的阴霾中。内心之中,我感到一丝焦虑的种子悄然滋生,如同那被层层阴云遮掩的太阳,让我倍感失落和彷徨。与天气的阴沉相伴,我的内心也被犹如漩涡般的焦虑所缠绕,深深地嵌入其中,难以自拔。我顶着个黑眼圈来到我的工位,开起电脑眼前发黑,挨在椅子上。
这时,旁边的李子拍了拍我说到:"怎么回事,没点精神"。我回到:"新需求,网上没啥有用的资料"。李子说:"你之前不是自己搭建了一个Chatgpt应用吗?有问过Chatgpt吗?"。突然间我仿佛看到一道光,犹如夜空中的一颗明星,突然破晓,划破黑暗;或像雷鸣般突如其来,震撼人心,令人无法忽视。它不需要多言,却能一瞬间点燃思绪,引起人们对于世界的深思,使人醒悟于存在的意义。
噢,是的,我完成了ChatGPT的搭建,应该是用得上了呢,迫不及待地来试试看吧。
运用我的CV大法,抄到我的electron应用中,然后运行
嗯,问题不大,应该是这个库版本问题,去github上找找这个node-rtsp-stream库,看看例子demo
跟着demo走,接着一番操作后试错后,成功了,但结果不太妙
js
const rtspStream = new Stream({
name: 'name',
streamUrl: 'rtsp://192.168.2.162:554/1',
wsPort: 9925,
ffmpegOptions: { // options ffmpeg flags
'-stats': '', // an option with no neccessary value uses a blank string
'-r': 30 // options with required values specify the value after the key
}
})
setTimeout(()=>{
var player = new window.jsmpeg(new WebSocket('ws://localhost:9925/'), {
canvas: document.querySelector('canvas') // Canvas should be a canvas DOM element
})
},2000)
这里注意的是npm下载后引入的jsmpeg有问题,所以从node_modules中拿到,通过require引入,通过查看代码发现
网上一堆直接就将url地址是ws://xxxx
的放入到jsmpeg中结果发现 net::ERR_DISALLOWED_URL_SCHEME
,从源码中看到如果直接以字符串的形式放入,是通过ajax获取数据的,但是node-rtsp-stream实际上是通过socket传输数据的。下图就是结果,一直花屏。
天色渐暗,傍晚将至,我定下的期限越来越近,我仿佛看到老大询问我试验的情况,我心中烦忧。我重新回顾整个寻找方法的过程,目光落在了播放rtsp流常用操作的第三条后端通过ffmpeg实时转码将rtsp流转成video标签能播放的形式,我猛地想到,node-rtsp-stream实际上就是本地开启服务,然后进行数据传输而已,那么也就是说我也可以本地开启服务,直接用ffmpeg来转,接着数据传输不就完了吗?
接着开始往这个方向去找各种资料,终于集众家所长,硬是在最后时刻试验成功
核心思路
- electron通过线程开启本地ffmpeg服务将rtsp视频流转换为flv
- electron开启本地websocket服务,通过websocket传输flv视频流
- electron视图连接本地websocket,在获取到视频流后,使用flvjs对视频流再一次处理并进行播放
最终,赶在临下班前的半小时,找到老大说,这需求没问题,能做👊👊👊。
完整代码
js
const http = require("http");
const { WebSocketServer } = require("ws");
const { spawn } = require("child_process");
const webSocketStream = require("websocket-stream/stream");
class LocalFFmpegServer {
constructor() {
this.app = null;
this.port = 30000;
}
parseURL(url) {
var list = url.split("?");
console.log(list);
var _url = list.shift();
var params = list.join("?");
if (params === "") {
return {
url: _url,
params: {}
};
}
var result = {};
var paramsList = params.split("&");
paramsList.forEach((item) => {
var l = item.split("=");
var key = l.shift();
result[key] = l.join("=");
});
console.log(_url);
return {
url: _url,
params: result
};
}
init() {
var _uid = 0;
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Not Find Server");
});
const wss = new WebSocketServer({ server });
wss.on("connection", (ws, req) => {
_uid += 1;
const id = _uid;
var { url, params } = this.parseURL(req.url);
console.log(`【${id} process open】\nurl =>${url}\nparams =>${params}`);
if (params['url'] === void 0) {
reject();
}
const stream = webSocketStream(
ws,
{
binary: true,
browserBufferTimeout: 1000000
},
{
browserBufferTimeout: 1000000
}
);
var ffmpegProcess = null;
try {
// 这里的ffmpeg需要将打包的地址进行特换
// 如xxx/xxx/ffmpeg.exe
ffmpegProcess = spawn("ffmpeg", [
"-i",
params['url'],
"-c:v",
"copy",
"-c:a",
"copy",
"-f",
"flv",
"pipe:1"
]);
// 处理错误信息
ffmpegProcess.stderr.on("data", (data) => {
console.error(`【${id}】FFmpeg Error: ${data}`);
});
// 监听FFmpeg进程的退出事件
ffmpegProcess.on("close", (code) => {
if (code !== 0) {
console.error(`【${id}】FFmpeg process exited with code ${code}`);
}
});
ffmpegProcess.stdout.pipe(stream);
} catch (e) {
if (ffmpegProcess !== null) {
ffmpegProcess.kill();
}
console.log(e);
}
});
this.app = server;
// 避免端口被电脑其他服务占用,检测到无法使用后,端口自动加一,直至能使用为止
var openServer = () => {
console.log("try open Server");
if (this.app === null) {
reject();
return;
}
server
.listen(this.port, () => {
resolve();
console.log(`Server listening on port ${this.port}`);
})
.on("error", () => {
this.port += 1;
setTimeout(openServer, 1000);
});
server.on("close", function() {
console.log("LocalFFmpegServer close");
});
};
openServer();
});
}
destory() {
if (this.app !== null) {
this.app.close();
this.app = null;
}
}
}
使用方式
js
var ffmpegServer = new LocalFFmpegServer();
// 不使用时记得调用destory释放服务,释放资源避免引发奇奇怪怪的问题
ffmpegServer.init().then(() => {
if (flvjs.isSupported()) {
let video = this.$refs.player;
this.rtsp = "rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream";
// this.rtsp = "rtsp://192.168.2.162:554/1"
if (video) {
this.player = flvjs.createPlayer({
type: "flv",
isLive: true,
url: `ws://localhost:${ffmpegServer.port}/rtsp/101/?url=${this.rtsp}`
});
this.player.attachMediaElement(video);
try {
this.player.load();
this.player.play();
} catch (error) {
console.log(error);
}
}
}
});
rtsp测试地址
分享rtsp测试地址
来源 | 地址 | 延迟 |
---|---|---|
Nordland | rtsp://77.110.228.219/axis-media/media.amp | 200ms |
Norwich | rtsp://37.157.51.30/axis-media/media.amp | 250ms |
Orlando | rtsp://97.68.104.34/axis-media/media.am | 350ms |
PriceCenterPlaza | rtsp://132.239.12.145:554/axis-media/media.amp | 280ms |
Vaison-La-Romaine | rtsp://176.139.87.16/axis-media/media.amp | 差 |
VyhladJazero | rtsp://stream.strba.sk:1935/strba/VYHLAD_JAZERO.stream | 160ms |
Western Cape | rtsp://196.21.92.82/axis-media/media.amp | 450ms |
Zeeland | rtsp://213.34.225.97/axis-media/media.amp | 270ms |
Allendale | rtsp://71.83.5.156/axis-media/media.amp | 270ms |
Bedford Hills | rtsp://73.114.177.111/axis-media/media.amp | 340ms |