@editframe/elements 0.18.7-beta.0 → 0.18.19-beta.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.
- package/dist/elements/EFAudio.d.ts +1 -2
- package/dist/elements/EFAudio.js +6 -9
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +4 -1
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +3 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +28 -17
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +30 -11
- package/dist/elements/EFMedia/BaseMediaEngine.js +83 -31
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +2 -4
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -12
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +7 -2
- package/dist/elements/EFVideo.d.ts +0 -1
- package/dist/elements/EFVideo.js +0 -9
- package/dist/elements/TargetController.js +3 -2
- package/package.json +2 -2
- package/src/elements/EFAudio.ts +7 -20
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +10 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +45 -21
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +311 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +168 -51
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +2 -12
- package/src/elements/EFMedia/JitMediaEngine.ts +25 -16
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +10 -1
- package/src/elements/EFTemporal.browsertest.ts +47 -0
- package/src/elements/EFVideo.browsertest.ts +127 -281
- package/src/elements/EFVideo.ts +9 -9
- package/src/elements/TargetController.ts +6 -2
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +3 -8
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +4 -9
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +2 -4
- package/test/recordReplayProxyPlugin.js +46 -31
- package/test/setup.ts +16 -0
- package/test/useAssetMSW.ts +54 -0
- package/test/useMSW.ts +4 -11
- package/types.json +1 -1
- package/dist/elements/MediaController.d.ts +0 -30
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +0 -164
- package/src/elements/MediaController.ts +0 -98
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +0 -22
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +0 -22
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +0 -22
- /package/dist/elements/EFMedia/{BaseMediaEngine.test.d.ts → BaseMediaEngine.browsertest.d.ts} +0 -0
|
@@ -2,8 +2,7 @@ import { Task } from '@lit/task';
|
|
|
2
2
|
import { EFMedia } from './EFMedia.js';
|
|
3
3
|
declare const EFAudio_base: typeof EFMedia;
|
|
4
4
|
export declare class EFAudio extends EFAudio_base {
|
|
5
|
-
|
|
6
|
-
attributeChangedCallback(name: string, old: string | null, value: string | null): void;
|
|
5
|
+
private _propertyHack;
|
|
7
6
|
audioElementRef: import('lit-html/directives/ref.js').Ref<HTMLAudioElement>;
|
|
8
7
|
render(): import('lit-html').TemplateResult<1>;
|
|
9
8
|
frameTask: Task<readonly [import('@lit/task').TaskStatus, import('@lit/task').TaskStatus, import('@lit/task').TaskStatus, import('@lit/task').TaskStatus], void>;
|
package/dist/elements/EFAudio.js
CHANGED
|
@@ -2,12 +2,13 @@ import { EFMedia } from "./EFMedia.js";
|
|
|
2
2
|
import { TWMixin } from "../gui/TWMixin2.js";
|
|
3
3
|
import { Task } from "@lit/task";
|
|
4
4
|
import { html } from "lit";
|
|
5
|
-
import { customElement } from "lit/decorators.js";
|
|
5
|
+
import { customElement, property } from "lit/decorators.js";
|
|
6
6
|
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
7
7
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
8
8
|
let EFAudio = class EFAudio$1 extends TWMixin(EFMedia) {
|
|
9
9
|
constructor(..._args) {
|
|
10
10
|
super(..._args);
|
|
11
|
+
this._propertyHack = false;
|
|
11
12
|
this.audioElementRef = createRef();
|
|
12
13
|
this.frameTask = new Task(this, {
|
|
13
14
|
args: () => [
|
|
@@ -25,14 +26,6 @@ let EFAudio = class EFAudio$1 extends TWMixin(EFMedia) {
|
|
|
25
26
|
}
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
static get observedAttributes() {
|
|
29
|
-
const parentAttributes = super.observedAttributes || [];
|
|
30
|
-
return [...parentAttributes];
|
|
31
|
-
}
|
|
32
|
-
attributeChangedCallback(name, old, value) {
|
|
33
|
-
super.attributeChangedCallback(name, old, value);
|
|
34
|
-
if (name === "asset-id") this.assetId = value;
|
|
35
|
-
}
|
|
36
29
|
render() {
|
|
37
30
|
return html`<audio ${ref(this.audioElementRef)}></audio>`;
|
|
38
31
|
}
|
|
@@ -61,5 +54,9 @@ let EFAudio = class EFAudio$1 extends TWMixin(EFMedia) {
|
|
|
61
54
|
return this.audioBufferTask;
|
|
62
55
|
}
|
|
63
56
|
};
|
|
57
|
+
_decorate([property({
|
|
58
|
+
type: Boolean,
|
|
59
|
+
attribute: "dummy-property"
|
|
60
|
+
})], EFAudio.prototype, "_propertyHack", void 0);
|
|
64
61
|
EFAudio = _decorate([customElement("ef-audio")], EFAudio);
|
|
65
62
|
export { EFAudio };
|
|
@@ -7,9 +7,12 @@ var AssetIdMediaEngine = class AssetIdMediaEngine extends AssetMediaEngine {
|
|
|
7
7
|
return new AssetIdMediaEngine(host, assetId, data, apiHost);
|
|
8
8
|
}
|
|
9
9
|
constructor(host, assetId, data, apiHost) {
|
|
10
|
-
super(host, assetId
|
|
10
|
+
super(host, assetId);
|
|
11
11
|
this.assetId = assetId;
|
|
12
12
|
this.apiHost = apiHost;
|
|
13
|
+
this.data = data;
|
|
14
|
+
const longestFragment = Object.values(this.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
|
|
15
|
+
this.durationMs = longestFragment * 1e3;
|
|
13
16
|
}
|
|
14
17
|
get initSegmentPaths() {
|
|
15
18
|
const paths = {};
|
|
@@ -5,12 +5,11 @@ import { EFMedia } from '../EFMedia';
|
|
|
5
5
|
import { BaseMediaEngine } from './BaseMediaEngine';
|
|
6
6
|
import { MediaRendition } from './shared/MediaTaskUtils';
|
|
7
7
|
export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
8
|
-
host: EFMedia;
|
|
9
8
|
src: string;
|
|
10
9
|
protected data: Record<number, TrackFragmentIndex>;
|
|
11
|
-
static fetch(host: EFMedia, urlGenerator: UrlGenerator, src: string): Promise<AssetMediaEngine>;
|
|
12
10
|
durationMs: number;
|
|
13
|
-
constructor(host: EFMedia, src: string
|
|
11
|
+
constructor(host: EFMedia, src: string);
|
|
12
|
+
static fetch(host: EFMedia, urlGenerator: UrlGenerator, src: string): Promise<AssetMediaEngine>;
|
|
14
13
|
get audioTrackIndex(): import('../../../../assets/src/index.ts').AudioTrackFragmentIndex | undefined;
|
|
15
14
|
get videoTrackIndex(): import('../../../../assets/src/index.ts').VideoTrackFragmentIndex | undefined;
|
|
16
15
|
get videoRendition(): {
|
|
@@ -33,7 +32,7 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
|
|
|
33
32
|
trackId: number | undefined;
|
|
34
33
|
src: string;
|
|
35
34
|
}, signal: AbortSignal): Promise<ArrayBuffer>;
|
|
36
|
-
|
|
35
|
+
fetchMediaSegment(segmentId: number, rendition: {
|
|
37
36
|
trackId: number | undefined;
|
|
38
37
|
src: string;
|
|
39
38
|
}, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { BaseMediaEngine } from "./BaseMediaEngine.js";
|
|
2
2
|
import { convertToScaledTime, roundToMilliseconds } from "./shared/PrecisionUtils.js";
|
|
3
3
|
var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
4
|
+
constructor(host, src) {
|
|
5
|
+
super(host);
|
|
6
|
+
this.data = {};
|
|
7
|
+
this.durationMs = 0;
|
|
8
|
+
this.src = src;
|
|
9
|
+
}
|
|
4
10
|
static async fetch(host, urlGenerator, src) {
|
|
11
|
+
const engine = new AssetMediaEngine(host, src);
|
|
5
12
|
const url = urlGenerator.generateTrackFragmentIndexUrl(src);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
super();
|
|
13
|
-
this.host = host;
|
|
14
|
-
this.src = src;
|
|
15
|
-
this.data = data;
|
|
16
|
-
const longestFragment = Object.values(data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
|
|
17
|
-
this.durationMs = longestFragment * 1e3;
|
|
13
|
+
const data = await engine.fetchManifest(url);
|
|
14
|
+
engine.data = data;
|
|
15
|
+
const longestFragment = Object.values(engine.data).reduce((max, fragment) => Math.max(max, fragment.duration / fragment.timescale), 0);
|
|
16
|
+
engine.durationMs = longestFragment * 1e3;
|
|
17
|
+
if (src.startsWith("/")) engine.src = src.slice(1);
|
|
18
|
+
return engine;
|
|
18
19
|
}
|
|
19
20
|
get audioTrackIndex() {
|
|
20
21
|
return Object.values(this.data).find((track) => track.type === "audio");
|
|
@@ -67,24 +68,30 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
67
68
|
const initSegment = this.data[rendition.trackId]?.initSegment;
|
|
68
69
|
if (!initSegment) throw new Error("Init segment not found");
|
|
69
70
|
const headers = { Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}` };
|
|
70
|
-
return this.
|
|
71
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
71
72
|
}
|
|
72
|
-
async
|
|
73
|
+
async fetchMediaSegment(segmentId, rendition, signal) {
|
|
73
74
|
if (!rendition.trackId) throw new Error("Track ID is required for asset metadata");
|
|
74
75
|
if (segmentId === void 0) throw new Error("Segment ID is not available");
|
|
75
76
|
const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
|
|
76
77
|
const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
|
|
77
78
|
if (!mediaSegment) throw new Error("Media segment not found");
|
|
78
79
|
const headers = { Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}` };
|
|
79
|
-
return this.
|
|
80
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Calculate audio segments for variable-duration segments using track fragment index
|
|
83
84
|
*/
|
|
84
85
|
calculateAudioSegmentRange(fromMs, toMs, rendition, _durationMs) {
|
|
85
|
-
if (fromMs >= toMs || !rendition.trackId)
|
|
86
|
+
if (fromMs >= toMs || !rendition.trackId) {
|
|
87
|
+
console.warn(`calculateAudioSegmentRange: invalid fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify(rendition)}`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
86
90
|
const track = this.data[rendition.trackId];
|
|
87
|
-
if (!track)
|
|
91
|
+
if (!track) {
|
|
92
|
+
console.warn(`calculateAudioSegmentRange: track not found for rendition ${JSON.stringify(rendition)}`);
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
88
95
|
const { timescale, segments } = track;
|
|
89
96
|
const segmentRanges = [];
|
|
90
97
|
for (let i = 0; i < segments.length; i++) {
|
|
@@ -99,6 +106,10 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
99
106
|
endMs: segmentEndMs
|
|
100
107
|
});
|
|
101
108
|
}
|
|
109
|
+
if (segmentRanges.length === 0) console.warn(`calculateAudioSegmentRange: no segments found for fromMs ${fromMs} toMs ${toMs} rendition ${JSON.stringify({
|
|
110
|
+
rendition,
|
|
111
|
+
track
|
|
112
|
+
})}`);
|
|
102
113
|
return segmentRanges;
|
|
103
114
|
}
|
|
104
115
|
computeSegmentId(desiredSeekTimeMs, rendition) {
|
|
@@ -1,28 +1,53 @@
|
|
|
1
|
+
import { RequestDeduplicator } from '../../transcoding/cache/RequestDeduplicator.js';
|
|
1
2
|
import { AudioRendition, SegmentTimeRange, VideoRendition } from '../../transcoding/types';
|
|
3
|
+
import { SizeAwareLRUCache } from '../../utils/LRUCache.js';
|
|
2
4
|
import { EFMedia } from '../EFMedia.js';
|
|
5
|
+
export declare const mediaCache: SizeAwareLRUCache<string>;
|
|
6
|
+
export declare const globalRequestDeduplicator: RequestDeduplicator;
|
|
3
7
|
export declare abstract class BaseMediaEngine {
|
|
4
|
-
|
|
8
|
+
protected host: EFMedia;
|
|
9
|
+
constructor(host: EFMedia);
|
|
5
10
|
abstract get videoRendition(): VideoRendition | undefined;
|
|
6
11
|
abstract get audioRendition(): AudioRendition | undefined;
|
|
7
|
-
abstract get host(): EFMedia;
|
|
8
12
|
getVideoRendition(): VideoRendition;
|
|
9
13
|
getAudioRendition(): AudioRendition;
|
|
10
14
|
/**
|
|
11
15
|
* Generate cache key for segment requests
|
|
12
16
|
*/
|
|
13
17
|
private getSegmentCacheKey;
|
|
18
|
+
/**
|
|
19
|
+
* Unified fetch method with caching and global deduplication
|
|
20
|
+
* All requests (media, manifest, init segments) go through this method
|
|
21
|
+
*/
|
|
22
|
+
protected fetchWithCache(url: string, options: {
|
|
23
|
+
responseType: "arrayBuffer" | "json";
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<any>;
|
|
27
|
+
/**
|
|
28
|
+
* Handles abort logic for a cached request without affecting the underlying fetch
|
|
29
|
+
* This allows multiple instances to share the same cached request while each
|
|
30
|
+
* manages their own abort behavior
|
|
31
|
+
*/
|
|
32
|
+
private handleAbortForCachedRequest;
|
|
33
|
+
fetchMedia(url: string, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
34
|
+
fetchManifest(url: string, signal?: AbortSignal): Promise<any>;
|
|
35
|
+
fetchMediaWithHeaders(url: string, headers: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
36
|
+
fetchMediaCache(url: string, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
37
|
+
fetchManifestCache(url: string, signal?: AbortSignal): Promise<any>;
|
|
38
|
+
fetchMediaCacheWithHeaders(url: string, headers: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
14
39
|
/**
|
|
15
40
|
* Abstract method for actual segment fetching - implemented by subclasses
|
|
16
41
|
*/
|
|
17
|
-
abstract
|
|
42
|
+
abstract fetchMediaSegment(segmentId: number, rendition: {
|
|
18
43
|
trackId: number | undefined;
|
|
19
44
|
src: string;
|
|
20
45
|
}): Promise<ArrayBuffer>;
|
|
21
46
|
/**
|
|
22
47
|
* Fetch media segment with built-in deduplication
|
|
23
|
-
*
|
|
48
|
+
* Now uses global deduplication for all requests
|
|
24
49
|
*/
|
|
25
|
-
|
|
50
|
+
fetchMediaSegmentWithDeduplication(segmentId: number, rendition: {
|
|
26
51
|
trackId: number | undefined;
|
|
27
52
|
src: string;
|
|
28
53
|
}, _signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
@@ -41,12 +66,6 @@ export declare abstract class BaseMediaEngine {
|
|
|
41
66
|
* Cancel all active segment requests (for cleanup)
|
|
42
67
|
*/
|
|
43
68
|
cancelAllSegmentRequests(): void;
|
|
44
|
-
fetchMediaCache(mediaUrl: string): Promise<ArrayBuffer>;
|
|
45
|
-
/**
|
|
46
|
-
* Enhanced caching method that supports custom headers (e.g., Range requests)
|
|
47
|
-
* Cache key includes both URL and headers for proper cache isolation
|
|
48
|
-
*/
|
|
49
|
-
fetchMediaCacheWithHeaders(mediaUrl: string, headers?: Record<string, string>, signal?: AbortSignal): Promise<ArrayBuffer>;
|
|
50
69
|
/**
|
|
51
70
|
* Calculate audio segments needed for a time range
|
|
52
71
|
* Each media engine implements this based on their segment structure
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
|
|
2
2
|
import { SizeAwareLRUCache } from "../../utils/LRUCache.js";
|
|
3
3
|
const mediaCache = new SizeAwareLRUCache(100 * 1024 * 1024);
|
|
4
|
+
const globalRequestDeduplicator = new RequestDeduplicator();
|
|
4
5
|
var BaseMediaEngine = class {
|
|
5
|
-
constructor() {
|
|
6
|
-
this.
|
|
6
|
+
constructor(host) {
|
|
7
|
+
this.host = host;
|
|
7
8
|
}
|
|
8
9
|
getVideoRendition() {
|
|
9
10
|
if (!this.videoRendition) throw new Error("No video rendition available");
|
|
@@ -20,13 +21,86 @@ var BaseMediaEngine = class {
|
|
|
20
21
|
return `${rendition.src}-${rendition.id}-${segmentId}-${rendition.trackId}`;
|
|
21
22
|
}
|
|
22
23
|
/**
|
|
24
|
+
* Unified fetch method with caching and global deduplication
|
|
25
|
+
* All requests (media, manifest, init segments) go through this method
|
|
26
|
+
*/
|
|
27
|
+
async fetchWithCache(url, options) {
|
|
28
|
+
const { responseType, headers, signal } = options;
|
|
29
|
+
const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
|
|
30
|
+
const cached = mediaCache.get(cacheKey);
|
|
31
|
+
if (cached) {
|
|
32
|
+
if (signal) return this.handleAbortForCachedRequest(cached, signal);
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
const promise = globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
36
|
+
try {
|
|
37
|
+
const response = await this.host.fetch(url, { headers });
|
|
38
|
+
if (responseType === "json") return response.json();
|
|
39
|
+
return response.arrayBuffer();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
mediaCache.set(cacheKey, promise);
|
|
46
|
+
promise.catch((error) => {
|
|
47
|
+
if (error instanceof DOMException && error.name === "AbortError") mediaCache.delete(cacheKey);
|
|
48
|
+
});
|
|
49
|
+
if (signal) return this.handleAbortForCachedRequest(promise, signal);
|
|
50
|
+
return promise;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Handles abort logic for a cached request without affecting the underlying fetch
|
|
54
|
+
* This allows multiple instances to share the same cached request while each
|
|
55
|
+
* manages their own abort behavior
|
|
56
|
+
*/
|
|
57
|
+
handleAbortForCachedRequest(promise, signal) {
|
|
58
|
+
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
59
|
+
return Promise.race([promise, new Promise((_, reject) => {
|
|
60
|
+
signal.addEventListener("abort", () => {
|
|
61
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
62
|
+
});
|
|
63
|
+
})]);
|
|
64
|
+
}
|
|
65
|
+
async fetchMedia(url, signal) {
|
|
66
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
67
|
+
return this.fetchWithCache(url, {
|
|
68
|
+
responseType: "arrayBuffer",
|
|
69
|
+
signal
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async fetchManifest(url, signal) {
|
|
73
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
74
|
+
return this.fetchWithCache(url, {
|
|
75
|
+
responseType: "json",
|
|
76
|
+
signal
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async fetchMediaWithHeaders(url, headers, signal) {
|
|
80
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
81
|
+
return this.fetchWithCache(url, {
|
|
82
|
+
responseType: "arrayBuffer",
|
|
83
|
+
headers,
|
|
84
|
+
signal
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async fetchMediaCache(url, signal) {
|
|
88
|
+
return this.fetchMedia(url, signal);
|
|
89
|
+
}
|
|
90
|
+
async fetchManifestCache(url, signal) {
|
|
91
|
+
return this.fetchManifest(url, signal);
|
|
92
|
+
}
|
|
93
|
+
async fetchMediaCacheWithHeaders(url, headers, signal) {
|
|
94
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
23
97
|
* Fetch media segment with built-in deduplication
|
|
24
|
-
*
|
|
98
|
+
* Now uses global deduplication for all requests
|
|
25
99
|
*/
|
|
26
|
-
async
|
|
100
|
+
async fetchMediaSegmentWithDeduplication(segmentId, rendition, _signal) {
|
|
27
101
|
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
28
|
-
return
|
|
29
|
-
return this.
|
|
102
|
+
return globalRequestDeduplicator.executeRequest(cacheKey, async () => {
|
|
103
|
+
return this.fetchMediaSegment(segmentId, rendition);
|
|
30
104
|
});
|
|
31
105
|
}
|
|
32
106
|
/**
|
|
@@ -34,41 +108,19 @@ var BaseMediaEngine = class {
|
|
|
34
108
|
*/
|
|
35
109
|
isSegmentBeingFetched(segmentId, rendition) {
|
|
36
110
|
const cacheKey = this.getSegmentCacheKey(segmentId, rendition);
|
|
37
|
-
return
|
|
111
|
+
return globalRequestDeduplicator.isPending(cacheKey);
|
|
38
112
|
}
|
|
39
113
|
/**
|
|
40
114
|
* Get count of active segment requests (for debugging/monitoring)
|
|
41
115
|
*/
|
|
42
116
|
getActiveSegmentRequestCount() {
|
|
43
|
-
return
|
|
117
|
+
return globalRequestDeduplicator.getPendingCount();
|
|
44
118
|
}
|
|
45
119
|
/**
|
|
46
120
|
* Cancel all active segment requests (for cleanup)
|
|
47
121
|
*/
|
|
48
122
|
cancelAllSegmentRequests() {
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
async fetchMediaCache(mediaUrl) {
|
|
52
|
-
const cached = mediaCache.get(mediaUrl);
|
|
53
|
-
if (cached) return cached;
|
|
54
|
-
const promise = this.host.fetch(mediaUrl).then((response) => response.arrayBuffer());
|
|
55
|
-
mediaCache.set(mediaUrl, promise);
|
|
56
|
-
return promise;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Enhanced caching method that supports custom headers (e.g., Range requests)
|
|
60
|
-
* Cache key includes both URL and headers for proper cache isolation
|
|
61
|
-
*/
|
|
62
|
-
async fetchMediaCacheWithHeaders(mediaUrl, headers, signal) {
|
|
63
|
-
const cacheKey = headers ? `${mediaUrl}:${JSON.stringify(headers)}` : mediaUrl;
|
|
64
|
-
const cached = mediaCache.get(cacheKey);
|
|
65
|
-
if (cached) return cached;
|
|
66
|
-
const promise = this.host.fetch(mediaUrl, {
|
|
67
|
-
headers,
|
|
68
|
-
signal
|
|
69
|
-
}).then((response) => response.arrayBuffer());
|
|
70
|
-
mediaCache.set(cacheKey, promise);
|
|
71
|
-
return promise;
|
|
123
|
+
globalRequestDeduplicator.clear();
|
|
72
124
|
}
|
|
73
125
|
/**
|
|
74
126
|
* Calculate audio segments needed for a time range
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { AudioRendition, MediaEngine, RenditionId, VideoRendition } from '../../transcoding/types';
|
|
2
|
-
import { ManifestResponse } from '../../transcoding/types/index.js';
|
|
3
2
|
import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
|
|
4
3
|
import { EFMedia } from '../EFMedia.js';
|
|
5
4
|
import { BaseMediaEngine } from './BaseMediaEngine';
|
|
6
5
|
export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
7
|
-
host: EFMedia;
|
|
8
6
|
private urlGenerator;
|
|
9
7
|
private data;
|
|
10
8
|
static fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string): Promise<JitMediaEngine>;
|
|
11
|
-
constructor(host: EFMedia, urlGenerator: UrlGenerator
|
|
9
|
+
constructor(host: EFMedia, urlGenerator: UrlGenerator);
|
|
12
10
|
get durationMs(): number;
|
|
13
11
|
get src(): string;
|
|
14
12
|
get audioRendition(): AudioRendition | undefined;
|
|
@@ -22,7 +20,7 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
|
|
|
22
20
|
trackId: number | undefined;
|
|
23
21
|
src: string;
|
|
24
22
|
}, signal: AbortSignal): Promise<ArrayBuffer>;
|
|
25
|
-
|
|
23
|
+
fetchMediaSegment(segmentId: number, rendition: {
|
|
26
24
|
id?: RenditionId;
|
|
27
25
|
trackId: number | undefined;
|
|
28
26
|
src: string;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { BaseMediaEngine } from "./BaseMediaEngine.js";
|
|
2
2
|
var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
3
3
|
static async fetch(host, urlGenerator, url) {
|
|
4
|
-
const
|
|
5
|
-
const data = await
|
|
6
|
-
|
|
4
|
+
const engine = new JitMediaEngine(host, urlGenerator);
|
|
5
|
+
const data = await engine.fetchManifest(url);
|
|
6
|
+
engine.data = data;
|
|
7
|
+
return engine;
|
|
7
8
|
}
|
|
8
|
-
constructor(host, urlGenerator
|
|
9
|
-
super();
|
|
10
|
-
this.
|
|
9
|
+
constructor(host, urlGenerator) {
|
|
10
|
+
super(host);
|
|
11
|
+
this.data = {};
|
|
11
12
|
this.urlGenerator = urlGenerator;
|
|
12
|
-
this.data = data;
|
|
13
13
|
}
|
|
14
14
|
get durationMs() {
|
|
15
15
|
return this.data.durationMs;
|
|
@@ -18,6 +18,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
18
18
|
return this.data.sourceUrl;
|
|
19
19
|
}
|
|
20
20
|
get audioRendition() {
|
|
21
|
+
if (!this.data.audioRenditions || this.data.audioRenditions.length === 0) return void 0;
|
|
21
22
|
const rendition = this.data.audioRenditions[0];
|
|
22
23
|
if (!rendition) return void 0;
|
|
23
24
|
return {
|
|
@@ -29,6 +30,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
get videoRendition() {
|
|
33
|
+
if (!this.data.videoRenditions || this.data.videoRenditions.length === 0) return void 0;
|
|
32
34
|
const rendition = this.data.videoRenditions[0];
|
|
33
35
|
if (!rendition) return void 0;
|
|
34
36
|
return {
|
|
@@ -45,14 +47,12 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
45
47
|
async fetchInitSegment(rendition, signal) {
|
|
46
48
|
if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
|
|
47
49
|
const url = this.urlGenerator.generateSegmentUrl("init", rendition.id, this);
|
|
48
|
-
|
|
49
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
50
|
-
return arrayBuffer;
|
|
50
|
+
return this.fetchMedia(url, signal);
|
|
51
51
|
}
|
|
52
|
-
async
|
|
52
|
+
async fetchMediaSegment(segmentId, rendition) {
|
|
53
53
|
if (!rendition.id) throw new Error("Rendition ID is required for JIT metadata");
|
|
54
54
|
const url = this.urlGenerator.generateSegmentUrl(segmentId, rendition.id, this);
|
|
55
|
-
return this.
|
|
55
|
+
return this.fetchMedia(url);
|
|
56
56
|
}
|
|
57
57
|
computeSegmentId(desiredSeekTimeMs, rendition) {
|
|
58
58
|
if (desiredSeekTimeMs > this.durationMs) return void 0;
|
|
@@ -4,8 +4,13 @@ const makeAudioSeekTask = (host) => {
|
|
|
4
4
|
return new Task(host, {
|
|
5
5
|
args: () => [host.desiredSeekTimeMs, host.audioInputTask.value],
|
|
6
6
|
onError: (error) => {
|
|
7
|
-
if (error instanceof IgnorableError)
|
|
8
|
-
|
|
7
|
+
if (error instanceof IgnorableError) {
|
|
8
|
+
console.info("audioSeekTask aborted");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (error instanceof DOMException) console.error(`audioSeekTask error: ${error.message} ${error.name} ${error.code}`);
|
|
12
|
+
else if (error instanceof Error) console.error(`audioSeekTask error ${error.name}: ${error.message}`);
|
|
13
|
+
else console.error("audioSeekTask unknown error", error);
|
|
9
14
|
},
|
|
10
15
|
onComplete: (_value) => {},
|
|
11
16
|
task: async ([targetSeekTimeMs], { signal }) => {
|
|
@@ -11,7 +11,6 @@ interface LoadingState {
|
|
|
11
11
|
}
|
|
12
12
|
declare const EFVideo_base: typeof EFMedia;
|
|
13
13
|
export declare class EFVideo extends EFVideo_base {
|
|
14
|
-
static get observedAttributes(): string[];
|
|
15
14
|
static styles: import('lit').CSSResult[];
|
|
16
15
|
canvasRef: import('lit-html/directives/ref').Ref<HTMLCanvasElement>;
|
|
17
16
|
/**
|
package/dist/elements/EFVideo.js
CHANGED
|
@@ -15,15 +15,6 @@ import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
|
15
15
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
16
16
|
const log = debug("ef:elements:EFVideo");
|
|
17
17
|
let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
18
|
-
static get observedAttributes() {
|
|
19
|
-
const parentAttributes = EFMedia.observedAttributes || [];
|
|
20
|
-
return [
|
|
21
|
-
...parentAttributes,
|
|
22
|
-
"video-buffer-duration",
|
|
23
|
-
"max-video-buffer-fetches",
|
|
24
|
-
"enable-video-buffering"
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
18
|
static {
|
|
28
19
|
this.styles = [css`
|
|
29
20
|
:host {
|
|
@@ -21,7 +21,8 @@ var TargetRegistry = class {
|
|
|
21
21
|
this.idMap.set(id, target);
|
|
22
22
|
for (const callback of this.callbacks.get(id) ?? []) callback(target);
|
|
23
23
|
}
|
|
24
|
-
unregister(id) {
|
|
24
|
+
unregister(id, target) {
|
|
25
|
+
if (this.idMap.get(id) !== target) return;
|
|
25
26
|
for (const callback of this.callbacks.get(id) ?? []) callback(void 0);
|
|
26
27
|
this.idMap.delete(id);
|
|
27
28
|
this.callbacks.delete(id);
|
|
@@ -46,7 +47,7 @@ const EFTargetable = (superClass) => {
|
|
|
46
47
|
updateRegistry(oldValue, newValue) {
|
|
47
48
|
if (!this.#registry) return;
|
|
48
49
|
if (oldValue === newValue) return;
|
|
49
|
-
if (oldValue) this.#registry.unregister(oldValue);
|
|
50
|
+
if (oldValue) this.#registry.unregister(oldValue, this);
|
|
50
51
|
if (newValue) this.#registry.register(newValue, this);
|
|
51
52
|
}
|
|
52
53
|
connectedCallback() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.19-beta.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"license": "UNLICENSED",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@bramus/style-observer": "^1.3.0",
|
|
30
|
-
"@editframe/assets": "0.18.
|
|
30
|
+
"@editframe/assets": "0.18.19-beta.0",
|
|
31
31
|
"@lit/context": "^1.1.2",
|
|
32
32
|
"@lit/task": "^1.0.1",
|
|
33
33
|
"d3": "^7.9.0",
|
package/src/elements/EFAudio.ts
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
2
|
import { html } from "lit";
|
|
3
|
-
import { customElement } from "lit/decorators.js";
|
|
3
|
+
import { customElement, property } from "lit/decorators.js";
|
|
4
4
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
5
|
import { TWMixin } from "../gui/TWMixin.js";
|
|
6
6
|
import { EFMedia } from "./EFMedia.js";
|
|
7
7
|
|
|
8
8
|
@customElement("ef-audio")
|
|
9
9
|
export class EFAudio extends TWMixin(EFMedia) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
attributeChangedCallback(
|
|
17
|
-
name: string,
|
|
18
|
-
old: string | null,
|
|
19
|
-
value: string | null,
|
|
20
|
-
) {
|
|
21
|
-
super.attributeChangedCallback(name, old, value);
|
|
22
|
-
|
|
23
|
-
// Explicitly handle asset-id attribute to property conversion
|
|
24
|
-
// EFVideo works without this fix, but EFAudio requires it due to some fundamental difference
|
|
25
|
-
if (name === "asset-id") {
|
|
26
|
-
this.assetId = value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
10
|
+
// HACK: This dummy property is needed to activate Lit's property processing system
|
|
11
|
+
// Without it, inherited properties from EFMedia don't work correctly
|
|
12
|
+
// TODO: Remove this as soon as we have an audio-specific property that needs @property
|
|
13
|
+
@property({ type: Boolean, attribute: "dummy-property" })
|
|
14
|
+
// @ts-expect-error - This is a hack to activate Lit's property processing system
|
|
15
|
+
private _propertyHack = false;
|
|
29
16
|
|
|
30
17
|
audioElementRef = createRef<HTMLAudioElement>();
|
|
31
18
|
|
|
@@ -27,7 +27,16 @@ export class AssetIdMediaEngine
|
|
|
27
27
|
private apiHost: string,
|
|
28
28
|
) {
|
|
29
29
|
// Pass assetId as src to parent constructor for compatibility
|
|
30
|
-
super(host, assetId
|
|
30
|
+
super(host, assetId);
|
|
31
|
+
// Initialize data after parent constructor
|
|
32
|
+
this.data = data;
|
|
33
|
+
|
|
34
|
+
// Calculate duration from the data
|
|
35
|
+
const longestFragment = Object.values(this.data).reduce(
|
|
36
|
+
(max, fragment) => Math.max(max, fragment.duration / fragment.timescale),
|
|
37
|
+
0,
|
|
38
|
+
);
|
|
39
|
+
this.durationMs = longestFragment * 1000;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
// Override URL-building methods to use API endpoints instead of file paths
|