javascriptsafarimobile-safariw3cmedia-source

The ManagedMediaSource API does not render content


Broadly browsers have the window.MediaSource source API. However, due to battery concerns Apple never adopted this API on mobile devices. In 2023 they released a spec for window.ManagedMediaSource which is suppose to have the same API but gives more control to the User Agent which reduces power impact. According to caniuse.com the ManagedMediaSource this API is live in Safari both iOS and desktop.

However! I don't think the window.ManagedMediaSource API is yet released and functional on Safari 18.1.11 on MobileSafari 18.0.1. Here is my demonstration of this.

index.html with MediaSource (working)

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>MSE Demo</title>
  </head>
  <body>
    <h1>MSE Demo</h1>
    <div>
      <video controls width="80%"></video>
    </div>

    <script type="text/javascript">
      (function () {
        var baseUrl =
          "https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/dash/";
        var initUrl = baseUrl + "init.mp4";
        var templateUrl = baseUrl + "segment_$Number$.m4s";
        var sourceBuffer;
        var index = 0;
        var numberOfChunks = 52;
        var video = document.querySelector("video");

        if (!window.MediaSource) {
          console.error("No Media Source API available");
          return;
        }

        var ms = new MediaSource();
        video.src = window.URL.createObjectURL(ms);
        ms.addEventListener("sourceopen", onMediaSourceOpen);

        function onMediaSourceOpen() {
          sourceBuffer = ms.addSourceBuffer('video/mp4; codecs="avc1.4d401f"');
          sourceBuffer.addEventListener("updateend", nextSegment);

          GET(initUrl, appendToBuffer);

          video.play();
        }

        function nextSegment() {
          var url = templateUrl.replace("$Number$", index);
          GET(url, appendToBuffer);
          index++;
          if (index > numberOfChunks) {
            sourceBuffer.removeEventListener("updateend", nextSegment);
          }
        }

        function appendToBuffer(videoChunk) {
          if (videoChunk) {
            sourceBuffer.appendBuffer(new Uint8Array(videoChunk));
          }
        }

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

          xhr.onload = function (e) {
            if (xhr.status != 200) {
              console.warn(
                "Unexpected status code " + xhr.status + " for " + url,
              );
              return false;
            }
            callback(xhr.response);
          };

          xhr.send();
        }
      })();
    </script>
  </body>
</html>

index.html with ManagedMediaSource (not working)

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>MSE Demo</title>
  </head>
  <body>
    <h1>MSE Demo</h1>
    <div>
      <video controls width="80%"></video>
    </div>

    <script type="text/javascript">
      (function () {
        var baseUrl =
          "https://bitdash-a.akamaihd.net/content/MI201109210084_1/video/720_2400000/dash/";
        var initUrl = baseUrl + "init.mp4";
        var templateUrl = baseUrl + "segment_$Number$.m4s";
        var sourceBuffer;
        var index = 0;
        var numberOfChunks = 52;
        var video = document.querySelector("video");

        if (!window.ManagedMediaSource) {
          console.error("No Media Source API available");
          return;
        }

        var ms = new ManagedMediaSource();
        video.src = window.URL.createObjectURL(ms);
        ms.addEventListener("sourceopen", onMediaSourceOpen);

        function onMediaSourceOpen() {
          sourceBuffer = ms.addSourceBuffer('video/mp4; codecs="avc1.4d401f"');
          sourceBuffer.addEventListener("updateend", nextSegment);

          GET(initUrl, appendToBuffer);

          video.play();
        }

        function nextSegment() {
          var url = templateUrl.replace("$Number$", index);
          GET(url, appendToBuffer);
          index++;
          if (index > numberOfChunks) {
            sourceBuffer.removeEventListener("updateend", nextSegment);
          }
        }

        function appendToBuffer(videoChunk) {
          if (videoChunk) {
            sourceBuffer.appendBuffer(new Uint8Array(videoChunk));
          }
        }

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

          xhr.onload = function (e) {
            if (xhr.status != 200) {
              console.warn(
                "Unexpected status code " + xhr.status + " for " + url,
              );
              return false;
            }
            callback(xhr.response);
          };

          xhr.send();
        }
      })();
    </script>
  </body>
</html>

I would appreciate any clarification on why this is not working or if I'm missing something what it is! 🙏


Solution

  • ManagedMediaSource has been shipping and enabled since Safari 17. I did a presentation at WWDC 2023 last year about it. For MMS relevant content check https://developer.apple.com/videos/play/wwdc2023/10122/ at about 10 minutes in. It's already used by major web sites (YouTube) and frameworks such as HLS.js

    The issue you are seeing is directly related to this point. https://developer.apple.com/videos/play/wwdc2023/10122/?time=1205

    "When designing Managed MSE, we wanted to make sure that nothing was left out by accident and that users continue to get the same level of features as they did in the past. So to activate Managed MSE on Mac, iPad, and iPhone, your player must provide an AirPlay source alternative. You can still have access to Managed MSE without it, but you must explicitly disable AirPlay by calling disableRemotePlayback on your media element from the Remote Playback API. And that's it. Managed MSE supports all the same great technology we added last year, such as SharePlay, spatialized audio, or HDR."

    So your MMS object will not move to readyState open. You haven't provided a remoteVideoPlayback alternative.

    There's two ways you can go about.

    1- Which is the recommended way and provide the best user experience for Apple's Safari users. You set multiple source alternative.

        // Set multiple child in source attibute
        const videoSource1 = document.createElement('source');
        videoSource1.type = 'video/mp4' ;
        videoSource1.src = URL.createObjectURL(ms);
        video.appendChild(videoSource1) ;
    
        // Set an AirPlay alternative as a 2nd source
        const videoSource2 = document.createElement('source');
        videoSource2. type = 'application/x-mpegURL' ;
        videoSource2.src = "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8";
        video.appendChild(videoSource2);
    
    1. Explicitly disable the Remote Playback API. Add to your code: video.disableRemotePlayback = true;

    Now, one more thing. To really benefit from MMS extra functionalities over plain MSE, you really need to listen to the startstreaming and end streaming event and only call sourceBuffer.appendBuffer during those two events. (you can use MMS just like MSE and append whenever you want, but it prevents the device to manage power efficiently. On iPhone with 5G in particular, if the pattern of appending outside the start/end streaming is detected, 5G will be disabled. So your site will use more power and the video will load more slowly than if you did.

    Check out the example in the spec. https://w3c.github.io/media-source/#using-a-managed-media-source

    For a simple working example: https://jyavenard.github.io/htmltests/tests/ManagedMediaSource/bipbop.html

    Hope this helps. Cheers Jean-Yves