@splinetool/viewer 2.0.9 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/hana-ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* @ts-self-types="./hana.d.ts" */
2
- import { loadImageFromBytes, loadImageFromUrl, loadVideoFromBytes, loadVideoFromUrl } from './snippets/hana-82e9eb82a79dead4/js-imports.js';
2
+ import { disposeVideo, loadImageFromBytes, loadImageFromUrl, loadVideoFromBytes, loadVideoFromUrl, videoFrameToken } from './snippets/hana-82e9eb82a79dead4/js-imports.js';
3
3
 
4
4
 
5
5
  export class DeviceLostPromise {
@@ -331,6 +331,16 @@ export class SceneController {
331
331
  const ret = wasm.scenecontroller_centerVectorData(this.__wbg_ptr);
332
332
  return takeObject(ret);
333
333
  }
334
+ /**
335
+ * Drop the export-time frame override for one video asset (see
336
+ * `setVideoExportFrame`): the renderer resumes sampling its `<video>`
337
+ * element and `seekVideos` drives it again. The export loop's per-video
338
+ * fallback for a WebCodecs source that stopped delivering mid-export.
339
+ * @param {ID} id
340
+ */
341
+ clearVideoExportFrame(id) {
342
+ wasm.scenecontroller_clearVideoExportFrame(this.__wbg_ptr, addHeapObject(id));
343
+ }
334
344
  /**
335
345
  * Drop every export-time video frame override (see `setVideoExportFrame`).
336
346
  */
@@ -1216,8 +1226,9 @@ export class SceneController {
1216
1226
  }
1217
1227
  /**
1218
1228
  * The video fills the export loop should decode with WebCodecs, as
1219
- * `{ id, src }` objects (`src` is the loaded `<video>` element's object
1220
- * URL). Enumerated from the start events' video actions the exact set
1229
+ * `{ id, src }` objects (`src` is the loaded `<video>` element's source:
1230
+ * the asset's own URL for streamed loads, a `blob:` URL for byte-loaded
1231
+ * ones — either way the same URL the element itself fetched). Enumerated from the start events' video actions — the exact set
1221
1232
  * `seekVideos` drives — so the WebCodecs path animates precisely the
1222
1233
  * fills the element path would: a fill without a play action stays a
1223
1234
  * static frame on both paths and both backends. Scoped to actions
@@ -1807,6 +1818,17 @@ function __wbg_get_imports() {
1807
1818
  __wbg_dispatchWorkgroups_afb2344298c62227: function(arg0, arg1, arg2, arg3) {
1808
1819
  getObject(arg0).dispatchWorkgroups(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0);
1809
1820
  },
1821
+ __wbg_displayHeight_465673c1823a1db3: function(arg0) {
1822
+ const ret = getObject(arg0).displayHeight;
1823
+ return ret;
1824
+ },
1825
+ __wbg_displayWidth_73729fd8eb965076: function(arg0) {
1826
+ const ret = getObject(arg0).displayWidth;
1827
+ return ret;
1828
+ },
1829
+ __wbg_disposeVideo_c94c1ccd57a63f68: function(arg0) {
1830
+ disposeVideo(getObject(arg0));
1831
+ },
1810
1832
  __wbg_document_aceb08cd6489baf5: function(arg0) {
1811
1833
  const ret = getObject(arg0).document;
1812
1834
  return isLikeNone(ret) ? 0 : addHeapObject(ret);
@@ -2453,7 +2475,7 @@ function __wbg_get_imports() {
2453
2475
  const a = state0.a;
2454
2476
  state0.a = 0;
2455
2477
  try {
2456
- return __wasm_bindgen_func_elem_14150(a, state0.b, arg0, arg1);
2478
+ return __wasm_bindgen_func_elem_14186(a, state0.b, arg0, arg1);
2457
2479
  } finally {
2458
2480
  state0.a = a;
2459
2481
  }
@@ -2499,7 +2521,7 @@ function __wbg_get_imports() {
2499
2521
  const a = state0.a;
2500
2522
  state0.a = 0;
2501
2523
  try {
2502
- return __wasm_bindgen_func_elem_14150(a, state0.b, arg0, arg1);
2524
+ return __wasm_bindgen_func_elem_14186(a, state0.b, arg0, arg1);
2503
2525
  } finally {
2504
2526
  state0.a = a;
2505
2527
  }
@@ -2626,6 +2648,10 @@ function __wbg_get_imports() {
2626
2648
  __wbg_readPixels_d00c1cae608273d8: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) {
2627
2649
  getObject(arg0).readPixels(arg1, arg2, arg3, arg4, arg5 >>> 0, arg6 >>> 0, arg7);
2628
2650
  }, arguments); },
2651
+ __wbg_readyState_77b1b1db1b8c2a5f: function(arg0) {
2652
+ const ret = getObject(arg0).readyState;
2653
+ return ret;
2654
+ },
2629
2655
  __wbg_reason_b02cd587d1a09948: function(arg0) {
2630
2656
  const ret = getObject(arg0).reason;
2631
2657
  return (__wbindgen_enum_GpuDeviceLostReason.indexOf(ret) + 1 || 3) - 1;
@@ -3581,6 +3607,10 @@ function __wbg_get_imports() {
3581
3607
  __wbg_vertexAttribPointer_3bc50c0431082f73: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
3582
3608
  getObject(arg0).vertexAttribPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4 !== 0, arg5, arg6);
3583
3609
  },
3610
+ __wbg_videoFrameToken_6f05524e85fa2b75: function(arg0) {
3611
+ const ret = videoFrameToken(getObject(arg0));
3612
+ return ret;
3613
+ },
3584
3614
  __wbg_videoHeight_e267fd1e74bb1b06: function(arg0) {
3585
3615
  const ret = getObject(arg0).videoHeight;
3586
3616
  return ret;
@@ -3606,48 +3636,48 @@ function __wbg_get_imports() {
3606
3636
  getObject(arg0).writeTexture(getObject(arg1), getArrayU8FromWasm0(arg2, arg3), getObject(arg4), getObject(arg5));
3607
3637
  }, arguments); },
3608
3638
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
3609
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1052, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3610
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_14148);
3639
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1054, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3640
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_14184);
3611
3641
  return addHeapObject(ret);
3612
3642
  },
3613
3643
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
3614
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 32, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3615
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_5101);
3644
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 34, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3645
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_5119);
3616
3646
  return addHeapObject(ret);
3617
3647
  },
3618
3648
  __wbindgen_cast_0000000000000003: function(arg0, arg1) {
3619
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUComputePipeline")], shim_idx: 927, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3620
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11107);
3649
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUComputePipeline")], shim_idx: 929, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3650
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11140);
3621
3651
  return addHeapObject(ret);
3622
3652
  },
3623
3653
  __wbindgen_cast_0000000000000004: function(arg0, arg1) {
3624
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUDevice")], shim_idx: 927, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3625
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11107_3);
3654
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUDevice")], shim_idx: 929, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3655
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11140_3);
3626
3656
  return addHeapObject(ret);
3627
3657
  },
3628
3658
  __wbindgen_cast_0000000000000005: function(arg0, arg1) {
3629
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUDeviceLostInfo")], shim_idx: 928, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3630
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11108);
3659
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUDeviceLostInfo")], shim_idx: 930, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3660
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11141);
3631
3661
  return addHeapObject(ret);
3632
3662
  },
3633
3663
  __wbindgen_cast_0000000000000006: function(arg0, arg1) {
3634
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPURenderPipeline")], shim_idx: 927, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3635
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11107_5);
3664
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPURenderPipeline")], shim_idx: 929, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3665
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11140_5);
3636
3666
  return addHeapObject(ret);
3637
3667
  },
3638
3668
  __wbindgen_cast_0000000000000007: function(arg0, arg1) {
3639
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("any")], shim_idx: 927, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3640
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11107_6);
3669
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("any")], shim_idx: 929, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3670
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11140_6);
3641
3671
  return addHeapObject(ret);
3642
3672
  },
3643
3673
  __wbindgen_cast_0000000000000008: function(arg0, arg1) {
3644
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 927, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3645
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11107_7);
3674
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 929, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
3675
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_11140_7);
3646
3676
  return addHeapObject(ret);
3647
3677
  },
3648
3678
  __wbindgen_cast_0000000000000009: function(arg0, arg1) {
3649
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1035, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3650
- const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_12514);
3679
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1037, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
3680
+ const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_12547);
3651
3681
  return addHeapObject(ret);
3652
3682
  },
3653
3683
  __wbindgen_cast_000000000000000a: function(arg0) {
@@ -3721,22 +3751,22 @@ function __wbg_get_imports() {
3721
3751
  };
3722
3752
  }
3723
3753
 
3724
- function __wasm_bindgen_func_elem_12514(arg0, arg1) {
3725
- wasm.__wasm_bindgen_func_elem_12514(arg0, arg1);
3754
+ function __wasm_bindgen_func_elem_12547(arg0, arg1) {
3755
+ wasm.__wasm_bindgen_func_elem_12547(arg0, arg1);
3726
3756
  }
3727
3757
 
3728
- function __wasm_bindgen_func_elem_5101(arg0, arg1, arg2) {
3729
- wasm.__wasm_bindgen_func_elem_5101(arg0, arg1, addHeapObject(arg2));
3758
+ function __wasm_bindgen_func_elem_5119(arg0, arg1, arg2) {
3759
+ wasm.__wasm_bindgen_func_elem_5119(arg0, arg1, addHeapObject(arg2));
3730
3760
  }
3731
3761
 
3732
- function __wasm_bindgen_func_elem_11108(arg0, arg1, arg2) {
3733
- wasm.__wasm_bindgen_func_elem_11108(arg0, arg1, addHeapObject(arg2));
3762
+ function __wasm_bindgen_func_elem_11141(arg0, arg1, arg2) {
3763
+ wasm.__wasm_bindgen_func_elem_11141(arg0, arg1, addHeapObject(arg2));
3734
3764
  }
3735
3765
 
3736
- function __wasm_bindgen_func_elem_14148(arg0, arg1, arg2) {
3766
+ function __wasm_bindgen_func_elem_14184(arg0, arg1, arg2) {
3737
3767
  try {
3738
3768
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3739
- wasm.__wasm_bindgen_func_elem_14148(retptr, arg0, arg1, addHeapObject(arg2));
3769
+ wasm.__wasm_bindgen_func_elem_14184(retptr, arg0, arg1, addHeapObject(arg2));
3740
3770
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3741
3771
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3742
3772
  if (r1) {
@@ -3747,10 +3777,10 @@ function __wasm_bindgen_func_elem_14148(arg0, arg1, arg2) {
3747
3777
  }
3748
3778
  }
3749
3779
 
3750
- function __wasm_bindgen_func_elem_11107(arg0, arg1, arg2) {
3780
+ function __wasm_bindgen_func_elem_11140(arg0, arg1, arg2) {
3751
3781
  try {
3752
3782
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3753
- wasm.__wasm_bindgen_func_elem_11107(retptr, arg0, arg1, addHeapObject(arg2));
3783
+ wasm.__wasm_bindgen_func_elem_11140(retptr, arg0, arg1, addHeapObject(arg2));
3754
3784
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3755
3785
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3756
3786
  if (r1) {
@@ -3761,10 +3791,10 @@ function __wasm_bindgen_func_elem_11107(arg0, arg1, arg2) {
3761
3791
  }
3762
3792
  }
3763
3793
 
3764
- function __wasm_bindgen_func_elem_11107_3(arg0, arg1, arg2) {
3794
+ function __wasm_bindgen_func_elem_11140_3(arg0, arg1, arg2) {
3765
3795
  try {
3766
3796
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3767
- wasm.__wasm_bindgen_func_elem_11107_3(retptr, arg0, arg1, addHeapObject(arg2));
3797
+ wasm.__wasm_bindgen_func_elem_11140_3(retptr, arg0, arg1, addHeapObject(arg2));
3768
3798
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3769
3799
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3770
3800
  if (r1) {
@@ -3775,10 +3805,10 @@ function __wasm_bindgen_func_elem_11107_3(arg0, arg1, arg2) {
3775
3805
  }
3776
3806
  }
3777
3807
 
3778
- function __wasm_bindgen_func_elem_11107_5(arg0, arg1, arg2) {
3808
+ function __wasm_bindgen_func_elem_11140_5(arg0, arg1, arg2) {
3779
3809
  try {
3780
3810
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3781
- wasm.__wasm_bindgen_func_elem_11107_5(retptr, arg0, arg1, addHeapObject(arg2));
3811
+ wasm.__wasm_bindgen_func_elem_11140_5(retptr, arg0, arg1, addHeapObject(arg2));
3782
3812
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3783
3813
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3784
3814
  if (r1) {
@@ -3789,10 +3819,10 @@ function __wasm_bindgen_func_elem_11107_5(arg0, arg1, arg2) {
3789
3819
  }
3790
3820
  }
3791
3821
 
3792
- function __wasm_bindgen_func_elem_11107_6(arg0, arg1, arg2) {
3822
+ function __wasm_bindgen_func_elem_11140_6(arg0, arg1, arg2) {
3793
3823
  try {
3794
3824
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3795
- wasm.__wasm_bindgen_func_elem_11107_6(retptr, arg0, arg1, addHeapObject(arg2));
3825
+ wasm.__wasm_bindgen_func_elem_11140_6(retptr, arg0, arg1, addHeapObject(arg2));
3796
3826
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3797
3827
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3798
3828
  if (r1) {
@@ -3803,10 +3833,10 @@ function __wasm_bindgen_func_elem_11107_6(arg0, arg1, arg2) {
3803
3833
  }
3804
3834
  }
3805
3835
 
3806
- function __wasm_bindgen_func_elem_11107_7(arg0, arg1, arg2) {
3836
+ function __wasm_bindgen_func_elem_11140_7(arg0, arg1, arg2) {
3807
3837
  try {
3808
3838
  const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
3809
- wasm.__wasm_bindgen_func_elem_11107_7(retptr, arg0, arg1, addHeapObject(arg2));
3839
+ wasm.__wasm_bindgen_func_elem_11140_7(retptr, arg0, arg1, addHeapObject(arg2));
3810
3840
  var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
3811
3841
  var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
3812
3842
  if (r1) {
@@ -3817,8 +3847,8 @@ function __wasm_bindgen_func_elem_11107_7(arg0, arg1, arg2) {
3817
3847
  }
3818
3848
  }
3819
3849
 
3820
- function __wasm_bindgen_func_elem_14150(arg0, arg1, arg2, arg3) {
3821
- wasm.__wasm_bindgen_func_elem_14150(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
3850
+ function __wasm_bindgen_func_elem_14186(arg0, arg1, arg2, arg3) {
3851
+ wasm.__wasm_bindgen_func_elem_14186(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
3822
3852
  }
3823
3853
 
3824
3854
 
Binary file
@@ -1,93 +1,220 @@
1
-
2
1
  export async function loadImageFromBytes(bytes) {
3
- const blob = new Blob([bytes])
4
- const url = URL.createObjectURL(blob)
5
- return createImageBitmap(blob).finally(() => {
6
- URL.revokeObjectURL(url)
7
- })
2
+ return createImageBitmap(new Blob([bytes]))
8
3
  }
9
4
 
10
5
  export async function loadImageFromUrl(url) {
11
- return fetch(url)
12
- .then(response => {
13
- if (!response.ok) {
14
- throw new Error(`Failed to fetch image: ${response.statusText}`);
15
- }
16
- return response.blob();
17
- })
18
- .then(blob => createImageBitmap(blob));
6
+ return fetch(url)
7
+ .then((response) => {
8
+ if (!response.ok) {
9
+ throw new Error(`Failed to fetch image: ${response.statusText}`)
10
+ }
11
+ return response.blob()
12
+ })
13
+ .then((blob) => createImageBitmap(blob))
19
14
  }
20
15
 
21
16
  // Video Imports
22
- function setupVideo(video) {
23
- video.preload = "auto";
24
- video.autoplay = true; //iOs hack to show the first frame
25
- video.loop = true;
26
- video.muted = true;
27
- video.playsInline = true;
28
- video.currentTime = 0.01;
29
- video.load();
30
-
31
- video.onloadeddata = () => {
32
- video.muted = true;
33
- // DOMException: The play() request was interrupted by a new load request.
34
- // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
35
- const playPromise = video.play();
36
-
37
- if (playPromise !== undefined) {
38
- playPromise
39
- .then((_) => {
40
- // Automatic playback started! Do nothing all good!
41
- })
42
- .catch(() => {
43
- // for some reason the video could not be played (The play() request was interrupted by a new load request.)
44
- // so let's try to play it again
45
- video.play();
46
- }).finally(() => {
47
- video.pause();
48
- });
49
- }
50
- };
51
-
52
- let copyVideo = false;
53
- video.addEventListener("playing", function() {
54
- copyVideo = true;
55
- }, true);
56
-
57
- return new Promise((resolve) => {
58
- const checkVideo = () => {
59
- if (copyVideo) {
60
- resolve(video);
61
- } else {
62
- setTimeout(checkVideo, 10);
63
- }
64
- };
65
- checkVideo();
66
- });
67
- }
68
17
 
18
+ let videoFrameSeq = 0
69
19
 
70
- export async function loadVideoFromBytes(bytes) {
71
- const blob = new Blob([bytes])
72
- const video = document.createElement('video');
73
- video.src = URL.createObjectURL(blob);
74
- return setupVideo(video);
20
+ // Fallback tokens (see `videoFrameToken`) are the media clock offset per element
21
+ // by this stride, so two elements' ranges stay disjoint for any media under ~11.5 days.
22
+ const MEDIA_CLOCK_STRIDE = 1e6
23
+
24
+ // `HTMLMediaElement.HAVE_CURRENT_DATA` — a decoded frame the renderer can sample.
25
+ const HAVE_CURRENT_DATA = 2
26
+
27
+ // Generous, because a slow decode is not a failure — but bounded, because a source
28
+ // the browser silently declines must not wedge the load forever.
29
+ const VIDEO_READY_TIMEOUT_MS = 30000
30
+
31
+ // Maintains a token that changes exactly when the element presents a new frame,
32
+ // so the renderer can tell whether a GPU copy is needed at all.
33
+ //
34
+ // The callback chain re-arms itself and keeps `video` reachable until
35
+ // `disposeVideo` cancels it. It only fires on a presented frame, so a paused or
36
+ // stalled element costs nothing.
37
+ function trackPresentedFrames(video) {
38
+ if (typeof video.requestVideoFrameCallback !== 'function') {
39
+ // No presented-frame signal: `videoFrameToken` falls back to the media clock,
40
+ // offset per element to keep the guarantee below.
41
+ video.hanaClockBase = ++videoFrameSeq * MEDIA_CLOCK_STRIDE
42
+ return
43
+ }
44
+
45
+ // One global sequence, so no two elements ever report the same token: when an
46
+ // asset is replaced, the token the renderer still holds for the displaced
47
+ // element cannot collide with the replacement's.
48
+ video.hanaPresentedFrames = ++videoFrameSeq
49
+ const bump = () => {
50
+ // Disposal detaches the source, which fires `seeked` on the way out.
51
+ if (video.hanaDisposed) return
52
+ video.hanaPresentedFrames = ++videoFrameSeq
53
+ }
54
+
55
+ // A seek repaints a paused element without necessarily running the frame
56
+ // callback, and the video export seeks frame by frame with the element paused —
57
+ // it must never be handed the texture from the previous seek.
58
+ video.addEventListener('seeked', bump)
59
+
60
+ const onFrame = () => {
61
+ bump()
62
+ // Never re-arm a disposed element: that resurrects the cancelled chain.
63
+ if (video.hanaDisposed) return
64
+ video.hanaFrameCallback = video.requestVideoFrameCallback(onFrame)
65
+ }
66
+ video.hanaFrameCallback = video.requestVideoFrameCallback(onFrame)
75
67
  }
76
68
 
69
+ // Read by the renderer once per frame per video. Where
70
+ // `requestVideoFrameCallback` is missing, falls back to the media clock: browsers
71
+ // advance `currentTime` at frame granularity, so it answers the same question. The
72
+ // per-element base keeps the cross-element guarantee across a reload, where
73
+ // `Assets::insert_video` replaces the element under the same id.
74
+ export function videoFrameToken(video) {
75
+ const presented = video.hanaPresentedFrames
76
+ return presented === undefined ? video.hanaClockBase + video.currentTime : presented
77
+ }
77
78
 
78
- export async function loadVideoFromUrl(url) {
79
- return fetch(url)
80
- .then(response => {
81
- if (!response.ok) {
82
- throw new Error(`Failed to fetch video: ${response.statusText}`);
83
- }
84
- return response.blob();
79
+ // Only for `Local(bytes)`, which has no URL to stream from. A blob: URL pins its
80
+ // Blob — the whole file — until revoked, so keep it on the element for
81
+ // `disposeVideo` to release.
82
+ function setVideoSourceFromBlob(video, blob) {
83
+ video.hanaObjectUrl = URL.createObjectURL(blob)
84
+ video.src = video.hanaObjectUrl
85
+ }
86
+
87
+ // Releases everything the element holds: the frame-callback chain, the blob: URL
88
+ // mapping, and the buffered media and decoder behind it. Runs from `OwnedVideo`'s
89
+ // `Drop` in `assets.rs`. Idempotent, and each step is independently conditional —
90
+ // an element loaded without a blob: URL still gets the rest of the teardown.
91
+ export function disposeVideo(video) {
92
+ if (video.hanaDisposed) return
93
+ // Set first: the steps below fire events that must not read as a live element.
94
+ video.hanaDisposed = true
95
+
96
+ const callback = video.hanaFrameCallback
97
+ if (callback !== undefined) {
98
+ video.hanaFrameCallback = undefined
99
+ // A throw here must not skip the rest of the teardown.
100
+ if (typeof video.cancelVideoFrameCallback === 'function') {
101
+ video.cancelVideoFrameCallback(callback)
102
+ }
103
+ }
104
+
105
+ const url = video.hanaObjectUrl
106
+ if (url !== undefined) {
107
+ video.hanaObjectUrl = undefined
108
+ URL.revokeObjectURL(url)
109
+ }
110
+
111
+ video.pause()
112
+ // Revoking only drops the URL -> Blob mapping. Detaching the source is what
113
+ // actually releases the media the element has buffered and its decoder.
114
+ video.removeAttribute('src')
115
+ video.load()
116
+ }
117
+
118
+ // Resolves once the element holds a frame the renderer can sample, rejects if it
119
+ // never will. Every branch settles: an unsettled load holds its `PendingLoadGuard`,
120
+ // and headless hosts poll `Assets::pending_loads` to know a document is ready.
121
+ //
122
+ // Readiness is `loadeddata`, not `playing`: the video export drives the element
123
+ // paused, so `playing` never fires there, and a blocked autoplay is not a failure.
124
+ function whenSampleable(video) {
125
+ const mediaError = (prefix) => {
126
+ const err = video.error
127
+ return new Error(err === null ? prefix : `${prefix} (code ${err.code}: ${err.message})`)
128
+ }
129
+ // A rejected load never becomes an `OwnedVideo`, so nothing else releases the
130
+ // element: it would pin its blob: URL for the page lifetime and, if it buffers
131
+ // after a timeout, decode invisibly on loop (`autoplay` and `loop` stay set). The
132
+ // error is built first — `disposeVideo`'s `load()` clears `video.error`.
133
+ const failure = (prefix) => {
134
+ const err = mediaError(prefix)
135
+ disposeVideo(video)
136
+ return err
137
+ }
138
+
139
+ if (video.error !== null) return Promise.reject(failure('video failed to load'))
140
+ if (video.readyState >= HAVE_CURRENT_DATA) return Promise.resolve(video)
141
+
142
+ return new Promise((resolve, reject) => {
143
+ // Armed first so `settle` can clear it; nothing below runs this tick.
144
+ const timer = setTimeout(
145
+ () => settle(reject, failure('video timed out before presenting a frame')),
146
+ VIDEO_READY_TIMEOUT_MS,
147
+ )
148
+ const settle = (finish, value) => {
149
+ clearTimeout(timer)
150
+ video.removeEventListener('loadeddata', onReady)
151
+ video.removeEventListener('canplay', onReady)
152
+ video.removeEventListener('error', onError)
153
+ finish(value)
154
+ }
155
+ const onReady = () => settle(resolve, video)
156
+ const onError = () => settle(reject, failure('video failed to load'))
157
+
158
+ video.addEventListener('loadeddata', onReady)
159
+ // A partly-loaded source can become sampleable without a fresh `loadeddata`.
160
+ video.addEventListener('canplay', onReady)
161
+ video.addEventListener('error', onError)
162
+ })
163
+ }
164
+
165
+ function setupVideo(video) {
166
+ video.preload = 'auto'
167
+ video.autoplay = true //iOs hack to show the first frame
168
+ video.loop = true
169
+ video.muted = true
170
+ video.playsInline = true
171
+ video.currentTime = 0.01
172
+ video.load()
173
+
174
+ video.onloadeddata = () => {
175
+ video.muted = true
176
+ // DOMException: The play() request was interrupted by a new load request.
177
+ // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
178
+ const playPromise = video.play()
179
+
180
+ if (playPromise !== undefined) {
181
+ playPromise
182
+ .then((_) => {
183
+ // Automatic playback started! Do nothing all good!
184
+ })
185
+ .catch(() => {
186
+ // for some reason the video could not be played (The play() request was interrupted by a new load request.)
187
+ // so let's try to play it again
188
+ video.play()
189
+ })
190
+ .finally(() => {
191
+ video.pause()
85
192
  })
86
- .then(async blob => {
87
- const video = document.createElement('video');
88
- video.src = URL.createObjectURL(blob);
89
- return setupVideo(video);
90
- });
193
+ }
194
+ }
195
+
196
+ trackPresentedFrames(video)
197
+
198
+ return whenSampleable(video)
91
199
  }
92
200
 
201
+ export async function loadVideoFromBytes(bytes) {
202
+ const video = document.createElement('video')
203
+ setVideoSourceFromBlob(video, new Blob([bytes]))
204
+ return setupVideo(video)
205
+ }
93
206
 
207
+ // Streams from `url` instead of fetching it into a Blob first, so the browser can
208
+ // range-request it, playback starts before the last byte lands, and the file never
209
+ // sits in memory whole.
210
+ //
211
+ // `crossOrigin` must be set before `src`: without it a cross-origin element is
212
+ // tainted and every texture upload throws a SecurityError. It costs no reach — the
213
+ // `fetch` this replaces was already a CORS request, so any URL that used to load
214
+ // still does.
215
+ export async function loadVideoFromUrl(url) {
216
+ const video = document.createElement('video')
217
+ video.crossOrigin = 'anonymous'
218
+ video.src = url
219
+ return setupVideo(video)
220
+ }