摄像机流添加OSD

发布时间 2023-11-07 08:50:06作者: 风的线条昵称已被使用
export default class MediaStreamMixer {
	constructor() {
	}

	/**
	 *
	 * @param opts
	 */
	mix(opts) {
		let {audioTrack, videoTrack, stream} = opts || {};
		const ms = new MediaStream();
		if (stream instanceof MediaStream) {
			audioTrack = stream.getAudioTracks()[0];
			videoTrack = stream.getVideoTracks()[0];
		}
		if (audioTrack instanceof MediaStreamTrack && audioTrack.kind === 'audio') {
			ms.addTrack(audioTrack);
		}
		//if (videoTrack instanceof MediaStreamTrack && videoTrack.kind === 'video') {
		const mixed = this.mixVideo(videoTrack);
		ms.addTrack(mixed);
		//}
		return ms;
	}

	mixVideo(videoTrack) {
		const canvas = document.createElement('canvas');
		const context = canvas.getContext('2d');
		if (videoTrack) {
			const trackProcessor = new MediaStreamTrackProcessor(videoTrack);
			const frameReader = trackProcessor.readable.getReader();
			const frameParser = ({done, value: videoFrame}) => {
				if (done) return
				this.#draw(context, videoFrame.displayWidth, videoFrame.displayHeight, videoFrame);
				videoFrame.close();
				frameReader.read().then(data => frameParser(data));
			}
			frameReader.read().then(data => frameParser(data));
		} else {
			const draw = () => this.#draw(context, 1280, 720, null, '虚拟视频');
			draw();
			setInterval(draw.bind(this), 1E3);
		}
		return canvas.captureStream().getVideoTracks()[0];
	}


	mixVideo2(videoTrack) {
		if (videoTrack) {
			const canvas = new OffscreenCanvas(1, 1);//document.createElement('canvas');
			const context = canvas.getContext('2d');
			const trackProcessor = new MediaStreamTrackProcessor({track: videoTrack});
			const trackGenerator = new MediaStreamTrackGenerator({kind: "video"});
			const transformer = new TransformStream({
				transform: async (videoFrame, controller) => {
					this.#draw(context, videoFrame.displayWidth, videoFrame.displayHeight, videoFrame);
					const newFrame = new VideoFrame(canvas, {timestamp: +new Date()});
					videoFrame.close();
					controller.enqueue(newFrame);
				}
			});
			trackProcessor.readable.pipeThrough(transformer).pipeTo(trackGenerator.writable);
			return trackGenerator;
		} else {
			const canvas = document.createElement('canvas');
			const context = canvas.getContext('2d');
			setInterval(() => this.#draw(context, 640, 480, null), 1E3);
			return canvas.captureStream().getVideoTracks()[0];
		}
	}

	#draw(ctx, width, height, img, osd = '') {
		ctx.canvas.width = width;
		ctx.canvas.height = height;
		if (img) {
			ctx.drawImage(img, 0, 0);
		} else {
			ctx.fillStyle = '#000';
			ctx.fillRect(0, 0, width, height);
		}
		const fontSize = Math.max(12, height / 20 | 1);
		const [textHeight, margin, padding] = [fontSize, fontSize * 0.4, fontSize * 0.2];
		ctx.font = `${fontSize}px msyh`;
		let text = new Date().toLocaleString();
		let textWidth = ctx.measureText(text).width;
		this.#drawText(ctx, text, margin, margin, textWidth, textHeight, padding);

		if (osd) {
			text = osd;
			textWidth = ctx.measureText(text).width;
			this.#drawText(ctx, text, width - (textWidth + 2 * padding + margin), height - (textHeight + 2 * padding + margin), textWidth, textHeight, padding);
		}
	}

	#drawText(ctx, text, x, y, textWidth, textHeight, padding = 0) {
		const [w, h] = [textWidth + 2 * padding, textHeight + 2 * padding];
		ctx.fillStyle = '#808080c0';
		ctx.fillRect(x, y, w, h);
		ctx.fillStyle = '#fff';
		ctx.textAlign = 'center';
		ctx.textBaseline = 'middle';
		ctx.fillText(text, x + w / 2, y + h / 1.75);
	}
}