6

I am trying to use the MediaSource API to append separate WebM videos to a single source.

I found a Github project that was attempting the same thing, where a playlist of WebMs is loaded, and each one is appended as a SourceBuffer. But it was last committed a year ago, and thus out-of-sync with the current spec. So I forked it and updated to the latest API properties/methods, plus some restructuring. Much of the existing code was taken directly from the spec’s examples and Eric Bidelman’s test page.

However, I can not get it to work as expected. I am testing in two browsers, both on Mac OS X 10.9.2: Chrome 35 stable (latest at the time of this writing), and Firefox 30 beta with the flag media.mediasource.enabled set to true in about:config (this feature will not be introduced until FF 25, and current stable is 24).

Here are the problems I’m running into.

Both browsers

I want the video to be, in the end, one long video composed of the 11 WebMs (00.webm, 01.webm, …, 10.webm). Right now, each browser only plays 1 segment of the video.

Chrome

Wildly inconsistent behavior. Seems impossible to reproduce any of these bugs reliably.

  • Sometimes the video is blank, or has a tall black bar in the middle of it, and is unplayable.
  • Sometimes the video will load and pause on the first frame of 01.webm.
  • Sometimes, the video will play a couple of frames of the 02.webm and pause, having only loaded the first three segments.
  • The Play button is initially grayed out.
  • Pressing the grayed out Play button produces wildly inconsistent behaviors. Sometimes, it loads a black, unplayable video. Other times, it will play the first segment, then, when you get to the end, it stops, and when you press Play/Pause again, it will load the next segment. Even then, it will sometimes skip over segments and gets stuck on 04.webm. Regardless, it never plays the final segment, even though the console will report going through all of the buffers.

It is honestly different every time. I can’t list them all here.

Known caveats: Chrome does not currently implement sourceBuffer.mode, though I do not know what effect this might have.

Firefox

  1. Only plays 00.webm. Total running time is 0:08, the length of that video.
  2. Video seeking does not work. (This may be expected behavior, as there is nothing actually happening in the onSeeking event handler.)
  3. Video can not be restarted once finished.

My initial theory was that this had to do with mediaSource.sourceBuffers[0].timestampOffset = duration and duration = mediaSource.duration. But I can’t seem to get anything back from mediaSource.duration except for NaN, even though I’m appending new segments.

Completely lost here. Guidance very much appreciated.

EDIT: I uncommented the duration parts of the code, and ran mse_webm_remuxer from Aaron Colwell's Media Source Extension Tools (thanks Adam Hart for the tips) on all of the videos. Voila, no more unpredictable glitches in Chrome! But alas, it still pauses once a media segment ends, and even when you press play, it sometimes gets stuck on one frame.

In Firefox Beta, it doesn’t play past the first segment, responding with:

TypeError: Value being assigned to SourceBuffer.timestampOffset is not a finite floating-point value.

Logging the value of duration returns NaN (but only in FF).

Community
  • 1
  • 1
Hugh Guiney
  • 1,309
  • 2
  • 19
  • 34
  • Looks like Firefox does not implement MediaSource.duration: https://bugzilla.mozilla.org/show_bug.cgi?id=896884 – Hugh Guiney Jul 14 '14 at 00:44

2 Answers2

6

The main problem is with the video files. If you open chrome://media-internals/ you can see error Media segment did not begin with keyframe. Using properly formatted videos, like the one from Eric Bidelman's example (I hope he doesn't get mad that I keep linking directly to that video, but it's the only example video I've found that works), your code does work with the following change in appendNextMediaSegment():

duration = mediaSource.duration;
mediaSource.sourceBuffers[0].timestampOffset = duration;
mediaSource.sourceBuffers[0].appendBuffer(mediaSegment);

You can try Aaron Colwell's Media Source Extension Tools to try to get your videos working, but I've had limited success.

It also seems a little weird that you're looking at the onProgress event before appending segments, but I guess that could work if you only want to append if the video is actually playing. It could make the seekbar act odd since the video length is unknown, but that can be a problem in any case.

Adam Hart
  • 559
  • 4
  • 17
  • Thanks for the informative reply. However, that’s only one video—I’m not splitting it up into chunks like Eric’s doing. I want to combine separate videos together. I just ran `mse_webm_remuxer` from the tools you linked to on all of the videos. Voila, no more unpredictable glitches in Chrome! But alas, it still pauses once a media segment ends, and even when you press play, it sometimes gets stuck on one frame. In Firefox Beta, it doesn’t play past the first segment: `TypeError: Value being assigned to SourceBuffer.timestampOffset is not a finite floating-point value.` Will add this to the OP. – Hugh Guiney Jun 18 '14 at 15:01
  • 1
    I meant just using 12 full copies of his video back to back (not chunking them), which seems to play seamlessly for me and is a good way to test whether it's the videos or the javascript because it has the right keyframes and whatever else. Though yeah, I only tested it in Chrome. If I get some time, I'll mess around with it in firefox. – Adam Hart Jun 18 '14 at 23:34
  • OK, I tried that and it indeed works in Chrome. Still not working in Firefox, though my hunch is it may be an implementation issue where the duration property is never updated from the default value of `NaN`. – Hugh Guiney Jun 21 '14 at 20:18
  • Accepting this answer because it did help me get over the initial hurdle even if it’s not cross-browser. I also learned elsewhere on SO (can’t find the link) that you can get any WebM to work by encoding it with all keyframes, e.g. by using `-g 1` in ffmpeg. – Hugh Guiney Jul 07 '14 at 02:31
  • 1
    It seems from the spec that setting `mode` on the SourceBuffer to `"sequence"` is supposed to accomplish this automatically, but I haven't gotten it to work in Chrome. – protometa Dec 22 '14 at 20:35
0

I agree with the opinion Adam Hart said. With a webm file, I tried to implement an example like http://html5-demos.appspot.com/static/media-source.html and then made a conclusion that its problem caused the source file I used.

If you have an arrow left, how about trying to use "samplemuxer" introduced at https://developer.mozilla.org/en-US/docs/Web/HTML/DASH_Adaptive_Streaming_for_HTML_5_Video. In my opinion, samplemuxer is one of encoders like FFMPEG.

I found that the converted file works with mediaSource API. If you will also see it works, please let me know.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>MediaSource API Demo</title>
</head>
<body>

<h3>Appending .webm video chunks using the Media Source API</h3>

<section>
    <video controls autoplay width="320" height="240"></video>
    <pre id="log"></pre>
</section>


<script>
    //ORIGINAL CODE http://html5-demos.appspot.com/static/media-source.html

    var FILE = 'IU_output2.webm';

    var NUM_CHUNKS = 5;
    var video = document.querySelector('video');

    var mediaSource = new MediaSource();

    video.src = window.URL.createObjectURL(mediaSource);

    function callback(e) {
        var sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');

        logger.log('mediaSource readyState: ' + this.readyState);

        GET(FILE, function(uInt8Array) {
            var file = new Blob([uInt8Array], {type: 'video/webm'});
            var chunkSize = Math.ceil(file.size / NUM_CHUNKS);

            logger.log('num chunks:' + NUM_CHUNKS);
            logger.log('chunkSize:' + chunkSize + ', totalSize:' + file.size);

            // Slice the video into NUM_CHUNKS and append each to the media element.
            var i = 0;

            (function readChunk_(i) {
                var reader = new FileReader();

                // Reads aren't guaranteed to finish in the same order they're started in,
                // so we need to read + append the next chunk after the previous reader
                // is done (onload is fired).
                reader.onload = function(e) {

                    try {
                        sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
                        logger.log('appending chunk:' + i);
                    }catch(e){
                        console.log(e);
                    }

                    if (i == NUM_CHUNKS - 1) {
                        if(!sourceBuffer.updating)
                             mediaSource.endOfStream();
                    } else {
                        if (video.paused) {
                            video.play(); // Start playing after 1st chunk is appended.
                        }

                        sourceBuffer.addEventListener('updateend', function(e){
                            if( i < NUM_CHUNKS - 1 )
                                readChunk_(++i);
                        });
                    } //end if
                };

                var startByte = chunkSize * i;
                var chunk = file.slice(startByte, startByte + chunkSize);

                reader.readAsArrayBuffer(chunk);
            })(i);  // Start the recursive call by self calling.
        });
    }

    mediaSource.addEventListener('sourceopen', callback, false);
//    mediaSource.addEventListener('webkitsourceopen', callback, false);
//
//    mediaSource.addEventListener('webkitsourceended', function(e) {
//        logger.log('mediaSource readyState: ' + this.readyState);
//    }, false);

    function GET(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'arraybuffer';
        xhr.send();

        xhr.onload = function(e) {
            if (xhr.status != 200) {
                alert("Unexpected status code " + xhr.status + " for " + url);
                return false;
            }
            callback(new Uint8Array(xhr.response));
        };
    }
</script>
<script>
    function Logger(id) {
        this.el = document.getElementById('log');
    }
    Logger.prototype.log = function(msg) {
        var fragment = document.createDocumentFragment();
        fragment.appendChild(document.createTextNode(msg));
        fragment.appendChild(document.createElement('br'));
        this.el.appendChild(fragment);
    };

    Logger.prototype.clear = function() {
        this.el.textContent = '';
    };

    var logger = new Logger('log');
</script>
</body>
</html>

another test code

<!DOCTYPE html>
<html>
<head>
    <title>MediaSource API Demo</title>
</head>
<body>

<h3>Appending .webm video chunks using the Media Source API</h3>

<section>
    <video controls autoplay width="320" height="240"></video>
    <pre id="log"></pre>
</section>


<script>
    //ORIGINAL CODE http://html5-demos.appspot.com/static/media-source.html

    var FILE = 'IU_output2.webm';
    //    var FILE =  'test_movie_output.webm';

    var NUM_CHUNKS = 10;
    var video = document.querySelector('video');

    var mediaSource = new MediaSource();

    video.src = window.URL.createObjectURL(mediaSource);

    function callback(e) {
        var sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vorbis,vp8"');

        logger.log('mediaSource readyState: ' + this.readyState);

        GET(FILE, function(uInt8Array) {
            logger.log('byteLength:' + uInt8Array.byteLength );

            sourceBuffer.appendBuffer(uInt8Array);

        });
    }

    mediaSource.addEventListener('sourceopen', callback, false);
    //    mediaSource.addEventListener('webkitsourceopen', callback, false);
    //
    //    mediaSource.addEventListener('webkitsourceended', function(e) {
    //        logger.log('mediaSource readyState: ' + this.readyState);
    //    }, false);

    function GET(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.responseType = 'arraybuffer';
        xhr.send();

        xhr.onload = function(e) {
            if (xhr.status != 200) {
                alert("Unexpected status code " + xhr.status + " for " + url);
                return false;
            }
            callback(new Uint8Array(xhr.response));
        };
    }
</script>
<script>
    function Logger(id) {
        this.el = document.getElementById('log');
    }
    Logger.prototype.log = function(msg) {
        var fragment = document.createDocumentFragment();
        fragment.appendChild(document.createTextNode(msg));
        fragment.appendChild(document.createElement('br'));
        this.el.appendChild(fragment);
    };

    Logger.prototype.clear = function() {
        this.el.textContent = '';
    };

    var logger = new Logger('log');
</script>
</body>
</html>

Thanks.

GyuBeom Choi
  • 418
  • 3
  • 5