@flighthq/video 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e

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/README.md CHANGED
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/video`. The `@flight
13
13
  This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
14
14
 
15
15
  - [Flight project](https://github.com/flighthq/flight)
16
- - [Source for @flighthq/video@0.5.0-next.1944.cd6b6e7](https://github.com/flighthq/flight/tree/cd6b6e71eb472b3a4249830a7743333f019fa7b7/packages/video)
17
- - [License](https://github.com/flighthq/flight/blob/cd6b6e71eb472b3a4249830a7743333f019fa7b7/LICENSE.md)
16
+ - [Source for @flighthq/video@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/video)
17
+ - [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { canPlayVideoType, createVideoResource, createVideoResourceFromMediaStream, detectVideoMimeType, disposeVideoResource, getVideoResourceDuration, getVideoResourceHeight, getVideoResourceWidth, hasVideoResourceElement, inferVideoMimeType, isVideoResourceEmpty, isVideoResourceReady, loadVideoResourceFromBlob, loadVideoResourceFromUrl, loadVideoResourceFromUrls, selectVideoResourceUrl, } from './contract';
1
+ export { canPlayVideoType, createVideoResource, createVideoResourceFromMediaStream, destroyVideoResource, detectVideoMimeType, disposeVideoResource, explainVideoCapabilityBackend, explainVideoCapabilityOperation, getVideoResourceDuration, getVideoResourceHeight, getVideoResourceWidth, hasVideoCapabilityOperation, hasVideoResourceElement, inferVideoMimeType, isVideoResourceEmpty, isVideoResourceReady, loadVideoResourceFromBlob, loadVideoResourceFromUrl, loadVideoResourceFromUrls, selectVideoResourceUrl, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,kCAAkC,EAClC,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,kCAAkC,EAClC,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,6BAA6B,EAC7B,+BAA+B,EAC/B,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { canPlayVideoType, createVideoResource, createVideoResourceFromMediaStream, detectVideoMimeType, disposeVideoResource, getVideoResourceDuration, getVideoResourceHeight, getVideoResourceWidth, hasVideoResourceElement, inferVideoMimeType, isVideoResourceEmpty, isVideoResourceReady, loadVideoResourceFromBlob, loadVideoResourceFromUrl, loadVideoResourceFromUrls, selectVideoResourceUrl, } from './contract';
1
+ export { canPlayVideoType, createVideoResource, createVideoResourceFromMediaStream, destroyVideoResource, detectVideoMimeType, disposeVideoResource, explainVideoCapabilityBackend, explainVideoCapabilityOperation, getVideoResourceDuration, getVideoResourceHeight, getVideoResourceWidth, hasVideoCapabilityOperation, hasVideoResourceElement, inferVideoMimeType, isVideoResourceEmpty, isVideoResourceReady, loadVideoResourceFromBlob, loadVideoResourceFromUrl, loadVideoResourceFromUrls, selectVideoResourceUrl, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,kCAAkC,EAClC,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,kCAAkC,EAClC,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,6BAA6B,EAC7B,+BAA+B,EAC/B,wBAAwB,EACxB,sBAAsB,EACtB,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,EACvB,kBAAkB,EAClB,oBAAoB,EACpB,oBAAoB,EACpB,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
@@ -1,6 +1,15 @@
1
- import type { VideoResourceUrl } from '@flighthq/types/contract';
1
+ import type { BackendExplanation, BackendOperationExplanation, VideoCapabilityBackend, VideoCapabilityOperation, VideoResourceUrl } from '@flighthq/types/contract';
2
2
  export declare function canPlayVideoType(mimeType: string): boolean;
3
3
  export declare function detectVideoMimeType(data: ArrayBuffer | Uint8Array): string | null;
4
+ export declare function explainVideoCapabilityBackend(): BackendExplanation;
5
+ export declare function explainVideoCapabilityOperation(operation: VideoCapabilityOperation): BackendOperationExplanation;
6
+ export declare function getVideoCapabilityBackend(): VideoCapabilityBackend;
7
+ export declare function hasVideoCapabilityHostBackend(): boolean;
8
+ export declare function hasVideoCapabilityOperation(operation: VideoCapabilityOperation): boolean;
4
9
  export declare function inferVideoMimeType(url: string): string | null;
10
+ export declare function installVideoCapabilityHostBackend(backend: VideoCapabilityBackend): void;
11
+ export declare function observeVideoCapabilityHostResult(operation: VideoCapabilityOperation, succeeded: boolean): void;
12
+ export declare function resetVideoCapabilityBackendForTest(): void;
5
13
  export declare function selectVideoResourceUrl(sources: Readonly<VideoResourceUrl[]>): VideoResourceUrl | null;
14
+ export declare function setVideoCapabilityBackend(backend: VideoCapabilityBackend | null): void;
6
15
  //# sourceMappingURL=videoFormat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"videoFormat.d.ts","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAKjE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG1D;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAcjF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwB7D;AAKD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAKrG"}
1
+ {"version":3,"file":"videoFormat.d.ts","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG1D;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAcjF;AAED,wBAAgB,6BAA6B,IAAI,kBAAkB,CAalE;AAED,wBAAgB,+BAA+B,CAAC,SAAS,EAAE,wBAAwB,GAAG,2BAA2B,CAgBhH;AAED,wBAAgB,yBAAyB,IAAI,sBAAsB,CAElE;AAED,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,wBAAwB,GAAG,OAAO,CAExF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwB7D;AAED,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAMvF;AAED,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,wBAAwB,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAK9G;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAKzD;AAKD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,GAAG,gBAAgB,GAAG,IAAI,CASrG;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAEtF"}
@@ -1,9 +1,7 @@
1
- // True when the current environment claims it can play the given MIME type (a non-empty canPlayType
2
- // result — 'maybe' or 'probably'). Returns false in headless environments (jsdom) where no codecs
3
- // are registered. This is the codec-probe primitive that source selection is built on.
4
1
  export function canPlayVideoType(mimeType) {
5
- const probe = document.createElement('video');
6
- return probe.canPlayType(mimeType) !== '';
2
+ if (mimeType === '')
3
+ return false;
4
+ return probeSelectedBackend(getVideoCapabilityBackend(), mimeType);
7
5
  }
8
6
  // Sniffs a container MIME type from the leading bytes of encoded video, or null when unrecognised.
9
7
  // Mirrors detectImageMimeType. mkv and webm share the Matroska/EBML signature (the DocType that
@@ -23,6 +21,46 @@ export function detectVideoMimeType(data) {
23
21
  return 'video/ogg';
24
22
  return null;
25
23
  }
24
+ export function explainVideoCapabilityBackend() {
25
+ if (_custom !== null) {
26
+ return { conflict: _hostConflict, layer: 'custom', operation: null, viability: 'unobserved' };
27
+ }
28
+ if (_host !== null) {
29
+ return {
30
+ conflict: _hostConflict,
31
+ layer: 'host',
32
+ operation: _hostObservation !== null ? _hostObservation.operation : null,
33
+ viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
34
+ };
35
+ }
36
+ return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
37
+ }
38
+ export function explainVideoCapabilityOperation(operation) {
39
+ if (_custom !== null) {
40
+ return typeof _custom[operation] === 'function'
41
+ ? { implemented: true, layer: 'custom', operation }
42
+ : { implemented: false, layer: 'none', operation };
43
+ }
44
+ if (_host !== null) {
45
+ return typeof _host[operation] === 'function'
46
+ ? { implemented: true, layer: 'host', operation }
47
+ : { implemented: false, layer: 'none', operation };
48
+ }
49
+ return {
50
+ implemented: false,
51
+ layer: typeof _sentinel[operation] === 'function' ? 'sentinel' : 'none',
52
+ operation,
53
+ };
54
+ }
55
+ export function getVideoCapabilityBackend() {
56
+ return _custom ?? _host ?? _sentinel;
57
+ }
58
+ export function hasVideoCapabilityHostBackend() {
59
+ return _host !== null;
60
+ }
61
+ export function hasVideoCapabilityOperation(operation) {
62
+ return explainVideoCapabilityOperation(operation).implemented;
63
+ }
26
64
  export function inferVideoMimeType(url) {
27
65
  const ext = url.split('?')[0].split('.').pop()?.toLowerCase();
28
66
  switch (ext) {
@@ -48,14 +86,64 @@ export function inferVideoMimeType(url) {
48
86
  return null;
49
87
  }
50
88
  }
89
+ export function installVideoCapabilityHostBackend(backend) {
90
+ if (_host !== null) {
91
+ if (_host !== backend)
92
+ _hostConflict = true;
93
+ return;
94
+ }
95
+ _host = backend;
96
+ }
97
+ export function observeVideoCapabilityHostResult(operation, succeeded) {
98
+ _hostObservation = {
99
+ operation,
100
+ viability: succeeded ? 'available' : 'runtime-api-unavailable',
101
+ };
102
+ }
103
+ export function resetVideoCapabilityBackendForTest() {
104
+ _custom = null;
105
+ _host = null;
106
+ _hostConflict = false;
107
+ _hostObservation = null;
108
+ }
51
109
  // Picks the first source the environment can play, resolving each source's MIME type from its
52
110
  // explicit `type` or, failing that, its URL extension. Returns null when none is playable — the
53
111
  // source-negotiation primitive behind loadVideoResourceFromUrls.
54
112
  export function selectVideoResourceUrl(sources) {
113
+ let backend = null;
55
114
  for (const source of sources) {
56
- if (canPlayVideoType(source.type ?? inferVideoMimeType(source.url) ?? ''))
115
+ const mimeType = source.type ?? inferVideoMimeType(source.url) ?? '';
116
+ if (mimeType === '')
117
+ continue;
118
+ backend ??= getVideoCapabilityBackend();
119
+ if (probeSelectedBackend(backend, mimeType))
57
120
  return source;
58
121
  }
59
122
  return null;
60
123
  }
124
+ export function setVideoCapabilityBackend(backend) {
125
+ _custom = backend;
126
+ }
127
+ function probeSelectedBackend(backend, mimeType) {
128
+ if (mimeType === '')
129
+ return false;
130
+ try {
131
+ return backend.canPlayType(mimeType) === true;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ const _sentinel = {
138
+ canPlayType() {
139
+ return false;
140
+ },
141
+ createVideoElement() {
142
+ return null;
143
+ },
144
+ };
145
+ let _custom = null;
146
+ let _host = null;
147
+ let _hostConflict = false;
148
+ let _hostObservation = null;
61
149
  //# sourceMappingURL=videoFormat.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"videoFormat.js","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,kGAAkG;AAClG,uFAAuF;AACvF,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;AAC5C,CAAC;AAED,mGAAmG;AACnG,gGAAgG;AAChG,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,IAA8B;IAChE,MAAM,CAAC,GAAG,IAAI,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IACnE,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,yEAAyE;IACzE,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAE9G,4CAA4C;IAC5C,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,YAAY,CAAC;IAE1F,6BAA6B;IAC7B,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC;IAEzF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC;IAC9D,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,KAAK,CAAC;QACX,KAAK,KAAK;YACR,OAAO,WAAW,CAAC;QACrB,KAAK,MAAM;YACT,OAAO,YAAY,CAAC;QACtB,KAAK,KAAK;YACR,OAAO,kBAAkB,CAAC;QAC5B,KAAK,KAAK,CAAC;QACX,KAAK,KAAK;YACR,OAAO,WAAW,CAAC;QACrB,KAAK,KAAK;YACR,OAAO,iBAAiB,CAAC;QAC3B,KAAK,KAAK;YACR,OAAO,YAAY,CAAC;QACtB,KAAK,MAAM;YACT,OAAO,+BAA+B,CAAC;QACzC,KAAK,KAAK;YACR,OAAO,sBAAsB,CAAC;QAChC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,8FAA8F;AAC9F,gGAAgG;AAChG,iEAAiE;AACjE,MAAM,UAAU,sBAAsB,CAAC,OAAqC;IAC1E,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,gBAAgB,CAAC,MAAM,CAAC,IAAI,IAAI,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;YAAE,OAAO,MAAM,CAAC;IAC3F,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"videoFormat.js","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { VideoResource } from '@flighthq/types/contract';
2
- export declare function createVideoResource(element?: HTMLVideoElement, objectUrl?: string): VideoResource;
2
+ export declare function createVideoResource(element?: HTMLVideoElement, objectUrl?: string, ownsElement?: boolean): VideoResource;
3
+ export declare function destroyVideoResource(resource: VideoResource): void;
3
4
  export declare function disposeVideoResource(resource: VideoResource): void;
4
5
  export declare function getVideoResourceDuration(resource: Readonly<VideoResource>): number;
5
6
  export declare function getVideoResourceHeight(resource: Readonly<VideoResource>): number;
@@ -1 +1 @@
1
- {"version":3,"file":"videoResource.d.ts","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAO9D,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAEjG;AAQD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAalE;AAID,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAGlF;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAGhF;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAG/E;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAElF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAG/E;AAID,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAG/E"}
1
+ {"version":3,"file":"videoResource.d.ts","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAS9D,wBAAgB,mBAAmB,CACjC,OAAO,CAAC,EAAE,gBAAgB,EAC1B,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,CAAC,EAAE,OAAO,GACpB,aAAa,CAEf;AAMD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAgBlE;AAKD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAYlE;AAID,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAGlF;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAGhF;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAG/E;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAElF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAG/E;AAID,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,GAAG,OAAO,CAG/E"}
@@ -1,30 +1,49 @@
1
1
  // No cloneVideoResource: a VideoResource is a thin carrier over a single HTMLVideoElement, and an
2
2
  // element cannot be duplicated (each carries its own decoder and playback position). Wrap the same
3
3
  // element in a second createVideoResource call if two carriers over one stream are truly wanted.
4
- // `objectUrl` transfers ownership of a blob object URL to the resource, so disposal revokes it. Pass
4
+ // `objectUrl` transfers ownership of a blob object URL to the resource, so destruction revokes it. Pass
5
5
  // it only for a URL this resource should own — a caller-managed URL stays the caller's to revoke.
6
- export function createVideoResource(element, objectUrl) {
7
- return { element: element ?? null, objectUrl: objectUrl ?? null };
8
- }
9
- // Releases the decoder the element holds: clearing the src and calling load() detaches the media
10
- // resource so the browser can free its buffers, then the element reference is dropped for GC. Also
11
- // revokes an owned object URL, whose blob-store entry is what keeps the underlying Blob reachable —
12
- // the revoke belongs here, at end of life, because the element fetches from that URL for as long as it
13
- // plays. Both are dispose*, not destroy*: each releases a reference so memory becomes GC-eligible,
14
- // rather than freeing a non-GC handle.
6
+ // `ownsElement` true means this resource created the element and destroyVideoResource will release
7
+ // its decoder; false means the caller supplied it and destruction only drops the reference.
8
+ export function createVideoResource(element, objectUrl, ownsElement) {
9
+ return { element: element ?? null, objectUrl: objectUrl ?? null, ownsElement: ownsElement ?? false };
10
+ }
11
+ // Releases a video resource's non-GC state: for owned elements, detaches the media source so the
12
+ // browser can free decoder buffers and drops MediaStream srcObject (without stopping caller-owned
13
+ // tracks); for borrowed elements, only drops the reference. Revokes a held object URL in either case
14
+ // since it is the resource's to manage regardless of element ownership.
15
+ export function destroyVideoResource(resource) {
16
+ const element = resource.element;
17
+ if (element !== null && resource.ownsElement) {
18
+ // Detach a live MediaStream without stopping its tracks — those belong to the caller.
19
+ if (element.srcObject !== null) {
20
+ element.srcObject = null;
21
+ }
22
+ element.removeAttribute('src');
23
+ element.load();
24
+ }
25
+ if (resource.objectUrl !== null) {
26
+ URL.revokeObjectURL(resource.objectUrl);
27
+ resource.objectUrl = null;
28
+ }
29
+ resource.element = null;
30
+ resource.ownsElement = false;
31
+ }
32
+ // Legacy unconditional teardown — releases the decoder regardless of ownership. Callers that know
33
+ // they own the element (e.g. loader error paths that just created it) can still use this; for
34
+ // caller-provided elements, prefer destroyVideoResource which respects the ownership flag.
15
35
  export function disposeVideoResource(resource) {
16
36
  const element = resource.element;
17
37
  if (element !== null) {
18
38
  element.removeAttribute('src');
19
39
  element.load();
20
40
  }
21
- // Revoked after the element has let go of the src, so the media resource is never detached from a
22
- // URL the element is still reading through.
23
41
  if (resource.objectUrl !== null) {
24
42
  URL.revokeObjectURL(resource.objectUrl);
25
43
  resource.objectUrl = null;
26
44
  }
27
45
  resource.element = null;
46
+ resource.ownsElement = false;
28
47
  }
29
48
  // Duration in seconds of the loaded media, or 0 when no element is attached. May be NaN before
30
49
  // metadata has loaded and Infinity for open-ended live streams — both come straight from the element.
@@ -1 +1 @@
1
- {"version":3,"file":"videoResource.js","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,mGAAmG;AACnG,iGAAiG;AACjG,qGAAqG;AACrG,kGAAkG;AAClG,MAAM,UAAU,mBAAmB,CAAC,OAA0B,EAAE,SAAkB;IAChF,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,SAAS,EAAE,SAAS,IAAI,IAAI,EAAE,CAAC;AACpE,CAAC;AAED,iGAAiG;AACjG,mGAAmG;AACnG,oGAAoG;AACpG,uGAAuG;AACvG,mGAAmG;AACnG,uCAAuC;AACvC,MAAM,UAAU,oBAAoB,CAAC,QAAuB;IAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,EAAE,CAAC;IACjB,CAAC;IACD,kGAAkG;IAClG,4CAA4C;IAC5C,IAAI,QAAQ,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;QAChC,GAAG,CAAC,eAAe,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACxC,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC;IAC5B,CAAC;IACD,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED,+FAA+F;AAC/F,sGAAsG;AACtG,MAAM,UAAU,wBAAwB,CAAC,QAAiC;IACxE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAiC;IACtE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAiC;IACrE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,OAAO,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAiC;IACvE,OAAO,QAAQ,CAAC,OAAO,KAAK,IAAI,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAiC;IACpE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,OAAO,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC,IAAI,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC;AACjF,CAAC;AAED,qGAAqG;AACrG,kGAAkG;AAClG,MAAM,UAAU,oBAAoB,CAAC,QAAiC;IACpE,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,OAAO,OAAO,KAAK,IAAI,IAAI,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC;AACrE,CAAC;AAED,4FAA4F;AAC5F,MAAM,iBAAiB,GAAG,CAAC,CAAC"}
1
+ {"version":3,"file":"videoResource.js","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { VideoResource, VideoResourceLoadOptions, VideoResourceUrl } from '@flighthq/types/contract';
2
- export declare function createVideoResourceFromMediaStream(stream: MediaStream): VideoResource;
2
+ export declare function createVideoResourceFromMediaStream(stream: MediaStream): VideoResource | null;
3
3
  export declare function loadVideoResourceFromBlob(blob: Blob, options?: Readonly<VideoResourceLoadOptions>, signal?: AbortSignal): Promise<VideoResource>;
4
4
  export declare function loadVideoResourceFromUrl(url: string, options?: Readonly<VideoResourceLoadOptions>, signal?: AbortSignal): Promise<VideoResource>;
5
5
  export declare function loadVideoResourceFromUrls(sources: Readonly<VideoResourceUrl[]>, options?: Readonly<VideoResourceLoadOptions>, signal?: AbortSignal): Promise<VideoResource>;
@@ -1 +1 @@
1
- {"version":3,"file":"videoResourceFrom.d.ts","sourceRoot":"","sources":["../src/videoResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAO1G,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,CAIrF;AAUD,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAWxB;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CA4CxB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,EACrC,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAIxB"}
1
+ {"version":3,"file":"videoResourceFrom.d.ts","sourceRoot":"","sources":["../src/videoResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAO1G,wBAAgB,kCAAkC,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,GAAG,IAAI,CAK5F;AAUD,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAWxB;AAED,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CA6CxB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,EACrC,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAIxB"}
@@ -1,11 +1,13 @@
1
- import { selectVideoResourceUrl } from './videoFormat';
1
+ import { getVideoCapabilityBackend, selectVideoResourceUrl } from './videoFormat';
2
2
  import { createVideoResource, disposeVideoResource } from './videoResource';
3
3
  // Wraps a live MediaStream (camera, screen capture, canvas.captureStream) as a video resource by
4
- // assigning it to element.srcObject. Pure DOM, no load — the stream feeds frames as they arrive.
4
+ // assigning it to element.srcObject. Returns null when the backend cannot create a video element.
5
5
  export function createVideoResourceFromMediaStream(stream) {
6
- const element = document.createElement('video');
6
+ const element = (getVideoCapabilityBackend().createVideoElement?.() ?? null);
7
+ if (element === null)
8
+ return null;
7
9
  element.srcObject = stream;
8
- return createVideoResource(element);
10
+ return createVideoResource(element, undefined, true);
9
11
  }
10
12
  // Loads from a Blob by wrapping it in an object URL, which the returned resource then owns: pass it to
11
13
  // disposeVideoResource to revoke it. The revoke deliberately does not happen when the load settles —
@@ -31,8 +33,10 @@ export async function loadVideoResourceFromBlob(blob, options, signal) {
31
33
  export function loadVideoResourceFromUrl(url, options, signal) {
32
34
  if (signal?.aborted)
33
35
  return Promise.reject(signal.reason);
36
+ const element = (getVideoCapabilityBackend().createVideoElement?.() ?? null);
37
+ if (element === null)
38
+ return Promise.reject(new Error('No video element backend available'));
34
39
  return new Promise((resolve, reject) => {
35
- const element = document.createElement('video');
36
40
  element.preload = (options?.preload ?? 'auto');
37
41
  // crossOrigin must be set before assigning src so the fetched frames stay untainted for GPU upload.
38
42
  if (options?.crossOrigin !== undefined)
@@ -44,7 +48,7 @@ export function loadVideoResourceFromUrl(url, options, signal) {
44
48
  const readyEvent = readinessEventName(options?.readiness);
45
49
  const onReady = () => {
46
50
  cleanup();
47
- resolve(createVideoResource(element));
51
+ resolve(createVideoResource(element, undefined, true));
48
52
  };
49
53
  // Both rejection paths abandon an element this function created, so both owe it the same decoder
50
54
  // release — routed through disposeVideoResource so the sequence has exactly one home. Assigning
@@ -1 +1 @@
1
- {"version":3,"file":"videoResourceFrom.js","sourceRoot":"","sources":["../src/videoResourceFrom.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"videoResourceFrom.js","sourceRoot":"","sources":["../src/videoResourceFrom.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/video",
3
- "version": "0.5.0-next.1944.cd6b6e7",
3
+ "version": "0.5.1-edge.638.d4dbd4e",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/types": "0.5.0-next.1944.cd6b6e7"
41
+ "@flighthq/types": "0.5.1-edge.638.d4dbd4e"
42
42
  },
43
43
  "devDependencies": {
44
44
  "typescript": "^5.3.0"
@@ -1,18 +1,56 @@
1
- import { canPlayVideoType, detectVideoMimeType, inferVideoMimeType, selectVideoResourceUrl } from './videoFormat';
1
+ import type { VideoCapabilityBackend, VideoResourceUrl } from '@flighthq/types/contract';
2
+
3
+ import {
4
+ canPlayVideoType,
5
+ detectVideoMimeType,
6
+ explainVideoCapabilityBackend,
7
+ explainVideoCapabilityOperation,
8
+ getVideoCapabilityBackend,
9
+ hasVideoCapabilityHostBackend,
10
+ hasVideoCapabilityOperation,
11
+ inferVideoMimeType,
12
+ installVideoCapabilityHostBackend,
13
+ observeVideoCapabilityHostResult,
14
+ resetVideoCapabilityBackendForTest,
15
+ selectVideoResourceUrl,
16
+ setVideoCapabilityBackend,
17
+ } from './videoFormat';
2
18
 
3
19
  afterEach(() => {
20
+ resetVideoCapabilityBackendForTest();
4
21
  vi.restoreAllMocks();
5
22
  });
6
23
 
7
24
  describe('canPlayVideoType', () => {
8
- it('is false in jsdom where no codecs are registered', () => {
25
+ it('is false when no backend is installed', () => {
9
26
  expect(canPlayVideoType('video/mp4')).toBe(false);
10
27
  });
11
28
 
12
- it('is true when the element reports it can play the type', () => {
13
- vi.spyOn(HTMLVideoElement.prototype, 'canPlayType').mockReturnValue('probably');
29
+ it('uses an installed custom backend', () => {
30
+ setVideoCapabilityBackend({ canPlayType: () => true });
14
31
  expect(canPlayVideoType('video/mp4')).toBe(true);
15
32
  });
33
+
34
+ it('rejects an empty MIME type without invoking the selected backend', () => {
35
+ const canPlayType = vi.fn(() => true);
36
+ setVideoCapabilityBackend({ canPlayType });
37
+ expect(canPlayVideoType('')).toBe(false);
38
+ expect(canPlayType).not.toHaveBeenCalled();
39
+ });
40
+
41
+ it('accepts only primitive true from an untyped backend', () => {
42
+ setVideoCapabilityBackend({ canPlayType: () => 'probably' } as unknown as VideoCapabilityBackend);
43
+ expect(canPlayVideoType('video/mp4')).toBe(false);
44
+ });
45
+
46
+ it('normalizes backend exceptions to false', () => {
47
+ setVideoCapabilityBackend({
48
+ canPlayType(): boolean {
49
+ throw new Error('unavailable');
50
+ },
51
+ });
52
+ expect(canPlayVideoType('video/mp4')).toBe(false);
53
+ });
16
54
  });
17
55
 
18
56
  describe('detectVideoMimeType', () => {
@@ -44,6 +82,78 @@ describe('detectVideoMimeType', () => {
44
82
  });
45
83
  });
46
84
 
85
+ describe('explainVideoCapabilityBackend', () => {
86
+ it('reports host observations while custom selection remains unobserved', () => {
87
+ installVideoCapabilityHostBackend({ canPlayType: () => true });
88
+ observeVideoCapabilityHostResult('canPlayType', false);
89
+ expect(explainVideoCapabilityBackend()).toMatchObject({
90
+ layer: 'host',
91
+ operation: 'canPlayType',
92
+ viability: 'runtime-api-unavailable',
93
+ });
94
+ setVideoCapabilityBackend({ canPlayType: () => true });
95
+ expect(explainVideoCapabilityBackend()).toMatchObject({
96
+ layer: 'custom',
97
+ operation: null,
98
+ viability: 'unobserved',
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('explainVideoCapabilityOperation', () => {
104
+ it('reports sentinel, host, and custom operation support structurally', () => {
105
+ expect(explainVideoCapabilityOperation('canPlayType')).toEqual({
106
+ implemented: false,
107
+ layer: 'sentinel',
108
+ operation: 'canPlayType',
109
+ });
110
+ expect(hasVideoCapabilityOperation('canPlayType')).toBe(false);
111
+
112
+ installVideoCapabilityHostBackend({ canPlayType: () => true });
113
+ expect(explainVideoCapabilityOperation('canPlayType').layer).toBe('host');
114
+ expect(hasVideoCapabilityOperation('canPlayType')).toBe(true);
115
+
116
+ setVideoCapabilityBackend({ canPlayType: 1 } as unknown as VideoCapabilityBackend);
117
+ expect(explainVideoCapabilityOperation('canPlayType')).toEqual({
118
+ implemented: false,
119
+ layer: 'none',
120
+ operation: 'canPlayType',
121
+ });
122
+ expect(hasVideoCapabilityOperation('canPlayType')).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe('getVideoCapabilityBackend', () => {
127
+ it('uses custom over host and reveals the host again when custom is cleared', () => {
128
+ const host = { canPlayType: vi.fn(() => true) };
129
+ const custom = { canPlayType: vi.fn(() => false) };
130
+ installVideoCapabilityHostBackend(host);
131
+ setVideoCapabilityBackend(custom);
132
+ expect(getVideoCapabilityBackend()).toBe(custom);
133
+ expect(canPlayVideoType('video/mp4')).toBe(false);
134
+ setVideoCapabilityBackend(null);
135
+ expect(getVideoCapabilityBackend()).toBe(host);
136
+ expect(canPlayVideoType('video/mp4')).toBe(true);
137
+ });
138
+ });
139
+
140
+ describe('hasVideoCapabilityHostBackend', () => {
141
+ it('tracks host occupancy independently of a custom backend', () => {
142
+ setVideoCapabilityBackend({ canPlayType: () => true });
143
+ expect(hasVideoCapabilityHostBackend()).toBe(false);
144
+ installVideoCapabilityHostBackend({ canPlayType: () => false });
145
+ expect(hasVideoCapabilityHostBackend()).toBe(true);
146
+ });
147
+ });
148
+
149
+ describe('hasVideoCapabilityOperation', () => {
150
+ it('cannot diverge from the operation explanation', () => {
151
+ expect(hasVideoCapabilityOperation('canPlayType')).toBe(explainVideoCapabilityOperation('canPlayType').implemented);
152
+ setVideoCapabilityBackend({ canPlayType: () => true });
153
+ expect(hasVideoCapabilityOperation('canPlayType')).toBe(explainVideoCapabilityOperation('canPlayType').implemented);
154
+ });
155
+ });
156
+
47
157
  describe('inferVideoMimeType', () => {
48
158
  it('returns "video/mp4" for .mp4 files', () => {
49
159
  expect(inferVideoMimeType('clip.mp4')).toBe('video/mp4');
@@ -98,6 +208,47 @@ describe('inferVideoMimeType', () => {
98
208
  });
99
209
  });
100
210
 
211
+ describe('installVideoCapabilityHostBackend', () => {
212
+ it('keeps the first host and reports only a distinct second host as conflict', () => {
213
+ const first = { canPlayType: vi.fn(() => true) };
214
+ installVideoCapabilityHostBackend(first);
215
+ installVideoCapabilityHostBackend(first);
216
+ expect(explainVideoCapabilityBackend().conflict).toBe(false);
217
+ installVideoCapabilityHostBackend({ canPlayType: () => false });
218
+ expect(getVideoCapabilityBackend()).toBe(first);
219
+ expect(explainVideoCapabilityBackend().conflict).toBe(true);
220
+ });
221
+ });
222
+
223
+ describe('observeVideoCapabilityHostResult', () => {
224
+ it('records successful host viability', () => {
225
+ installVideoCapabilityHostBackend({ canPlayType: () => true });
226
+ observeVideoCapabilityHostResult('canPlayType', true);
227
+ expect(explainVideoCapabilityBackend()).toMatchObject({
228
+ operation: 'canPlayType',
229
+ viability: 'available',
230
+ });
231
+ });
232
+ });
233
+
234
+ describe('resetVideoCapabilityBackendForTest', () => {
235
+ it('reset clears custom, host, conflict, and observation state', () => {
236
+ installVideoCapabilityHostBackend({ canPlayType: () => true });
237
+ installVideoCapabilityHostBackend({ canPlayType: () => false });
238
+ observeVideoCapabilityHostResult('canPlayType', true);
239
+ setVideoCapabilityBackend({ canPlayType: () => true });
240
+ resetVideoCapabilityBackendForTest();
241
+ expect(hasVideoCapabilityHostBackend()).toBe(false);
242
+ expect(explainVideoCapabilityBackend()).toEqual({
243
+ conflict: false,
244
+ layer: 'host-not-enabled',
245
+ operation: null,
246
+ viability: 'unobserved',
247
+ });
248
+ expect(canPlayVideoType('video/mp4')).toBe(false);
249
+ });
250
+ });
251
+
101
252
  describe('selectVideoResourceUrl', () => {
102
253
  it('returns null when no source is playable', () => {
103
254
  expect(selectVideoResourceUrl([{ url: 'clip.mp4' }])).toBeNull();
@@ -108,18 +259,86 @@ describe('selectVideoResourceUrl', () => {
108
259
  });
109
260
 
110
261
  it('picks the first source whose inferred type is playable', () => {
111
- vi.spyOn(HTMLVideoElement.prototype, 'canPlayType').mockImplementation((type) =>
112
- type === 'video/webm' ? 'probably' : '',
113
- );
262
+ setVideoCapabilityBackend({ canPlayType: (type) => type === 'video/webm' });
114
263
  const selected = selectVideoResourceUrl([{ url: 'clip.mp4' }, { url: 'clip.webm' }]);
115
264
  expect(selected?.url).toBe('clip.webm');
116
265
  });
117
266
 
118
267
  it('honours an explicit type over the URL extension', () => {
119
- vi.spyOn(HTMLVideoElement.prototype, 'canPlayType').mockImplementation((type) =>
120
- type === 'video/mp4' ? 'maybe' : '',
121
- );
268
+ setVideoCapabilityBackend({ canPlayType: (type) => type === 'video/mp4' });
122
269
  const selected = selectVideoResourceUrl([{ url: 'stream', type: 'video/mp4' }]);
123
270
  expect(selected?.url).toBe('stream');
124
271
  });
272
+
273
+ it('treats an explicit empty type as authoritative and does not read the URL', () => {
274
+ const canPlayType = vi.fn(() => true);
275
+ setVideoCapabilityBackend({ canPlayType });
276
+ const source = Object.defineProperties(
277
+ {},
278
+ {
279
+ type: { value: '', enumerable: true },
280
+ url: {
281
+ enumerable: true,
282
+ get(): string {
283
+ throw new Error('URL must remain unread');
284
+ },
285
+ },
286
+ },
287
+ ) as VideoResourceUrl;
288
+ expect(selectVideoResourceUrl([source])).toBeNull();
289
+ expect(canPlayType).not.toHaveBeenCalled();
290
+ });
291
+
292
+ it('skips unknown inferred types without invoking the backend', () => {
293
+ const canPlayType = vi.fn(() => true);
294
+ setVideoCapabilityBackend({ canPlayType });
295
+ expect(selectVideoResourceUrl([{ url: 'clip.avi' }])).toBeNull();
296
+ expect(canPlayType).not.toHaveBeenCalled();
297
+ });
298
+
299
+ it('pins one lazily resolved backend across a reentrant custom-backend change', () => {
300
+ const replacement = { canPlayType: vi.fn(() => false) };
301
+ const initial = {
302
+ canPlayType: vi.fn((type: string) => {
303
+ setVideoCapabilityBackend(replacement);
304
+ return type === 'video/webm';
305
+ }),
306
+ };
307
+ setVideoCapabilityBackend(initial);
308
+ expect(selectVideoResourceUrl([{ url: 'clip.mp4' }, { url: 'clip.webm' }])?.url).toBe('clip.webm');
309
+ expect(initial.canPlayType).toHaveBeenCalledTimes(2);
310
+ expect(replacement.canPlayType).not.toHaveBeenCalled();
311
+ });
312
+
313
+ it('continues after invalid truthy results and backend exceptions', () => {
314
+ const canPlayType = vi
315
+ .fn()
316
+ .mockReturnValueOnce('probably')
317
+ .mockImplementationOnce(() => {
318
+ throw new Error('unavailable');
319
+ })
320
+ .mockReturnValueOnce(true);
321
+ setVideoCapabilityBackend({ canPlayType } as VideoCapabilityBackend);
322
+ const selected = selectVideoResourceUrl([{ url: 'first.mp4' }, { url: 'second.webm' }, { url: 'third.ogv' }]);
323
+ expect(selected?.url).toBe('third.ogv');
324
+ });
325
+
326
+ it('does not swallow caller property-access exceptions', () => {
327
+ const source = Object.defineProperty({ url: 'clip.mp4' }, 'type', {
328
+ get(): string {
329
+ throw new Error('caller getter failed');
330
+ },
331
+ }) as VideoResourceUrl;
332
+ expect(() => selectVideoResourceUrl([source])).toThrow('caller getter failed');
333
+ });
334
+ });
335
+
336
+ describe('setVideoCapabilityBackend', () => {
337
+ it('selects and clears a custom backend', () => {
338
+ const custom = { canPlayType: () => true };
339
+ setVideoCapabilityBackend(custom);
340
+ expect(getVideoCapabilityBackend()).toBe(custom);
341
+ setVideoCapabilityBackend(null);
342
+ expect(getVideoCapabilityBackend()).not.toBe(custom);
343
+ });
125
344
  });
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  createVideoResource,
3
+ destroyVideoResource,
3
4
  disposeVideoResource,
4
5
  getVideoResourceDuration,
5
6
  getVideoResourceHeight,
@@ -19,12 +20,135 @@ describe('createVideoResource', () => {
19
20
  it('returns a resource with null element when called with no arguments', () => {
20
21
  const resource = createVideoResource();
21
22
  expect(resource.element).toBeNull();
23
+ expect(resource.ownsElement).toBe(false);
22
24
  });
23
25
 
24
- it('stores the provided video element', () => {
26
+ it('stores the provided video element as borrowed by default', () => {
25
27
  const element = document.createElement('video');
26
28
  const resource = createVideoResource(element);
27
29
  expect(resource.element).toBe(element);
30
+ expect(resource.ownsElement).toBe(false);
31
+ });
32
+
33
+ it('marks element as owned when explicitly requested', () => {
34
+ const element = document.createElement('video');
35
+ const resource = createVideoResource(element, undefined, true);
36
+ expect(resource.element).toBe(element);
37
+ expect(resource.ownsElement).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('destroyVideoResource', () => {
42
+ it('releases an owned element by detaching src and reloading to free the decoder', () => {
43
+ const removeAttribute = vi.fn();
44
+ const load = vi.fn();
45
+ const element = { removeAttribute, load, srcObject: null } as unknown as HTMLVideoElement;
46
+ const resource = createVideoResource(element, undefined, true);
47
+
48
+ destroyVideoResource(resource);
49
+
50
+ expect(removeAttribute).toHaveBeenCalledWith('src');
51
+ expect(load).toHaveBeenCalledOnce();
52
+ expect(resource.element).toBeNull();
53
+ expect(resource.ownsElement).toBe(false);
54
+ });
55
+
56
+ it('does not release a borrowed element — the caller manages its lifecycle', () => {
57
+ const removeAttribute = vi.fn();
58
+ const load = vi.fn();
59
+ const element = { removeAttribute, load, srcObject: null } as unknown as HTMLVideoElement;
60
+ const resource = createVideoResource(element);
61
+
62
+ destroyVideoResource(resource);
63
+
64
+ expect(removeAttribute).not.toHaveBeenCalled();
65
+ expect(load).not.toHaveBeenCalled();
66
+ expect(resource.element).toBeNull();
67
+ expect(resource.ownsElement).toBe(false);
68
+ });
69
+
70
+ it('clears srcObject on an owned MediaStream element without stopping caller-owned tracks', () => {
71
+ const track = { stop: vi.fn() } as unknown as MediaStreamTrack;
72
+ const stream = { getTracks: () => [track] } as unknown as MediaStream;
73
+ const element = {
74
+ removeAttribute: vi.fn(),
75
+ load: vi.fn(),
76
+ srcObject: stream,
77
+ } as unknown as HTMLVideoElement;
78
+ const resource = createVideoResource(element, undefined, true);
79
+
80
+ destroyVideoResource(resource);
81
+
82
+ expect(element.srcObject).toBeNull();
83
+ expect(track.stop).not.toHaveBeenCalled();
84
+ });
85
+
86
+ it('does not touch srcObject on a borrowed element', () => {
87
+ const stream = {} as MediaStream;
88
+ const element = {
89
+ removeAttribute: vi.fn(),
90
+ load: vi.fn(),
91
+ srcObject: stream,
92
+ } as unknown as HTMLVideoElement;
93
+ const resource = createVideoResource(element);
94
+
95
+ destroyVideoResource(resource);
96
+
97
+ expect(element.srcObject).toBe(stream);
98
+ });
99
+
100
+ it('revokes an owned object URL after the element has released its src', () => {
101
+ const order: string[] = [];
102
+ vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => void order.push('revoke'));
103
+ const element = {
104
+ removeAttribute: vi.fn(() => void order.push('removeAttribute')),
105
+ load: vi.fn(() => void order.push('load')),
106
+ srcObject: null,
107
+ } as unknown as HTMLVideoElement;
108
+
109
+ destroyVideoResource(createVideoResource(element, 'blob:owned', true));
110
+
111
+ expect(order).toEqual(['removeAttribute', 'load', 'revoke']);
112
+ });
113
+
114
+ it('revokes an object URL even when the element is borrowed', () => {
115
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
116
+ const element = { srcObject: null } as unknown as HTMLVideoElement;
117
+ const resource = createVideoResource(element, 'blob:owned');
118
+
119
+ destroyVideoResource(resource);
120
+
121
+ expect(revokeSpy).toHaveBeenCalledWith('blob:owned');
122
+ expect(resource.objectUrl).toBeNull();
123
+ });
124
+
125
+ it('is idempotent — a second destruction is a no-op', () => {
126
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
127
+ const element = {
128
+ removeAttribute: vi.fn(),
129
+ load: vi.fn(),
130
+ srcObject: null,
131
+ } as unknown as HTMLVideoElement;
132
+ const resource = createVideoResource(element, 'blob:owned', true);
133
+
134
+ destroyVideoResource(resource);
135
+ destroyVideoResource(resource);
136
+
137
+ expect(element.removeAttribute).toHaveBeenCalledTimes(1);
138
+ expect(revokeSpy).toHaveBeenCalledTimes(1);
139
+ });
140
+
141
+ it('nulls the element so VideoChannel queries degrade to sentinels', () => {
142
+ const element = {
143
+ removeAttribute: vi.fn(),
144
+ load: vi.fn(),
145
+ srcObject: null,
146
+ } as unknown as HTMLVideoElement;
147
+ const resource = createVideoResource(element, undefined, true);
148
+
149
+ destroyVideoResource(resource);
150
+
151
+ expect(resource.element).toBeNull();
28
152
  });
29
153
  });
30
154
 
@@ -40,6 +164,7 @@ describe('disposeVideoResource', () => {
40
164
  expect(removeAttribute).toHaveBeenCalledWith('src');
41
165
  expect(load).toHaveBeenCalledOnce();
42
166
  expect(resource.element).toBeNull();
167
+ expect(resource.ownsElement).toBe(false);
43
168
  });
44
169
 
45
170
  it('is a no-op on an already element-less resource', () => {
@@ -1,3 +1,8 @@
1
+ import {
2
+ installVideoCapabilityHostBackend,
3
+ resetVideoCapabilityBackendForTest,
4
+ setVideoCapabilityBackend,
5
+ } from './videoFormat';
1
6
  import {
2
7
  createVideoResourceFromMediaStream,
3
8
  loadVideoResourceFromBlob,
@@ -5,20 +10,23 @@ import {
5
10
  loadVideoResourceFromUrls,
6
11
  } from './videoResourceFrom';
7
12
 
8
- // Capture every <video> the loaders create so tests can drive its media events synchronously.
13
+ // Capture every <video> the backend creates so tests can drive its media events synchronously.
9
14
  let created: HTMLVideoElement[];
10
15
 
11
16
  beforeEach(() => {
12
17
  created = [];
13
- const original = document.createElement.bind(document);
14
- vi.spyOn(document, 'createElement').mockImplementation(((tag: string, options?: ElementCreationOptions) => {
15
- const element = original(tag, options);
16
- if (tag === 'video') created.push(element as HTMLVideoElement);
17
- return element;
18
- }) as typeof document.createElement);
18
+ installVideoCapabilityHostBackend({
19
+ canPlayType: () => false,
20
+ createVideoElement() {
21
+ const element = document.createElement('video');
22
+ created.push(element);
23
+ return element;
24
+ },
25
+ });
19
26
  });
20
27
 
21
28
  afterEach(() => {
29
+ resetVideoCapabilityBackendForTest();
22
30
  vi.restoreAllMocks();
23
31
  });
24
32
 
@@ -27,11 +35,19 @@ function lastVideo(): HTMLVideoElement {
27
35
  }
28
36
 
29
37
  describe('createVideoResourceFromMediaStream', () => {
30
- it('wraps a MediaStream by assigning it to srcObject', () => {
38
+ it('wraps a MediaStream by assigning it to srcObject and marks the element as owned', () => {
31
39
  const stream = {} as MediaStream;
32
40
  const resource = createVideoResourceFromMediaStream(stream);
33
- expect(resource.element).not.toBeNull();
34
- expect((resource.element as HTMLVideoElement).srcObject).toBe(stream);
41
+ expect(resource).not.toBeNull();
42
+ expect(resource!.element).not.toBeNull();
43
+ expect((resource!.element as HTMLVideoElement).srcObject).toBe(stream);
44
+ expect(resource!.ownsElement).toBe(true);
45
+ });
46
+
47
+ it('returns null when the backend cannot create a video element', () => {
48
+ resetVideoCapabilityBackendForTest();
49
+ const resource = createVideoResourceFromMediaStream({} as MediaStream);
50
+ expect(resource).toBeNull();
35
51
  });
36
52
  });
37
53
 
@@ -46,6 +62,7 @@ describe('loadVideoResourceFromBlob', () => {
46
62
  lastVideo().dispatchEvent(new Event('canplay'));
47
63
  const resource = await promise;
48
64
  expect(resource.objectUrl).toBe('blob:mock');
65
+ expect(resource.ownsElement).toBe(true);
49
66
  expect(revokeSpy).not.toHaveBeenCalled();
50
67
  });
51
68
 
@@ -97,6 +114,12 @@ describe('loadVideoResourceFromUrl', () => {
97
114
  element.dispatchEvent(new Event('canplay'));
98
115
  const resource = await promise;
99
116
  expect(resource.element).toBe(element);
117
+ expect(resource.ownsElement).toBe(true);
118
+ });
119
+
120
+ it('rejects when the backend cannot create a video element', async () => {
121
+ resetVideoCapabilityBackendForTest();
122
+ await expect(loadVideoResourceFromUrl('test.mp4')).rejects.toThrow('No video element backend available');
100
123
  });
101
124
 
102
125
  it('applies crossOrigin, muted, and preload from options', async () => {
@@ -185,7 +208,14 @@ describe('loadVideoResourceFromUrls', () => {
185
208
  });
186
209
 
187
210
  it('loads the first playable source', async () => {
188
- vi.spyOn(HTMLVideoElement.prototype, 'canPlayType').mockReturnValue('probably');
211
+ setVideoCapabilityBackend({
212
+ canPlayType: () => true,
213
+ createVideoElement() {
214
+ const element = document.createElement('video');
215
+ created.push(element);
216
+ return element;
217
+ },
218
+ });
189
219
  const promise = loadVideoResourceFromUrls([{ url: 'clip.mp4' }]);
190
220
  const element = lastVideo();
191
221
  element.dispatchEvent(new Event('canplay'));