@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 +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/videoFormat.d.ts +10 -1
- package/dist/videoFormat.d.ts.map +1 -1
- package/dist/videoFormat.js +94 -6
- package/dist/videoFormat.js.map +1 -1
- package/dist/videoResource.d.ts +2 -1
- package/dist/videoResource.d.ts.map +1 -1
- package/dist/videoResource.js +31 -12
- package/dist/videoResource.js.map +1 -1
- package/dist/videoResourceFrom.d.ts +1 -1
- package/dist/videoResourceFrom.d.ts.map +1 -1
- package/dist/videoResourceFrom.js +10 -6
- package/dist/videoResourceFrom.js.map +1 -1
- package/package.json +2 -2
- package/src/videoFormat.test.ts +229 -10
- package/src/videoResource.test.ts +126 -1
- package/src/videoResourceFrom.test.ts +41 -11
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.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/videoFormat.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/videoFormat.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
package/dist/videoFormat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"videoFormat.js","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"videoFormat.js","sourceRoot":"","sources":["../src/videoFormat.ts"],"names":[],"mappings":"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"}
|
package/dist/videoResource.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/videoResource.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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,
|
|
1
|
+
{"version":3,"file":"videoResource.js","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,mGAAmG;AACnG,iGAAiG;AACjG,wGAAwG;AACxG,kGAAkG;AAClG,mGAAmG;AACnG,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CACjC,OAA0B,EAC1B,SAAkB,EAClB,WAAqB;IAErB,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,SAAS,EAAE,SAAS,IAAI,IAAI,EAAE,WAAW,EAAE,WAAW,IAAI,KAAK,EAAE,CAAC;AACvG,CAAC;AAED,iGAAiG;AACjG,kGAAkG;AAClG,qGAAqG;AACrG,wEAAwE;AACxE,MAAM,UAAU,oBAAoB,CAAC,QAAuB;IAC1D,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAkC,CAAC;IAC5D,IAAI,OAAO,KAAK,IAAI,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC7C,sFAAsF;QACtF,IAAI,OAAO,CAAC,SAAS,KAAK,IAAI,EAAE,CAAC;YAC/B,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;QAC3B,CAAC;QACD,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,EAAE,CAAC;IACjB,CAAC;IACD,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;IACxB,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC;AAC/B,CAAC;AAED,kGAAkG;AAClG,8FAA8F;AAC9F,2FAA2F;AAC3F,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,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;IACxB,QAAQ,CAAC,WAAW,GAAG,KAAK,CAAC;AAC/B,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,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,
|
|
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.
|
|
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 =
|
|
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":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"videoResourceFrom.js","sourceRoot":"","sources":["../src/videoResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAClF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,iGAAiG;AACjG,kGAAkG;AAClG,MAAM,UAAU,kCAAkC,CAAC,MAAmB;IACpE,MAAM,OAAO,GAAG,CAAC,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,EAAE,IAAI,IAAI,CAA4B,CAAC;IACxG,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,CAAC,SAAS,GAAG,MAAM,CAAC;IAC3B,OAAO,mBAAmB,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AACvD,CAAC;AAED,uGAAuG;AACvG,qGAAqG;AACrG,sGAAsG;AACtG,sGAAsG;AACtG,+FAA+F;AAC/F,EAAE;AACF,oGAAoG;AACpG,iEAAiE;AACjE,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAU,EACV,OAA4C,EAC5C,MAAoB;IAEpB,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,QAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAClE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,KAAK,CAAC;IACd,CAAC;IACD,QAAQ,CAAC,SAAS,GAAG,GAAG,CAAC;IACzB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,GAAW,EACX,OAA4C,EAC5C,MAAoB;IAEpB,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,CAAC,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,EAAE,IAAI,IAAI,CAA4B,CAAC;IACxG,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC;IAC7F,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,OAAO,CAAC,OAAO,GAAG,CAAC,OAAO,EAAE,OAAO,IAAI,MAAM,CAAgC,CAAC;QAC9E,oGAAoG;QACpG,IAAI,OAAO,EAAE,WAAW,KAAK,SAAS;YAAE,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAClF,IAAI,OAAO,EAAE,KAAK,KAAK,SAAS;YAAE,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAChE,IAAI,OAAO,EAAE,WAAW,KAAK,SAAS;YAAE,OAAO,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;QAClF,MAAM,UAAU,GAAG,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAE1D,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC;QACzD,CAAC,CAAC;QAEF,iGAAiG;QACjG,gGAAgG;QAChG,iGAAiG;QACjG,uFAAuF;QACvF,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,OAAO,EAAE,CAAC;YACV,oBAAoB,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;YACnD,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,GAAG,EAAE,CAAC,CAAC,CAAC;QACpD,CAAC,CAAC;QAEF,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,OAAO,EAAE,CAAC;YACV,oBAAoB,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;YACnD,MAAM,CAAC,MAAO,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC,CAAC;QAEF,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,OAAO,CAAC,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACjD,OAAO,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzE,CAAC,CAAC;QAEF,OAAO,CAAC,gBAAgB,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,IAAI,MAAM,KAAK,SAAS;YAAE,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpF,OAAO,CAAC,GAAG,GAAG,GAAG,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,OAAqC,EACrC,OAA4C,EAC5C,MAAoB;IAEpB,MAAM,QAAQ,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACjD,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IACrE,OAAO,wBAAwB,CAAC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AACjE,CAAC;AAED,0FAA0F;AAC1F,SAAS,kBAAkB,CAAC,SAAgD;IAC1E,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,UAAU;YACb,OAAO,gBAAgB,CAAC;QAC1B,KAAK,gBAAgB;YACnB,OAAO,gBAAgB,CAAC;QAC1B;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/video",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
41
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"typescript": "^5.3.0"
|
package/src/videoFormat.test.ts
CHANGED
|
@@ -1,18 +1,56 @@
|
|
|
1
|
-
import {
|
|
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
|
|
25
|
+
it('is false when no backend is installed', () => {
|
|
9
26
|
expect(canPlayVideoType('video/mp4')).toBe(false);
|
|
10
27
|
});
|
|
11
28
|
|
|
12
|
-
it('
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
34
|
-
expect(
|
|
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
|
-
|
|
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'));
|