基于Node.js的低延迟开源流媒体服务开发:MediaSource对象不可用报错及流播放问题咨询
Hey there! Let's break down why you're hitting that "An attempt was made to use an object that is not, or is no longer, usable" error and get your twitch-with-nodejs streaming service working smoothly.
First, let's nail down the core issues in your current code:
- You're ignoring SourceBuffer's state: Your fixed 2-second interval tries to append new segments even if the previous
appendBufferoperation hasn't finished. WhenSourceBuffer.updatingistrue, callingappendBufferagain throws that exact error. - MediaRecorder isn't producing splittable WebM segments: The way you're manually stopping/starting MediaRecorder creates full WebM files with duplicate headers each time. MediaSource can't stitch these together properly—they need to be headerless, sequential chunks.
- Your server and client sync is off: Right now, the server only returns the latest chunk, but your client needs a continuous stream of ordered segments to play without gaps.
- Memory cleanup is timed poorly: Calling
sourceBuffer.remove()right after appending can interfere with the ongoing update process.
Fixes & Updated Code
1. Streamline the Broadcaster (MediaRecorder Setup)
Instead of manually stopping/starting MediaRecorder, use the built-in timeslice parameter to generate splittable, compatible WebM segments automatically. This ensures each chunk can be stitched together by MediaSource.
this.media = await navigator.mediaDevices.getUserMedia({ audio: this.config.audio, video: this.config.video ? { width: this.config.width, height: this.config.height, frameRate: this.config.fps, facingMode: (this.config.front ? "user" : "environment") } : false }).catch(e => { console.log(e.name + ": " + e.message); this.can = false; }); const video = document.querySelector('video'); video.srcObject = this.media; video.onloadedmetadata = () => { video.play() } // Use timeslice to auto-generate stitchable segments, match client MIME type const mediaRecorder = new MediaRecorder(this.media, { mimeType: "video/webm; codecs=\"vp8, vorbis\"" }); mediaRecorder.ondataavailable = async function(ev) { if (ev.data.size > 0) { // Skip empty chunks const data = new FormData(); data.append('file', ev.data); await fetch("/postStream", { method: "post", body: data }); } }; // Auto-split segments every 2 seconds mediaRecorder.start(2000);
2. Optimize the Server (Segment Management)
Maintain an ordered queue of segments, clean up old chunks automatically, and add an endpoint that returns sequential segments instead of just the latest one. This helps the client stay in sync.
this.chunks = []; const MAX_RETAIN_DURATION = 30 * 1000; // Keep last 30 seconds of segments // Auto-clean old segments every 5 seconds setInterval(() => { const cutoffTime = Date.now() - MAX_RETAIN_DURATION; this.chunks = this.chunks.filter(chunk => chunk.date > cutoffTime); }, 5000); app.post("/postStream", async (req, res) => { const fileProperty = req?.files?.file; if (!fileProperty?.data || fileProperty.mimetype !== 'video/webm') { return res.json(false); } // Add segments in order, use timestamp as ID for sync this.chunks.push({ data: fileProperty.data, id: Date.now(), date: Date.now() }); return res.json(true); }); // New endpoint: get all segments after a specific ID (for client sync) app.get("/getStreamSegments", (req, res) => { const lastId = parseInt(req.query.lastId) || 0; const newSegments = this.chunks.filter(chunk => chunk.id > lastId); if (newSegments.length === 0) { return res.json([]); } // Combine segments into one buffer (or send individually, either works) const combinedBuffer = Buffer.concat(newSegments.map(s => s.data)); res.setHeader("content-type", "video/webm"); res.setHeader("x-last-id", newSegments[newSegments.length - 1].id); // Send latest ID for next sync res.send(combinedBuffer); });
3. Fix the Viewer (MediaSource Sync)
Replace the fixed interval with event-driven fetching—only request new segments when the current one finishes processing. Also, sync with the server's segment IDs to avoid missing chunks, and clean up old buffer data safely.
const mediaSource = new MediaSource(); const videoPlaying = document.querySelector('#video-player'); videoPlaying.src = URL.createObjectURL(mediaSource); const mime = "video/webm; codecs=\"vp8, vorbis\""; let sourceBuffer; let lastSegmentId = 0; let isFetching = false; // Prevent concurrent requests mediaSource.addEventListener('sourceopen', async () => { sourceBuffer = mediaSource.addSourceBuffer(mime); // Trigger next fetch when current segment finishes processing sourceBuffer.addEventListener('updateend', fetchNextSegments); // Initial fetch to start playback await fetchNextSegments(); videoPlaying.play(); }); async function fetchNextSegments() { if (isFetching || sourceBuffer.updating) return; isFetching = true; try { const res = await fetch(`/getStreamSegments?lastId=${lastSegmentId}`); if (!res.ok) return; // Update sync ID with latest segment from server lastSegmentId = parseInt(res.headers.get('x-last-id')) || lastSegmentId; const data = await res.arrayBuffer(); if (data.byteLength > 0) { sourceBuffer.appendBuffer(data); // Safely clean up old buffer data (keep last 5 seconds for smooth playback) if (videoPlaying.currentTime > 5) { sourceBuffer.remove(0, videoPlaying.currentTime - 5); } } } catch (err) { console.error('Failed to fetch segments:', err); } finally { isFetching = false; // Keep polling for new segments if MediaSource is still open if (mediaSource.readyState === 'open') { setTimeout(fetchNextSegments, 1000); } } } // Add backup trigger: fetch more segments if buffer runs low videoPlaying.addEventListener('timeupdate', () => { const bufferEnd = sourceBuffer.buffered.length > 0 ? sourceBuffer.buffered.end(sourceBuffer.buffered.length - 1) : 0; // If less than 2 seconds of buffer left, request more if (bufferEnd - videoPlaying.currentTime < 2 && !isFetching) { fetchNextSegments(); } });
Extra Tips for Smooth Streaming
- Test on Chrome/Firefox first—they have the most robust MediaSource/WebM support.
- If you run into audio/video desync, try recording audio and video tracks separately and syncing them on the client.
- For true low-latency (sub-500ms), consider exploring WebRTC later—it's designed for real-time streaming, but your current setup is perfect for learning MediaSource fundamentals.
内容的提问来源于stack exchange,提问作者Pioupia

