3

The following code works when the video observing client is loaded first then the webcam client is loaded second it works flawlessly, however, if the order is switched or in any way the stream is interrupted for example by refreshing either client the stream will fail and the Media Source will change its ready state to closed.

My assumption is that the video being received on start needs initialization headers for starting and since the stream is being read midstream it never gets said initialization headers. I am unsure of how to even add such header to the webm file.

I have tried to change the sequence mode on the source buffer which did nothing. I have tried restarting the video recorder and that works, but my final plan is to have multiple observing clients and the video recorder restarting on every reconnection is not optimal.

Camera Client

main();
function main() {
    if (hasGetUserMedia()) {
        const constraints = {
            video: {
                facingMode: 'environment',
                frameRate: {
                    ideal: 10,
                    max: 15
                }
            },
            audio: true
        };

        navigator.mediaDevices.getUserMedia(constraints).
        then(stream => {
            setupRecorder(stream);
        });
    }
}

function setupRecorder(stream) {
    let mediaRecorder = new MediaRecorder(stream, {
        mimeType: 'video/webm; codecs="opus, vp9"'
    });

    mediaRecorder.ondataavailable = e => {
        var blob = e.data;
        socket.emit('video', blob);
    }

    mediaRecorder.start(500);
}

The server just broadcasts whatever is received

Observing Client

var sourceBuffer;
var queue = [];
var mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', sourceOpen, false);
main();

socket.on('stream', data => {
    if (mediaSource.readyState == "open") {
        if (sourceBuffer.updating || queue.length > 0) {
            queue.push(data.video);
        } else {
            sourceBuffer.appendBuffer(data.video);
        }
    }
});

function main() {
    videoElement = document.querySelector('#video');
    videoElement.src = URL.createObjectURL(mediaSource);
}

function sourceOpen(e) {
    console.log('open');
    sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="opus, vp9"');
    sourceBuffer.addEventListener('updateend', () => {
        console.log(sourceBuffer.updating, mediaSource.readyState);

        if (queue.length > 0 && !sourceBuffer.updating) {
            sourceBuffer.appendBuffer(queue.shift());
        }
    });
}

So the code, in fact, works just in a way that is not correct so nothing is wrong with the server of socket sending. It either has something to do with the MediaRecorder or MediaSource.

1 Answers1

4

My assumption is that the video being received on start needs initialization headers for starting and since the stream is being read midstream it never gets said initialization headers.

Correct!

To solve this, you need to know a bit about the WebM format. WebM is just a subset of Matroska (MKV). Matroska is a specification of a schema for storing media in EBML. EBML is a binary file format that can have arbitrary blocks. Think of it like a binary XML.

What this means is that you can use tools like EBML Viewer to inspect WebM files, and reference the Matroska specs to understand what's going on. For example:

EBMLViewer Example

This is the inspection of a WebM file that was pre-recorded. It will play fine in browsers. You'll note that there are elements that are nested.

There are two top-level elements found in every WebM file. EBML, which defines this binary file, and Segment which contains everything after.

Within Segment there are a couple elements that matter to you. One of which is Tracks. You'll note that this file has two tracks, one for audio in Opus, and one for video in VP9. The other important block is Info, which contains information about the timescale and some metadata about the muxer.

After all of that metadata, you find Cluster, Cluster, Cluster, etc. These are the places in which you can cut a WebM stream, provided that each Cluster begins with a keyframe.

In other words, your code should do the following:

  • Save all data before the first Cluster as "initialization data".
  • Split on Cluster after that.

On playback:

  • Use the previously-saved "initialization data" as the first thing you load in.
  • Start loading in Clusters after that, starting wherever you want in the stream.

Now, that who cluster-needs-a-keyframe bit is important. As far as I know, there's no way to configure MediaRecorder to do this, and browsers are particularly picky about this. At a minimum, you'll have to remux server-side... you might even need to re-encode. See also: Encoding FFMPEG to MPEG-DASH – or WebM with Keyframe Clusters – for MediaSource API

using media source with socket.io

I should point out that you don't even need MediaSource for this. You definitely don't need Socket.IO. It can be as simple as outputting this data over a normal HTTP stream. This is loadable directly in a <video> element. (By all means, use MediaSource if you want additional control, but it isn't necessary.)

Brad
  • 159,648
  • 54
  • 349
  • 530
  • Thank you for your response, Brad, it was very inciteful. I did try loading my video right into a video element, but it was not seamless which led me to use MediaSource, but I will try what you are recommending. Also, a question would be how would I even go about pulling the metadata to parse the clusters? – Justin Fernald May 09 '19 at 15:29
  • @JustinFernald You'll have to parse the WebM in your code. This is totally doable in JavaScript. WebM is pretty easy to decode. I think last time I did this, I ended up writing my own demuxer, but it looks like there's at least one package up on NPM that may be a starting place for you: https://github.com/mafintosh/webm-cluster-stream Really though, spend an afternoon with the Matroska spec and a hex editor. It's pretty straightforward once you get it! – Brad May 09 '19 at 15:37
  • I will be honest I've been looking into FFMPEG, Dash, and Matrosta, and I'm nowhere closer to my goal. I'm very lost on how I would send my video to FFMPEG then receive the output and send it to the observer. I initially thought that if I would just send the initialization segment then send the data it would work, however, after some testing, any interruption in the stream destroys the stream. I know this is kind of weird but Brad I would love an opportunity just to speak with you for a little and pick your brain as I'm very new to streaming all that. Documentation is not helping enough. – Justin Fernald May 23 '19 at 17:21
  • @JustinFernald Sure, drop me an e-mail at brad@audiopump.co with when you're available. I am available to hire for consulting, but happy to also chat briefly for free just to see if I can point you in the right direction. – Brad May 23 '19 at 17:42
  • Hey guys, did either of you find a library or code for creating the initialization headers? I've an electron app that records audio using MediaRecorder Web API, making lots of short audio files, saving them as WebM audio files. Outside of my app I can play each recording individualy, but they do not have any initialization headers, so I can't do much else (e.g. no scroll). EBML Viewer for any of my files says "the variable-length integer 0x01FF...FF does not represent valid entry size (file position #40)". I assume my files are just the audio data with no header. – Adam Marsh Jan 03 '21 at 00:42
  • 1
    @AdamMarsh I don't know of any off-the-shelf demuxer. EBML Viewer is giving you that error message because your file is for streaming, which means it has indefinite length. The example above is just to show what's in these types of files. You'll still have to write the demuxer and debug with a hex editor. You really do just need to split on the first cluster... so `0x1f43b675`... when you see that, you have the start of a cluster. Everything before that is essentially initialization data. – Brad Jan 03 '21 at 00:46
  • Thanks for your help @Brad, in the end I managed to use https://mkvtoolnix.download/doc/mkvmerge.html, making command line calls from the node.js app. This made webm compliant files when I merged them together using mkvmerge (or a single compliant file if I merged just a single file). The UI option was great to understand what I was trying to do, and then CLI calls made it automatic. – Adam Marsh Jan 07 '21 at 01:15
  • Is there a way to know which chunk has the initialization data in the browser mediaRecorder.ondataavailable = function(e) { e.data } before to send it to the backend?? Thanks! – JRichardsz Jan 14 '23 at 17:20