@flighthq/video 0.3.0-next.906.07cea63 → 0.3.0

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.
@@ -1,5 +1,5 @@
1
1
  import type { VideoResource } from '@flighthq/types/contract';
2
- export declare function createVideoResource(element?: HTMLVideoElement): VideoResource;
2
+ export declare function createVideoResource(element?: HTMLVideoElement, objectUrl?: string): VideoResource;
3
3
  export declare function disposeVideoResource(resource: VideoResource): void;
4
4
  export declare function getVideoResourceDuration(resource: Readonly<VideoResource>): number;
5
5
  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;AAK9D,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,aAAa,CAE7E;AAKD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAOlE;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;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,18 +1,29 @@
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
- export function createVideoResource(element) {
5
- return { element: element ?? null };
4
+ // `objectUrl` transfers ownership of a blob object URL to the resource, so disposal revokes it. Pass
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 };
6
8
  }
7
9
  // Releases the decoder the element holds: clearing the src and calling load() detaches the media
8
- // resource so the browser can free its buffers, then the element reference is dropped for GC. Unlike
9
- // destroy*, there is no non-GC handle to free here the element is plain GC-managed memory.
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.
10
15
  export function disposeVideoResource(resource) {
11
16
  const element = resource.element;
12
17
  if (element !== null) {
13
18
  element.removeAttribute('src');
14
19
  element.load();
15
20
  }
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
+ if (resource.objectUrl !== null) {
24
+ URL.revokeObjectURL(resource.objectUrl);
25
+ resource.objectUrl = null;
26
+ }
16
27
  resource.element = null;
17
28
  }
18
29
  // Duration in seconds of the loaded media, or 0 when no element is attached. May be NaN before
@@ -1 +1 @@
1
- {"version":3,"file":"videoResource.js","sourceRoot":"","sources":["../src/videoResource.ts"],"names":[],"mappings":"AAEA,kGAAkG;AAClG,mGAAmG;AACnG,iGAAiG;AACjG,MAAM,UAAU,mBAAmB,CAAC,OAA0B;IAC5D,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,CAAC;AACtC,CAAC;AAED,iGAAiG;AACjG,qGAAqG;AACrG,6FAA6F;AAC7F,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,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":"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 +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;AAID,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,IAAI,EACV,OAAO,CAAC,EAAE,QAAQ,CAAC,wBAAwB,CAAC,EAC5C,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,CAAC,CAOxB;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,CAuCxB;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,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,5 +1,5 @@
1
1
  import { selectVideoResourceUrl } from './videoFormat';
2
- import { createVideoResource } from './videoResource';
2
+ import { createVideoResource, disposeVideoResource } from './videoResource';
3
3
  // Wraps a live MediaStream (camera, screen capture, canvas.captureStream) as a video resource by
4
4
  // assigning it to element.srcObject. Pure DOM, no load — the stream feeds frames as they arrive.
5
5
  export function createVideoResourceFromMediaStream(stream) {
@@ -7,16 +7,26 @@ export function createVideoResourceFromMediaStream(stream) {
7
7
  element.srcObject = stream;
8
8
  return createVideoResource(element);
9
9
  }
10
- // Loads from a Blob by wrapping it in an object URL. This function owns that URL and revokes it once
11
- // the load settles (success or failure), so the caller never has to.
10
+ // Loads from a Blob by wrapping it in an object URL, which the returned resource then owns: pass it to
11
+ // disposeVideoResource to revoke it. The revoke deliberately does not happen when the load settles
12
+ // settling only means the readiness event fired, and the element keeps fetching from the URL while it
13
+ // plays and re-fetches on seek, so revoking there breaks playback of a video that just reported ready
14
+ // (most starkly under `readiness: 'metadata'`, where only the container header has been read).
15
+ //
16
+ // A failed load is the one case this function still revokes, because it returns no resource for the
17
+ // caller to dispose, so nothing else could ever release the URL.
12
18
  export async function loadVideoResourceFromBlob(blob, options, signal) {
13
19
  const url = URL.createObjectURL(blob);
20
+ let resource;
14
21
  try {
15
- return await loadVideoResourceFromUrl(url, options, signal);
22
+ resource = await loadVideoResourceFromUrl(url, options, signal);
16
23
  }
17
- finally {
24
+ catch (error) {
18
25
  URL.revokeObjectURL(url);
26
+ throw error;
19
27
  }
28
+ resource.objectUrl = url;
29
+ return resource;
20
30
  }
21
31
  export function loadVideoResourceFromUrl(url, options, signal) {
22
32
  if (signal?.aborted)
@@ -36,13 +46,18 @@ export function loadVideoResourceFromUrl(url, options, signal) {
36
46
  cleanup();
37
47
  resolve(createVideoResource(element));
38
48
  };
49
+ // Both rejection paths abandon an element this function created, so both owe it the same decoder
50
+ // release — routed through disposeVideoResource so the sequence has exactly one home. Assigning
51
+ // element.src = '' is not that sequence: an empty src resolves against the document base URL, so
52
+ // the element goes on to fetch the *page* as media, and the decoder is never detached.
39
53
  const onError = () => {
40
54
  cleanup();
55
+ disposeVideoResource(createVideoResource(element));
41
56
  reject(new Error(`Failed to load video: ${url}`));
42
57
  };
43
58
  const onAbort = () => {
44
59
  cleanup();
45
- element.src = '';
60
+ disposeVideoResource(createVideoResource(element));
46
61
  reject(signal.reason);
47
62
  };
48
63
  const cleanup = () => {
@@ -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,8 @@
1
1
  {
2
2
  "name": "@flighthq/video",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,7 +38,7 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/types": "0.3.0"
40
42
  },
41
43
  "devDependencies": {
42
44
  "typescript": "^5.3.0"
@@ -9,6 +9,12 @@ import {
9
9
  isVideoResourceReady,
10
10
  } from './videoResource';
11
11
 
12
+ // vi.spyOn hands back the *existing* spy when a method is already spied, so without this the URL
13
+ // spies below share one call history and every count assertion reads the previous test's calls too.
14
+ afterEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
12
18
  describe('createVideoResource', () => {
13
19
  it('returns a resource with null element when called with no arguments', () => {
14
20
  const resource = createVideoResource();
@@ -41,6 +47,54 @@ describe('disposeVideoResource', () => {
41
47
  disposeVideoResource(resource);
42
48
  expect(resource.element).toBeNull();
43
49
  });
50
+
51
+ it('revokes an owned object URL and clears it', () => {
52
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
53
+ const element = { removeAttribute: vi.fn(), load: vi.fn() } as unknown as HTMLVideoElement;
54
+ const resource = createVideoResource(element, 'blob:owned');
55
+
56
+ disposeVideoResource(resource);
57
+
58
+ expect(revokeSpy).toHaveBeenCalledWith('blob:owned');
59
+ expect(resource.objectUrl).toBeNull();
60
+ });
61
+
62
+ // Revoking is what makes the Blob GC-eligible, so it must survive a second disposal without
63
+ // double-revoking a URL the resource no longer owns.
64
+ it('does not revoke again when disposed twice', () => {
65
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
66
+ const element = { removeAttribute: vi.fn(), load: vi.fn() } as unknown as HTMLVideoElement;
67
+ const resource = createVideoResource(element, 'blob:owned');
68
+
69
+ disposeVideoResource(resource);
70
+ disposeVideoResource(resource);
71
+
72
+ expect(revokeSpy).toHaveBeenCalledTimes(1);
73
+ });
74
+
75
+ // A resource built over a URL the caller manages must not have it revoked out from under them.
76
+ it('revokes nothing when the resource owns no object URL', () => {
77
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
78
+ const element = { removeAttribute: vi.fn(), load: vi.fn() } as unknown as HTMLVideoElement;
79
+
80
+ disposeVideoResource(createVideoResource(element));
81
+
82
+ expect(revokeSpy).not.toHaveBeenCalled();
83
+ });
84
+
85
+ // The element must let go of the src before the URL behind it is revoked, not after.
86
+ it('revokes the object URL only after the element has released its src', () => {
87
+ const order: string[] = [];
88
+ vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => void order.push('revoke'));
89
+ const element = {
90
+ removeAttribute: vi.fn(() => void order.push('removeAttribute')),
91
+ load: vi.fn(() => void order.push('load')),
92
+ } as unknown as HTMLVideoElement;
93
+
94
+ disposeVideoResource(createVideoResource(element, 'blob:owned'));
95
+
96
+ expect(order).toEqual(['removeAttribute', 'load', 'revoke']);
97
+ });
44
98
  });
45
99
 
46
100
  describe('getVideoResourceDuration', () => {
@@ -36,16 +36,34 @@ describe('createVideoResourceFromMediaStream', () => {
36
36
  });
37
37
 
38
38
  describe('loadVideoResourceFromBlob', () => {
39
- it('revokes the object URL after a successful load', async () => {
39
+ // The load settling is not the end of the URL's life the element fetches through it for as long as
40
+ // it plays. So the invariant is that the resource comes back still playable, holding a live URL it
41
+ // now owns; an earlier version revoked here and returned a resource whose source was already gone.
42
+ it('returns a resource still holding its live object URL after the load settles', async () => {
40
43
  vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
41
44
  const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
42
45
  const promise = loadVideoResourceFromBlob(new Blob([], { type: 'video/mp4' }));
43
46
  lastVideo().dispatchEvent(new Event('canplay'));
44
- await promise;
45
- expect(revokeSpy).toHaveBeenCalledWith('blob:mock');
47
+ const resource = await promise;
48
+ expect(resource.objectUrl).toBe('blob:mock');
49
+ expect(revokeSpy).not.toHaveBeenCalled();
50
+ });
51
+
52
+ // Readiness 'metadata' is the starkest case: only the container header has been read, so every byte
53
+ // of media data is still to come through the URL.
54
+ it('keeps the object URL live when the load settles at metadata readiness', async () => {
55
+ vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
56
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
57
+ const promise = loadVideoResourceFromBlob(new Blob([], { type: 'video/mp4' }), { readiness: 'metadata' });
58
+ lastVideo().dispatchEvent(new Event('loadedmetadata'));
59
+ const resource = await promise;
60
+ expect(resource.objectUrl).toBe('blob:mock');
61
+ expect(revokeSpy).not.toHaveBeenCalled();
46
62
  });
47
63
 
48
- it('revokes the object URL even when the load fails', async () => {
64
+ // The failure path is the one place this function must still revoke: it returns no resource, so
65
+ // nothing else can ever own the URL.
66
+ it('revokes the object URL when the load fails, since no resource is returned to own it', async () => {
49
67
  vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
50
68
  const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
51
69
  const promise = loadVideoResourceFromBlob(new Blob([], { type: 'video/mp4' }));
@@ -53,6 +71,16 @@ describe('loadVideoResourceFromBlob', () => {
53
71
  await expect(promise).rejects.toThrow('Failed to load video');
54
72
  expect(revokeSpy).toHaveBeenCalledWith('blob:mock');
55
73
  });
74
+
75
+ it('revokes the object URL when the load is aborted, since no resource is returned to own it', async () => {
76
+ vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
77
+ const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
78
+ const controller = new AbortController();
79
+ const promise = loadVideoResourceFromBlob(new Blob([], { type: 'video/mp4' }), undefined, controller.signal);
80
+ controller.abort(new Error('cancelled'));
81
+ await expect(promise).rejects.toThrow('cancelled');
82
+ expect(revokeSpy).toHaveBeenCalledWith('blob:mock');
83
+ });
56
84
  });
57
85
 
58
86
  describe('loadVideoResourceFromUrl', () => {
@@ -119,6 +147,30 @@ describe('loadVideoResourceFromUrl', () => {
119
147
  controller.abort(new Error('cancelled'));
120
148
  await expect(promise).rejects.toThrow('cancelled');
121
149
  });
150
+
151
+ // The two rejection paths are the only ones that abandon an element the loader created; on success
152
+ // the caller takes ownership. Both are covered because a release sequence that is right on one
153
+ // abandonment path and absent on the other still leaks a decoder.
154
+ it('detaches the src and reloads the element it abandons when the load fails', async () => {
155
+ const promise = loadVideoResourceFromUrl('bad.mp4');
156
+ const element = lastVideo();
157
+ const loadSpy = vi.spyOn(element, 'load').mockImplementation(() => {});
158
+ element.dispatchEvent(new Event('error'));
159
+ await expect(promise).rejects.toThrow('Failed to load video');
160
+ expect(element.hasAttribute('src')).toBe(false);
161
+ expect(loadSpy).toHaveBeenCalledTimes(1);
162
+ });
163
+
164
+ it('detaches the src and reloads the element it abandons when the signal is aborted', async () => {
165
+ const controller = new AbortController();
166
+ const promise = loadVideoResourceFromUrl('clip.mp4', undefined, controller.signal);
167
+ const element = lastVideo();
168
+ const loadSpy = vi.spyOn(element, 'load').mockImplementation(() => {});
169
+ controller.abort(new Error('cancelled'));
170
+ await expect(promise).rejects.toThrow('cancelled');
171
+ expect(element.hasAttribute('src')).toBe(false);
172
+ expect(loadSpy).toHaveBeenCalledTimes(1);
173
+ });
122
174
  });
123
175
 
124
176
  describe('loadVideoResourceFromUrls', () => {