最近在做个桌面应用程序,用来播放rtsp视频,随着chrome将flash打入冷宫,因此算是基于最近新的前端技术做个dome耍耍(2022.04.11)
node.js 需大于等于12, vite需要
// 安装vite
npm init vite@latest
// 根据提示,完成接下来的选择,如下图所示
使用编辑器打开刚刚的项目,目录如下所示
安装npm包
// 安装vite及vue依赖
npm i
// 安装electron electron-builder
npm i electron electron-builder -D
// 安装解析需使用包,一定要用-S或者--save,否则打包之后会找不到该模块
npm i ffmpeg-static -S // 可按照不同系统自动下载对应的ffmpeg二进制文件
// rtsp-stream, 用-S或者--save,理由同上
npm i node-rtsp-stream -S
// 安装jsmpeg-player
npm i jsmpeg-player -D
// 安装concurrently及wait-on用于启动electron和vite
npm i concurrently wait-on -D
修改package文件,注意把注释部分去掉,json文件无法添加注释
{
"name": "electron-player",
"private": true,
"version": "0.0.0",
"main": "electron/main",
"scripts": {
// 添加启动指令
"start": "concurrently \"vite\" \"wait-on tcp:3000 && electron .\"",
// 添加打包指令
"dist": "vite build && electron-builder",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"ffmpeg-static": "^5.0.0",
"node-rtsp-stream": "^0.0.9",
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.0",
"concurrently": "^7.1.0",
"electron": "^18.0.3",
"electron-builder": "^22.14.13",
"jsmpeg-player": "^3.0.3",
"typescript": "^4.5.4",
"vite": "^2.9.0",
"vue-tsc": "^0.29.8",
"wait-on": "^6.0.1"
},
/**
* 修改electron打包文件位置,
* 默认为dist,因vite打包默认也是dist,
* 所以修改vite或者electron的打包位置
*/
"build": {
"directories": {
"output": "dist_electron"
}
}
}
修改vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
return {
base: command == 'serve' ? '/' : './', // 打包后文件要用相对路径,否则页面空白
plugins: [vue()],
}
})
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
// app生命周期相关
app.on('ready', createWindow)
.on('window-all-closed', closeWindow)
// rtsp相关,ffmpeg-static会自动下载对应平台windos,linux,mac(intle),mac(M1版本)的ffmpeg二进制文件
const ffmpegPath = require('ffmpeg-static')
/**
stream相关配置及方法
name = options.name
streamUrl = options.streamUrl
width = options.width
height = options.height
wsPort = options.wsPort
nputStreamStarted = false
stream = undefined
ffmpegPath = options?.ffmpegPath ?? ffmpeg
stop()
*/
const Stream = require('node-rtsp-stream')
/**
* rtsp列表
* interface {
* rtspUrl: {
* ws: websocket地址
* stream: stream实例
* }
* }
*/
const rtspOpenders = {}
let addPort = 9000
/**
* 开启rtsp
* @param rtsp {String} rtsp流地址
*/
ipcMain.on('openRtsp', (event, rtsp) => {
/** 判断是否已开启,若已开启,直接返回ws地址, 未开启则先开启再返回 */
if (rtspOpenders[rtsp]) {
event.returnValue = {
code: 200,
msg: '开启成功',
ws: rtspOpenders[rtsp].ws
}
} else {
addPort++
const stream = new Stream({
name: `socket-${addPort}`,
streamUrl: rtsp,
wsPort: addPort,
ffmpegPath: app.isPackaged ? ffmpegPath.replace('app.asar', 'app.asar.unpacked') : ffmpegPath,
ffmpegOptions: {
'-stats': '',
'-r': 30
}
}).on('exitWithError', () => {
stream.stop()
delete rtspOpenders[rtsp]
event.returnValue = {
code: 400,
msg: '开启失败'
}
})
rtspOpenders[rtsp] = {
ws: `ws://localhost:${addPort}`,
stream: stream
}
event.returnValue = {
code: 200,
msg: '开启成功',
ws: rtspOpenders[rtsp].ws
}
}
})
/**
* 关闭rtsp
*/
ipcMain.on('closeRtsp', (event, rtsp) => {
if (rtspOpenders[rtsp]) {
// 停止解析
rtspOpenders[rtsp].stream.stop()
// 删除该项
delete rtspOpenders[rtsp]
// 返回结果
event.returnValue = {
code: 200,
msg: '关闭成功'
}
} else {
event.returnValue = {
code: 400,
msg: '未找到该rtsp'
}
}
})
// 创建窗口
function createWindow () {
// 创建窗口
const win = new BrowserWindow({
// frame: false, // 隐藏目录,window下很难看
width: 800,
height: 600,
webPreferences: {
// 允许web断使用node
nodeIntegration: true,
contextIsolation: false,
// 同源策略关闭
webSecurity: false
}
})
// 加载html, 打包环境加载dist下打包文件, 开发环境加载vite服务,vite默认端口3000
const htmlUrl = app.isPackaged ? `file://${path.join(__dirname, '../dist/index.html')}` : `http://localhost:3000`
win.loadURL(htmlUrl)
// 开发环境, 打开chrome调试工具
if (!app.isPackaged) win.webContents.openDevTools()
}
// 关闭窗口
function closeWindow () {
/**
* 在macOS,除非cmd+q确定退出,否则绝大部分应用及其菜单栏都保持激活
*/
if (process.platform !== 'darwin') app.quit()
}
<script setup lang="ts">
import { ref } from 'vue'
import MpegPlayer from 'jsmpeg-player'
const { ipcRenderer } = require('electron')
const rtspUrl = ref('')
const mpegPlayer = ref()
const msg = ref('')
let player: any = null
const open = () => {
const res = ipcRenderer.sendSync('openRtsp', rtspUrl.value)
if (res.code === 200) {
player = new MpegPlayer.VideoElement(mpegPlayer.value, rtspUrl.value)
}
msg.value = res.msg
}
const close = () => {
const res = ipcRenderer.sendSync('closeRtsp', rtspUrl.value)
msg.value = res.msg
}
script>
<template>
<div class="flexBox">
<input type="text" v-model="rtspUrl">
<button @click="open">打开rtspbutton>
<button @click="close">关闭rtspbutton>
div>
<div class="mpegPlayer" ref="mpegPlayer">div>
<div>{{msg}}div>
template>
<style>
* {
padding: 0;
margin: 0;
}
.flexBox {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
}
.flexBox input {
height: 30px;
width: 500px;
box-sizing: border-box;
padding-left: 8px;
}
.flexBox button {
height: 30px;
padding: 0 12px;
}
.mpegPlayer {
width: 800px;
height: 450px;
background: #ccc;
}
style>
// 开启
npm run start
// 打包
npm run dist
不能在纯浏览器中实现,需要线程来调用ffmpeg解析rtsp
资源消耗相对vlc来说很大,如下图对比(解析同一rtsp,1920*1080分辨率)
优化思路:注意到electron时没有调用gpu,也许可以优化ffmpeg的命令来调用gpu解析,多路播放就很可靠了,也许可以达到vlc的性能?
@ffmpeg-installer/ffmpeg 这个包,经测试未匹配M1版本的mac,需手动下载对应的ffmpeg运行文件并配置环境变量,相关代码部分已添加注释(ps:手里的mac是15年的intle版本,此部分暂时无能为力,或者去催催@ffmpeg-installer/ffmpeg作者咯)
2022/04/13 更新
已将@ffmpeg-installer/ffmpeg 替换为 ffmpeg-static ,ffmpeg-static支持M1版本mac,详情查看ffmpeg-static - npm