electron播放rtsp流

发布时间 2023-11-07 08:50:06作者: 风的线条昵称已被使用

需要在客户端播放 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>