Search code examples
firefoxcontent-security-policyweb-workermedia-source

Firefox Security Error: Content at http://localhost:NNNN/ may not load data from blob:http://localhost:NNNN/... when assigning blob url of MediaSource


I currently have a setup to emulate Web Workers support when some functionalities are not supported on different browsers in order to use the same messaging system across all browsers. In this case Firefox does not support Media Source Extension in a Web Worker. When in "no-worker" mode I create a MessageChannel and exchange messages between the "worker" thread and the main thread using the MessageChannel ports. This is how my code looks like overall:

// on port1 of the MessageChannel (representing the worker side)
postMessageToMain("createVideo")
const mediaSource = new MediaSource()
if (workerSupported){
  const handle = mediaSource.handle
  postMessageToMain("setSource", handle, [handle])
} else {
  const handle = URL.createObjectURL(mediaSource)
  postMessageToMain("setSource", handle)
}

// on port2 of the MessageChannel (representing the main thread)
onmessage(m){
  switch (m) {
    case "createVideo":{
        videoElement = document.createElement('video')
        videoElement.playsInline = true
        break
    }
    case "setSource":{
        if(workerSupported){
         videoElement.srcObject = m.data.handle // it is of type MediaSourceHandle
        } else {
         videoElement.src = m.data.handle // is of type string, here the error in the title is thrown
        }
    }
  }
}

This code works on Chrome in worker and non-worker mode while on safari, given that MSE in Web Worker is not supported, works only in non-worker mode. I'm NOT setting any CSP header. Am i doing something wrong? I found the same issue in SO but i struggle to find the actual problem and its solution

Minimal reproducible example: https://stackblitz.com/edit/vitejs-vite-hxbktr?file=main.js

(open on firefox and check the console)


Solution

  • Very weird bug where it seems they don't hold the link between the MediaSource and the blob: URL when this latter isn't consumed in the same task it was created. Here is an actual minimal example:

    const video = document.querySelector('video');
    const mediaSource = new MediaSource();
    const url = URL.createObjectURL(mediaSource);
    setTimeout(() => {
      video.src = url;
    });
    <video controls></video>

    Setting the src synchronously would avoid the bug

    const video = document.querySelector('video');
    const mediaSource = new MediaSource();
    const url = URL.createObjectURL(mediaSource);
    video.src = url;
    <video controls></video>

    and so would creating the blob: URL from the async callback:

    const video = document.querySelector('video');
    const mediaSource = new MediaSource();
    setTimeout(() => {
      video.src = URL.createObjectURL(mediaSource);
    });
    <video controls></video>

    So for your case you may want to mock the Worker context with synchronous operations instead:

    // assuming you don't use addEventListener()
    const fakeMain = { postMessage(data) { fakeWorker.onmessage?.({ data }); } };
    const fakeWorker = { postMessage(data) { fakeMain.onmessage?.({ data }); } };
    

    /*
    Copyright 2017 Google Inc.
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    
    'use strict';
    
    // This code adapted from Eric Bidelman's demo at
    // http://html5-demos.appspot.com/static/media-source.html
    
    var FILE = 'https://upload.wikimedia.org/wikipedia/commons/a/a4/BBH_gravitational_lensing_of_gw150914.webm';
    var NUM_CHUNKS = 5;
    var video = document.querySelector('video');
    
    if (!window.MediaSource) {
      alert('The MediaSource API is not available on this platform');
    }
    
    const fakeMain = { postMessage(data) { fakeWorker.onmessage?.({ data }); } };
    const fakeWorker = { postMessage(data) { fakeMain.onmessage?.({ data }); } };
    
    fakeWorker.onmessage = (message) => {
      console.log('received message', message);
      switch (message.data.type) {
        case 'init': {
          console.log('received message in worker');
          const mediaSource = new MediaSource();
          mediaSource.addEventListener(
            'sourceopen',
            function () {
              var sourceBuffer = mediaSource.addSourceBuffer(
                'video/webm; codecs="vorbis,vp8"'
              );
              console.log(sourceBuffer);
    
              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);
    
                log('Number of chunks: ' + NUM_CHUNKS);
                log('Chunk size: ' + chunkSize + ', total size: ' + file.size);
    
                // Slice the video into NUM_CHUNKS and append each to the media element.
                var i = 0;
    
                (function readChunk_(i) {
                  // eslint-disable-line no-shadow
                  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) {
                    sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
                    log('Appending chunk: ' + i);
                    if (i === NUM_CHUNKS - 1) {
                      sourceBuffer.addEventListener('updateend', function () {
                        if (
                          !sourceBuffer.updating &&
                          mediaSource.readyState === 'open'
                        ) {
                          mediaSource.endOfStream();
                        }
                      });
                    } else {
                      if (video.paused) {
                        video.play(); // Start playing after 1st chunk is appended.
                      }
                      readChunk_(++i);
                    }
                  };
    
                  var startByte = chunkSize * i;
                  var chunk = file.slice(startByte, startByte + chunkSize);
    
                  reader.readAsArrayBuffer(chunk);
                })(i); // Start the recursive call by self calling.
              });
            },
            false
          );
    
          mediaSource.addEventListener(
            'sourceended',
            function () {
              log('MediaSource readyState: ' + this.readyState);
            },
            false
          );
          const url = window.URL.createObjectURL(mediaSource);
          fakeWorker.postMessage({ type: 'setSrc', url });
        }
      }
    };
    
    fakeMain.onmessage = (message) => {
      switch (message.data.type) {
        case 'setSrc': {
          console.log('received message in main');
    
          video.src = message.data.url;
        }
      }
    };
    
    document.querySelector('[data-num-chunks]').textContent = NUM_CHUNKS;
    fakeMain.postMessage({ type: 'init' });
    function get(url, callback) {
      var xhr = new XMLHttpRequest();
      xhr.open('GET', url, true);
      xhr.responseType = 'arraybuffer';
      xhr.send();
    
      xhr.onload = function () {
        if (xhr.status !== 200) {
          alert('Unexpected status code ' + xhr.status + ' for ' + url);
          return false;
        }
        callback(new Uint8Array(xhr.response));
      };
    }
    
    function log(message) {
      document.getElementById('data').innerHTML += message + '<br /><br />';
    }
    <video controls></video>
    <div data-num-chunks></div>
    <div id="data"></div>

    I see you did open https://bugzil.la/1906040 Let's hope this gets some traction and gets fixed soon enough.