<!DOCTYPE html>
<html>
<title>Test MediaSourceHandle transfer characteristics</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="mediasource-message-util.js"></script>
<body>
<script>

function assert_mseiw_supported() {
  // Fail fast if MSE-in-Workers is not supported.
  assert_true(
      MediaSource.hasOwnProperty('canConstructInDedicatedWorker'),
      'MediaSource hasOwnProperty \'canConstructInDedicatedWorker\'');
  assert_true(
      MediaSource.canConstructInDedicatedWorker,
      'MediaSource.canConstructInDedicatedWorker');
  assert_true(
      window.hasOwnProperty('MediaSourceHandle'),
      'window must have MediaSourceHandle visibility');
}

function get_handle_from_new_worker(
    t, script = 'mediasource-worker-handle-transfer-to-main.js') {
  return new Promise((r) => {
    let worker = new Worker(script);
    worker.addEventListener('message', t.step_func(e => {
      let subject = e.data.subject;
      assert_true(subject != undefined, 'message must have a subject field');
      switch (subject) {
        case messageSubject.ERROR:
          assert_unreached('Worker error: ' + e.data.info);
          break;
        case messageSubject.HANDLE:
          const handle = e.data.info;
          assert_not_equals(
              handle, null, 'must have a non-null MediaSourceHandle');
          r({worker, handle});
          break;
        default:
          assert_unreached('Unexpected message subject: ' + subject);
      }
    }));
  });
}

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle);
  }, 'serializing handle without transfer');
}, 'MediaSourceHandle serialization without transfer must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle, handle]);
  }, 'transferring same handle more than once in same postMessage');
}, 'Same MediaSourceHandle transferred multiple times in single postMessage must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  // Transferring handle to worker without including it in the message is still
  // a valid transfer, though the recipient will not be able to obtain the
  // handle itself. Regardless, the handle in this sender's context will be
  // detached.
  worker.postMessage(null, [handle]);

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(null, [handle]);
  }, 'transferring handle that was already detached should fail');

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was already detached should fail, even if this time it\'s included in the message');
}, 'Attempt to transfer detached MediaSourceHandle must fail, tested in window context');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);
  video.srcObject = handle;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was briefly srcObject before srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'MediaSourceHandle cannot be transferred, immediately after set as srcObject, even if srcObject immediately reset to null');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);
  video.srcObject = handle;
  assert_not_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING);
  // Initial step of resource selection algorithm sets networkState to
  // NETWORK_NO_SOURCE. networkState only becomes NETWORK_LOADING after stable
  // state awaited and resource selection algorithm continues with, in this
  // case, an assigned media provider object (which is the MediaSource
  // underlying the handle).
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

  // Wait until 'loadstart' media element event is dispatched.
  await new Promise((r) => {
    video.addEventListener(
        'loadstart', t.step_func(e => {
          r();
        }),
        {once: true});
  });
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_LOADING);

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject, after loadstart, fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was srcObject until \'loadstart\' when srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'MediaSourceHandle cannot be transferred, if it was srcObject when asynchronous load starts (loadstart), even if srcObject is then immediately reset to null');

promise_test(async t => {
  assert_mseiw_supported();
  let {worker, handle} = await get_handle_from_new_worker(t);
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let video = document.createElement('video');
  document.body.appendChild(video);

  // Transfer the handle away so that our instance of it is detached.
  worker.postMessage(null, [handle]);

  // Now assign handle to srcObject to attempt load. 'loadstart' event should
  // occur, but then media element error should occur due to failure to attach
  // to the underlying MediaSource of a detached MediaSourceHandle.

  video.srcObject = handle;
  assert_equals(
      video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE,
      'before async load start, networkState should be NETWORK_NO_SOURCE');

  // Before 'loadstart' dispatch, we don't expect the media element error.
  video.onerror = t.unreached_func(
      'Error is unexpected before \'loadstart\' event dispatch');

  // Wait until 'loadstart' media element event is dispatched.
  await new Promise((r) => {
    video.addEventListener(
        'loadstart', t.step_func(e => {
          r();
        }),
        {once: true});
  });

  // Now wait until 'error' media element event is dispatched.
  video.onerror = null;
  await new Promise((r) => {
    video.addEventListener(
        'error', t.step_func(e => {
          r();
        }),
        {once: true});
  });

  // Confirm expected error and states resulting from the "dedicated media
  // source failure steps":
  // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps
  let e = video.error;
  assert_true(e instanceof MediaError);
  assert_equals(e.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
  assert_equals(
      video.readyState, HTMLMediaElement.HAVE_NOTHING,
      'load failure should occur long before parsing any appended metadata.');
  assert_equals(video.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

  // Even if the handle is detached and attempt to load it failed, the handle is
  // still detached, and as well, has also been used as srcObject now. Re-verify
  // that such a handle instance must fail transfer attempt.
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring detached handle that is currently srcObject, after loadstart and load failure, fails');
  assert_equals(video.srcObject, handle);

  // Clear |handle| from being the srcObject value.
  video.srcObject = null;

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring detached handle that was srcObject until \'loadstart\' and load failure when srcObject was reset to null should also fail');
  assert_equals(video.srcObject, null);
}, 'A detached (already transferred away) MediaSourceHandle cannot successfully load when assigned to srcObject');

promise_test(async t => {
  assert_mseiw_supported();
  // Get a handle from a worker that is prepared to buffer real media once its
  // MediaSource instance attaches and 'sourceopen' is dispatched. Unlike
  // earlier cases in this file, we need positive indication from precisely one
  // of multiple media elements that the attachment and playback succeeded.
  let {worker, handle} =
      await get_handle_from_new_worker(t, 'mediasource-worker-play.js');
  assert_true(
      handle instanceof MediaSourceHandle, 'must be a MediaSourceHandle');

  let videos = [];
  const NUM_ELEMENTS = 5;
  for (let i = 0; i < NUM_ELEMENTS; ++i) {
    let v = document.createElement('video');
    videos.push(v);
    document.body.appendChild(v);
  }

  await new Promise((r) => {
    let errors = 0;
    let endeds = 0;

    // Setup handlers to expect precisely 1 ended and N-1 errors.
    videos.forEach((v) => {
      v.addEventListener(
          'error', t.step_func(e => {
            // Confirm expected error and states resulting from the "dedicated
            // media source failure steps":
            // https://html.spec.whatwg.org/multipage/media.html#dedicated-media-source-failure-steps
            let err = v.error;
            assert_true(err instanceof MediaError);
            assert_equals(err.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
            assert_equals(
                v.readyState, HTMLMediaElement.HAVE_NOTHING,
                'load failure should occur long before parsing any appended metadata.');
            assert_equals(v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE);

            errors++;
            if (errors + endeds == videos.length && endeds == 1)
              r();
          }),
          {once: true});
      v.addEventListener(
          'ended', t.step_func(e => {
            endeds++;
            if (errors + endeds == videos.length && endeds == 1)
              r();
          }),
          {once: true});
      v.srcObject = handle;
      assert_equals(
          v.networkState, HTMLMediaElement.NETWORK_NO_SOURCE,
          'before async load start, networkState should be NETWORK_NO_SOURCE');
    });

    let playPromises = [];
    videos.forEach((v) => {
      playPromises.push(v.play());
    });

    // Ignore playPromise success/rejection, if any.
    playPromises.forEach((p) => {
      if (p !== undefined) {
        p.then(_ => {}).catch(_ => {});
      }
    });
  });

  // Once the handle has been assigned as srcObject, it must fail transfer
  // steps.
  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that is currently srcObject on multiple elements, fails');
  videos.forEach((v) => {
    assert_equals(v.srcObject, handle);
    v.srcObject = null;
  });

  assert_throws_dom('DataCloneError', function() {
    worker.postMessage(handle, [handle]);
  }, 'transferring handle that was srcObject on multiple elements, then was unset on them, should also fail');
  videos.forEach((v) => {
    assert_equals(v.srcObject, null);
  });
}, 'Precisely one load of the same MediaSourceHandle assigned synchronously to multiple media element srcObjects succeeds');

fetch_tests_from_worker(new Worker('mediasource-worker-handle-transfer.js'));

</script>
</body>
</html>
