electron播放rtsp流
需要在客户端播放 rtsp 流, 用了一些第三方的方案(ffmpeg + websocket + jsmpeg)效果不是很好, 根据实际需求便改为 ffmpeg + mjpeg, 效果不错.
<template>
<div style="display: inline-block; width: 160px; height: 120px; background-color: black">
<img draggable="false" ref="refImg" src="" alt="" style="pointer-events: none; border: none; max-width: 100%; max-height: 100%">
<div style="position: absolute; inset: 0; width: 100%; height: 100%; display: grid; place-items: center">
{{ dm.error || dm.status }}
</div>
</div>
</template>
<script setup>
import {reactive, onMounted, onBeforeUnmount} from "vue";
const FFMPEG_PATH = `D:\\tools\\ffmpeg\\bin\\ffmpeg.exe`;
const props = defineProps({
url: {type: String, default: '', required: true},
framerate: {type: Number, default: 20},
imgWidth: {type: Number, default: 320},
imgHeight: {type: Number, default: 240},
// 图像质量[1-31], 其中 1 是最高质量, 31 是最低质量
imgQuality: {type: Number, default: 15},
// 超时(毫秒)
timeout: {type: Number, default: 20E3},
});
const refImg = $ref(null);
const dm = reactive({
status: '',
error: '',
});
let cp;
let tmrTimeout;
const start = () => {
cp && stop();
dm.status = 'waiting';
dm.error = '';
tmrTimeout = window.setTimeout(() => {
dm.status = 'error';
dm.error = 'TIMEOUT';
close();
}, props.timeout);
const {url, framerate, imgWidth, imgHeight, imgQuality} = props;
const args = ['-i', url, '-f', 'mjpeg', '-c:v', 'mjpeg', '-q:v', imgQuality, '-r', framerate, '-s', `${imgWidth}x${imgHeight}`, '-'];
// args.unshift('-hwaccel', 'cuda', ); // 启用硬件解码, 启用后 CPU 占用是降低了, 但内存占用增加了
refImg.onload = () => URL.revokeObjectURL(refImg.src);
cp = require('node:child_process').spawn(FFMPEG_PATH, args, {detached: false, windowsHide: true});
cp.stdout.on('data', data => {
refImg.src = URL.createObjectURL(new Blob([data], {type: 'image/jpeg'}));
dm.status = dm.error = '';
cancelTimeout();
/*
let reader = new FileReader();
reader.onload = evt => {
if (evt.target.readyState === FileReader.DONE) {
refImg.src = evt.target.result;
reader = null;
}
}
reader.readAsDataURL(new Blob([data], {type: 'image/jpeg'}));
*/
});
cp.stderr.on('data', data => {
data = data.toString();
if (data.startsWith('ffmpeg')) {
} else if (data.startsWith('[') && data.includes('method DESCRIBE failed')) {
dm.status = 'error';
dm.error = data.split('\n')[0].split(':').at(-1);
cancelTimeout();
} else if (data.startsWith('Input #')) {
} else if (data.startsWith('Output #')) {
} else if (data.startsWith('frame')) {
} else {
}
});
cp.on('error', err => {
console.error('===spawn error===', err)
}).on('exit', (code, signal) => {
console.log('===spawn exit===', code);
});
}
const stop = () => {
dm.status = dm.error = '';
close();
}
const cancelTimeout = () => {
if (tmrTimeout) {
window.clearTimeout(tmrTimeout);
tmrTimeout = null;
}
}
const close = () => {
cancelTimeout();
URL.revokeObjectURL(refImg.src);
refImg.src = '';
cp?.kill();
cp = null;
}
defineExpose({
start,
stop
});
onMounted(() => {
});
onBeforeUnmount(() => {
stop();
});
</script>