@editframe/elements 0.19.4-beta.0 → 0.20.0-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/ContextProxiesController.d.ts +40 -0
- package/dist/elements/ContextProxiesController.js +69 -0
- package/dist/elements/EFCaptions.d.ts +45 -6
- package/dist/elements/EFCaptions.js +220 -26
- package/dist/elements/EFImage.js +4 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
- package/dist/elements/EFMedia.js +25 -1
- package/dist/elements/EFSurface.browsertest.d.ts +0 -0
- package/dist/elements/EFSurface.d.ts +30 -0
- package/dist/elements/EFSurface.js +96 -0
- package/dist/elements/EFTemporal.js +7 -6
- package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
- package/dist/elements/EFThumbnailStrip.d.ts +86 -0
- package/dist/elements/EFThumbnailStrip.js +490 -0
- package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
- package/dist/elements/EFTimegroup.d.ts +6 -1
- package/dist/elements/EFTimegroup.js +46 -10
- package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
- package/dist/elements/updateAnimations.d.ts +5 -0
- package/dist/elements/updateAnimations.js +37 -13
- package/dist/getRenderInfo.js +1 -1
- package/dist/gui/ContextMixin.js +27 -14
- package/dist/gui/EFControls.browsertest.d.ts +0 -0
- package/dist/gui/EFControls.d.ts +38 -0
- package/dist/gui/EFControls.js +51 -0
- package/dist/gui/EFFilmstrip.d.ts +40 -1
- package/dist/gui/EFFilmstrip.js +240 -3
- package/dist/gui/EFPreview.js +2 -1
- package/dist/gui/EFScrubber.d.ts +6 -5
- package/dist/gui/EFScrubber.js +31 -21
- package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
- package/dist/gui/EFTimeDisplay.d.ts +2 -6
- package/dist/gui/EFTimeDisplay.js +13 -23
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/currentTimeContext.d.ts +3 -0
- package/dist/gui/currentTimeContext.js +3 -0
- package/dist/gui/durationContext.d.ts +3 -0
- package/dist/gui/durationContext.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/style.css +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/dist/utils/LRUCache.d.ts +46 -0
- package/dist/utils/LRUCache.js +382 -1
- package/dist/utils/LRUCache.test.d.ts +1 -0
- package/package.json +2 -2
- package/src/elements/ContextProxiesController.ts +123 -0
- package/src/elements/EFCaptions.browsertest.ts +1820 -0
- package/src/elements/EFCaptions.ts +373 -36
- package/src/elements/EFImage.ts +4 -1
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
- package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
- package/src/elements/EFMedia.ts +38 -1
- package/src/elements/EFSurface.browsertest.ts +155 -0
- package/src/elements/EFSurface.ts +141 -0
- package/src/elements/EFTemporal.ts +14 -8
- package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
- package/src/elements/EFThumbnailStrip.ts +905 -0
- package/src/elements/EFTimegroup.browsertest.ts +56 -7
- package/src/elements/EFTimegroup.ts +70 -11
- package/src/elements/updateAnimations.browsertest.ts +333 -11
- package/src/elements/updateAnimations.ts +68 -19
- package/src/gui/ContextMixin.browsertest.ts +0 -25
- package/src/gui/ContextMixin.ts +44 -20
- package/src/gui/EFControls.browsertest.ts +175 -0
- package/src/gui/EFControls.ts +84 -0
- package/src/gui/EFFilmstrip.ts +323 -4
- package/src/gui/EFPreview.ts +2 -1
- package/src/gui/EFScrubber.ts +29 -25
- package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
- package/src/gui/EFTimeDisplay.ts +12 -40
- package/src/gui/currentTimeContext.ts +5 -0
- package/src/gui/durationContext.ts +3 -0
- package/src/transcoding/types/index.ts +13 -0
- package/src/utils/LRUCache.test.ts +272 -0
- package/src/utils/LRUCache.ts +543 -0
- package/types.json +1 -1
- package/dist/transcoding/cache/CacheManager.d.ts +0 -73
- package/src/transcoding/cache/CacheManager.ts +0 -208
|
@@ -157,5 +157,16 @@ var AssetMediaEngine = class AssetMediaEngine extends BaseMediaEngine {
|
|
|
157
157
|
maxAudioBufferFetches: 1
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
+
convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
|
|
161
|
+
{
|
|
162
|
+
if (!rendition.trackId) throw new Error("Track ID is required for asset metadata");
|
|
163
|
+
const trackData = this.data[rendition.trackId];
|
|
164
|
+
if (!trackData) throw new Error("Track not found");
|
|
165
|
+
const segment = trackData.segments?.[segmentId];
|
|
166
|
+
if (!segment) throw new Error("Segment not found");
|
|
167
|
+
const segmentStartMs = segment.cts / trackData.timescale * 1e3;
|
|
168
|
+
return globalTimestamps.map((globalMs) => (globalMs - segmentStartMs) / 1e3);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
160
171
|
};
|
|
161
172
|
export { AssetMediaEngine };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { RequestDeduplicator } from '../../transcoding/cache/RequestDeduplicator.js';
|
|
2
|
-
import { AudioRendition, SegmentTimeRange, VideoRendition } from '../../transcoding/types';
|
|
2
|
+
import { AudioRendition, SegmentTimeRange, ThumbnailResult, VideoRendition } from '../../transcoding/types';
|
|
3
3
|
import { SizeAwareLRUCache } from '../../utils/LRUCache.js';
|
|
4
4
|
import { EFMedia } from '../EFMedia.js';
|
|
5
|
+
import { MediaRendition } from './shared/MediaTaskUtils.js';
|
|
5
6
|
export declare const mediaCache: SizeAwareLRUCache<string>;
|
|
6
7
|
export declare const globalRequestDeduplicator: RequestDeduplicator;
|
|
7
8
|
export declare abstract class BaseMediaEngine {
|
|
@@ -43,6 +44,11 @@ export declare abstract class BaseMediaEngine {
|
|
|
43
44
|
trackId: number | undefined;
|
|
44
45
|
src: string;
|
|
45
46
|
}): Promise<ArrayBuffer>;
|
|
47
|
+
abstract fetchInitSegment(rendition: {
|
|
48
|
+
trackId: number | undefined;
|
|
49
|
+
src: string;
|
|
50
|
+
}, signal: AbortSignal): Promise<ArrayBuffer>;
|
|
51
|
+
abstract computeSegmentId(desiredSeekTimeMs: number, rendition: MediaRendition): number | undefined;
|
|
46
52
|
/**
|
|
47
53
|
* Fetch media segment with built-in deduplication
|
|
48
54
|
* Now uses global deduplication for all requests
|
|
@@ -80,4 +86,10 @@ export declare abstract class BaseMediaEngine {
|
|
|
80
86
|
* Get cached segment IDs from a list for a given rendition
|
|
81
87
|
*/
|
|
82
88
|
getCachedSegments(segmentIds: number[], rendition: AudioRendition | VideoRendition): Set<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
91
|
+
* Default implementation provides helpful error information
|
|
92
|
+
*/
|
|
93
|
+
extractThumbnails(timestamps: number[]): Promise<(ThumbnailResult | null)[]>;
|
|
94
|
+
abstract convertToSegmentRelativeTimestamps(globalTimestamps: number[], segmentId: number, rendition: VideoRendition): number[];
|
|
83
95
|
}
|
|
@@ -190,5 +190,14 @@ var BaseMediaEngine = class {
|
|
|
190
190
|
getCachedSegments(segmentIds, rendition) {
|
|
191
191
|
return new Set(segmentIds.filter((id) => this.isSegmentCached(id, rendition)));
|
|
192
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
195
|
+
* Default implementation provides helpful error information
|
|
196
|
+
*/
|
|
197
|
+
async extractThumbnails(timestamps) {
|
|
198
|
+
const engineName = this.constructor.name;
|
|
199
|
+
console.warn(`${engineName}: extractThumbnails not properly implemented. This MediaEngine type does not support thumbnail generation. Supported engines: JitMediaEngine. Requested ${timestamps.length} thumbnail${timestamps.length === 1 ? "" : "s"}.`);
|
|
200
|
+
return timestamps.map(() => null);
|
|
201
|
+
}
|
|
193
202
|
};
|
|
194
203
|
export { BaseMediaEngine };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AudioRendition, MediaEngine, RenditionId, VideoRendition } from '../../transcoding/types';
|
|
1
|
+
import { AudioRendition, MediaEngine, RenditionId, ThumbnailResult, VideoRendition } from '../../transcoding/types';
|
|
2
2
|
import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
|
|
3
3
|
import { EFMedia } from '../EFMedia.js';
|
|
4
4
|
import { BaseMediaEngine } from './BaseMediaEngine';
|
|
5
5
|
export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
6
6
|
private urlGenerator;
|
|
7
7
|
private data;
|
|
8
|
+
private thumbnailExtractor;
|
|
8
9
|
static fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string): Promise<JitMediaEngine>;
|
|
9
10
|
constructor(host: EFMedia, urlGenerator: UrlGenerator);
|
|
10
11
|
get durationMs(): number;
|
|
@@ -37,4 +38,9 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
|
|
|
37
38
|
maxVideoBufferFetches: number;
|
|
38
39
|
maxAudioBufferFetches: number;
|
|
39
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Extract thumbnail canvases using same rendition priority as video playback for frame alignment
|
|
43
|
+
*/
|
|
44
|
+
extractThumbnails(timestamps: number[]): Promise<(ThumbnailResult | null)[]>;
|
|
45
|
+
convertToSegmentRelativeTimestamps(globalTimestamps: number[], _segmentId: number, _rendition: VideoRendition): number[];
|
|
40
46
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseMediaEngine } from "./BaseMediaEngine.js";
|
|
2
|
+
import { ThumbnailExtractor } from "./shared/ThumbnailExtractor.js";
|
|
2
3
|
var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
3
4
|
static async fetch(host, urlGenerator, url) {
|
|
4
5
|
const engine = new JitMediaEngine(host, urlGenerator);
|
|
@@ -10,6 +11,7 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
10
11
|
super(host);
|
|
11
12
|
this.data = {};
|
|
12
13
|
this.urlGenerator = urlGenerator;
|
|
14
|
+
this.thumbnailExtractor = new ThumbnailExtractor(this);
|
|
13
15
|
}
|
|
14
16
|
get durationMs() {
|
|
15
17
|
return this.data.durationMs;
|
|
@@ -101,5 +103,27 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
101
103
|
maxAudioBufferFetches: 3
|
|
102
104
|
};
|
|
103
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Extract thumbnail canvases using same rendition priority as video playback for frame alignment
|
|
108
|
+
*/
|
|
109
|
+
async extractThumbnails(timestamps) {
|
|
110
|
+
let rendition;
|
|
111
|
+
try {
|
|
112
|
+
const mainRendition = this.getVideoRendition();
|
|
113
|
+
if (mainRendition) rendition = mainRendition;
|
|
114
|
+
else {
|
|
115
|
+
const scrubRendition = this.getScrubVideoRendition();
|
|
116
|
+
if (scrubRendition) rendition = scrubRendition;
|
|
117
|
+
else throw new Error("No video rendition available");
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn("JitMediaEngine: No video rendition available for thumbnails", error);
|
|
121
|
+
return timestamps.map(() => null);
|
|
122
|
+
}
|
|
123
|
+
return this.thumbnailExtractor.extractThumbnails(timestamps, rendition, this.durationMs);
|
|
124
|
+
}
|
|
125
|
+
convertToSegmentRelativeTimestamps(globalTimestamps, _segmentId, _rendition) {
|
|
126
|
+
return globalTimestamps.map((timestamp) => timestamp / 1e3);
|
|
127
|
+
}
|
|
104
128
|
};
|
|
105
129
|
export { JitMediaEngine };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Input } from 'mediabunny';
|
|
2
|
+
/**
|
|
3
|
+
* Global cache for MediaBunny Input instances
|
|
4
|
+
* Shared across all MediaEngine instances to prevent duplicate decoding
|
|
5
|
+
* of the same segment data
|
|
6
|
+
*/
|
|
7
|
+
declare class GlobalInputCache {
|
|
8
|
+
private cache;
|
|
9
|
+
/**
|
|
10
|
+
* Generate standardized cache key for Input objects
|
|
11
|
+
* Format: "input:{src}:{segmentId}:{renditionId}"
|
|
12
|
+
*/
|
|
13
|
+
private generateKey;
|
|
14
|
+
/**
|
|
15
|
+
* Get cached Input object
|
|
16
|
+
*/
|
|
17
|
+
get(src: string, segmentId: number, renditionId?: string): Input | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Cache Input object
|
|
20
|
+
*/
|
|
21
|
+
set(src: string, segmentId: number, input: Input, renditionId?: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Check if Input is cached
|
|
24
|
+
*/
|
|
25
|
+
has(src: string, segmentId: number, renditionId?: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Clear all cached Input objects
|
|
28
|
+
*/
|
|
29
|
+
clear(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Get cache statistics for debugging
|
|
32
|
+
*/
|
|
33
|
+
getStats(): {
|
|
34
|
+
size: number;
|
|
35
|
+
cachedKeys: unknown[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare const globalInputCache: GlobalInputCache;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { LRUCache } from "../../../utils/LRUCache.js";
|
|
2
|
+
/**
|
|
3
|
+
* Global cache for MediaBunny Input instances
|
|
4
|
+
* Shared across all MediaEngine instances to prevent duplicate decoding
|
|
5
|
+
* of the same segment data
|
|
6
|
+
*/
|
|
7
|
+
var GlobalInputCache = class {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.cache = new LRUCache(50);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generate standardized cache key for Input objects
|
|
13
|
+
* Format: "input:{src}:{segmentId}:{renditionId}"
|
|
14
|
+
*/
|
|
15
|
+
generateKey(src, segmentId, renditionId) {
|
|
16
|
+
return `input:${src}:${segmentId}:${renditionId || "default"}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get cached Input object
|
|
20
|
+
*/
|
|
21
|
+
get(src, segmentId, renditionId) {
|
|
22
|
+
const key = this.generateKey(src, segmentId, renditionId);
|
|
23
|
+
return this.cache.get(key);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Cache Input object
|
|
27
|
+
*/
|
|
28
|
+
set(src, segmentId, input, renditionId) {
|
|
29
|
+
const key = this.generateKey(src, segmentId, renditionId);
|
|
30
|
+
this.cache.set(key, input);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if Input is cached
|
|
34
|
+
*/
|
|
35
|
+
has(src, segmentId, renditionId) {
|
|
36
|
+
const key = this.generateKey(src, segmentId, renditionId);
|
|
37
|
+
return this.cache.has(key);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clear all cached Input objects
|
|
41
|
+
*/
|
|
42
|
+
clear() {
|
|
43
|
+
this.cache.clear();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Get cache statistics for debugging
|
|
47
|
+
*/
|
|
48
|
+
getStats() {
|
|
49
|
+
return {
|
|
50
|
+
size: this.cache.size,
|
|
51
|
+
cachedKeys: Array.from(this.cache.cache.keys())
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const globalInputCache = new GlobalInputCache();
|
|
56
|
+
globalThis.debugInputCache = globalInputCache;
|
|
57
|
+
export { globalInputCache };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ThumbnailResult, VideoRendition } from '../../../transcoding/types/index.js';
|
|
2
|
+
import { BaseMediaEngine } from '../BaseMediaEngine.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared thumbnail extraction logic for all MediaEngine implementations
|
|
5
|
+
* Eliminates code duplication and provides consistent behavior
|
|
6
|
+
*/
|
|
7
|
+
export declare class ThumbnailExtractor {
|
|
8
|
+
private mediaEngine;
|
|
9
|
+
constructor(mediaEngine: BaseMediaEngine);
|
|
10
|
+
/**
|
|
11
|
+
* Extract thumbnails at multiple timestamps efficiently using segment batching
|
|
12
|
+
*/
|
|
13
|
+
extractThumbnails(timestamps: number[], rendition: VideoRendition, durationMs: number): Promise<(ThumbnailResult | null)[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Group timestamps by segment ID for efficient batch processing
|
|
16
|
+
*/
|
|
17
|
+
private groupTimestampsBySegment;
|
|
18
|
+
/**
|
|
19
|
+
* Extract thumbnails for a specific segment using CanvasSink
|
|
20
|
+
*/
|
|
21
|
+
private extractSegmentThumbnails;
|
|
22
|
+
/**
|
|
23
|
+
* Convert global timestamps to segment-relative timestamps for mediabunny
|
|
24
|
+
* This is where the main difference between JIT and Asset engines lies
|
|
25
|
+
*/
|
|
26
|
+
private convertToSegmentRelativeTimestamps;
|
|
27
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { globalInputCache } from "./GlobalInputCache.js";
|
|
2
|
+
import { ALL_FORMATS, BlobSource, CanvasSink, Input } from "mediabunny";
|
|
3
|
+
/**
|
|
4
|
+
* Shared thumbnail extraction logic for all MediaEngine implementations
|
|
5
|
+
* Eliminates code duplication and provides consistent behavior
|
|
6
|
+
*/
|
|
7
|
+
var ThumbnailExtractor = class {
|
|
8
|
+
constructor(mediaEngine) {
|
|
9
|
+
this.mediaEngine = mediaEngine;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extract thumbnails at multiple timestamps efficiently using segment batching
|
|
13
|
+
*/
|
|
14
|
+
async extractThumbnails(timestamps, rendition, durationMs) {
|
|
15
|
+
if (timestamps.length === 0) return [];
|
|
16
|
+
const validTimestamps = timestamps.filter((timeMs) => timeMs >= 0 && timeMs <= durationMs);
|
|
17
|
+
if (validTimestamps.length === 0) {
|
|
18
|
+
console.warn(`ThumbnailExtractor: All timestamps out of bounds (0-${durationMs}ms)`);
|
|
19
|
+
return timestamps.map(() => null);
|
|
20
|
+
}
|
|
21
|
+
const segmentGroups = this.groupTimestampsBySegment(validTimestamps, rendition);
|
|
22
|
+
const results = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const [segmentId, segmentTimestamps] of segmentGroups) try {
|
|
24
|
+
const segmentResults = await this.extractSegmentThumbnails(segmentId, segmentTimestamps, rendition);
|
|
25
|
+
for (const [timestamp, thumbnail] of segmentResults) results.set(timestamp, thumbnail);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(`ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`, error);
|
|
28
|
+
for (const timestamp of segmentTimestamps) results.set(timestamp, null);
|
|
29
|
+
}
|
|
30
|
+
return timestamps.map((t) => {
|
|
31
|
+
if (t < 0 || t > durationMs) return null;
|
|
32
|
+
return results.get(t) || null;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Group timestamps by segment ID for efficient batch processing
|
|
37
|
+
*/
|
|
38
|
+
groupTimestampsBySegment(timestamps, rendition) {
|
|
39
|
+
const segmentGroups = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const timeMs of timestamps) try {
|
|
41
|
+
const segmentId = this.mediaEngine.computeSegmentId(timeMs, rendition);
|
|
42
|
+
if (segmentId !== void 0) {
|
|
43
|
+
if (!segmentGroups.has(segmentId)) segmentGroups.set(segmentId, []);
|
|
44
|
+
const segmentGroup = segmentGroups.get(segmentId) ?? [];
|
|
45
|
+
if (!segmentGroup) segmentGroups.set(segmentId, []);
|
|
46
|
+
segmentGroup.push(timeMs);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn(`ThumbnailExtractor: Could not compute segment for timestamp ${timeMs}:`, error);
|
|
50
|
+
}
|
|
51
|
+
return segmentGroups;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract thumbnails for a specific segment using CanvasSink
|
|
55
|
+
*/
|
|
56
|
+
async extractSegmentThumbnails(segmentId, timestamps, rendition) {
|
|
57
|
+
const results = /* @__PURE__ */ new Map();
|
|
58
|
+
try {
|
|
59
|
+
const abortController = new AbortController();
|
|
60
|
+
const [initSegment, mediaSegment] = await Promise.all([this.mediaEngine.fetchInitSegment(rendition, abortController.signal), this.mediaEngine.fetchMediaSegment(segmentId, rendition)]);
|
|
61
|
+
const segmentBlob = new Blob([initSegment, mediaSegment]);
|
|
62
|
+
let input = globalInputCache.get(rendition.src, segmentId, rendition.id);
|
|
63
|
+
if (!input) {
|
|
64
|
+
input = new Input({
|
|
65
|
+
formats: ALL_FORMATS,
|
|
66
|
+
source: new BlobSource(segmentBlob)
|
|
67
|
+
});
|
|
68
|
+
globalInputCache.set(rendition.src, segmentId, input, rendition.id);
|
|
69
|
+
}
|
|
70
|
+
const videoTrack = await input.getPrimaryVideoTrack();
|
|
71
|
+
if (!videoTrack) {
|
|
72
|
+
for (const timestamp of timestamps) results.set(timestamp, null);
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
const sink = new CanvasSink(videoTrack);
|
|
76
|
+
const relativeTimestamps = this.convertToSegmentRelativeTimestamps(timestamps, segmentId, rendition);
|
|
77
|
+
const timestampResults = [];
|
|
78
|
+
for await (const result of sink.canvasesAtTimestamps(relativeTimestamps)) timestampResults.push(result);
|
|
79
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
80
|
+
const globalTimestamp = timestamps[i];
|
|
81
|
+
if (globalTimestamp === void 0) continue;
|
|
82
|
+
const result = timestampResults[i];
|
|
83
|
+
if (result?.canvas) {
|
|
84
|
+
const canvas = result.canvas;
|
|
85
|
+
if (canvas instanceof HTMLCanvasElement || canvas instanceof OffscreenCanvas) results.set(globalTimestamp, {
|
|
86
|
+
timestamp: globalTimestamp,
|
|
87
|
+
thumbnail: canvas
|
|
88
|
+
});
|
|
89
|
+
else results.set(globalTimestamp, null);
|
|
90
|
+
} else results.set(globalTimestamp, null);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`ThumbnailExtractor: Failed to extract thumbnails for segment ${segmentId}:`, error);
|
|
94
|
+
for (const timestamp of timestamps) results.set(timestamp, null);
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Convert global timestamps to segment-relative timestamps for mediabunny
|
|
100
|
+
* This is where the main difference between JIT and Asset engines lies
|
|
101
|
+
*/
|
|
102
|
+
convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition) {
|
|
103
|
+
return this.mediaEngine.convertToSegmentRelativeTimestamps(globalTimestamps, segmentId, rendition);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
export { ThumbnailExtractor };
|
package/dist/elements/EFMedia.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isContextMixin } from "../gui/ContextMixin.js";
|
|
1
2
|
import { UrlGenerator } from "../transcoding/utils/UrlGenerator.js";
|
|
2
3
|
import { makeMediaEngineTask } from "./EFMedia/tasks/makeMediaEngineTask.js";
|
|
3
4
|
import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.js";
|
|
@@ -64,7 +65,9 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
|
|
|
64
65
|
"asset-id",
|
|
65
66
|
"audio-buffer-duration",
|
|
66
67
|
"max-audio-buffer-fetches",
|
|
67
|
-
"enable-audio-buffering"
|
|
68
|
+
"enable-audio-buffering",
|
|
69
|
+
"sourcein",
|
|
70
|
+
"sourceout"
|
|
68
71
|
];
|
|
69
72
|
}
|
|
70
73
|
static {
|
|
@@ -105,6 +108,27 @@ var EFMedia = class extends EFTargetable(EFSourceMixin(EFTemporal(FetchMixin(Lit
|
|
|
105
108
|
const newCurrentSourceTimeMs = this.currentSourceTimeMs;
|
|
106
109
|
if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) this.executeSeek(newCurrentSourceTimeMs);
|
|
107
110
|
if (changedProperties.has("ownCurrentTimeMs")) this.executeSeek(this.currentSourceTimeMs);
|
|
111
|
+
const durationAffectingProps = [
|
|
112
|
+
"_trimStartMs",
|
|
113
|
+
"_trimEndMs",
|
|
114
|
+
"_sourceInMs",
|
|
115
|
+
"_sourceOutMs"
|
|
116
|
+
];
|
|
117
|
+
const hasDurationChange = durationAffectingProps.some((prop) => changedProperties.has(prop));
|
|
118
|
+
if (hasDurationChange) {
|
|
119
|
+
if (this.parentTimegroup) {
|
|
120
|
+
this.parentTimegroup.requestUpdate("durationMs");
|
|
121
|
+
this.parentTimegroup.requestUpdate("currentTime");
|
|
122
|
+
let parent = this.parentNode;
|
|
123
|
+
while (parent) {
|
|
124
|
+
if (isContextMixin(parent)) {
|
|
125
|
+
parent.dispatchEvent(new CustomEvent("child-duration-changed", { detail: { source: this } }));
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
parent = parent.parentNode;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
108
132
|
}
|
|
109
133
|
get hasOwnDuration() {
|
|
110
134
|
return true;
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Task } from '@lit/task';
|
|
2
|
+
import { LitElement } from 'lit';
|
|
3
|
+
import { ContextMixinInterface } from '../gui/ContextMixin.ts';
|
|
4
|
+
export declare class EFSurface extends LitElement {
|
|
5
|
+
#private;
|
|
6
|
+
static styles: import('lit').CSSResult[];
|
|
7
|
+
canvasRef: import('lit-html/directives/ref').Ref<HTMLCanvasElement>;
|
|
8
|
+
targetElement: ContextMixinInterface | null;
|
|
9
|
+
target: string;
|
|
10
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
11
|
+
get rootTimegroup(): any;
|
|
12
|
+
get currentTimeMs(): number;
|
|
13
|
+
get durationMs(): number;
|
|
14
|
+
get startTimeMs(): number;
|
|
15
|
+
get endTimeMs(): number;
|
|
16
|
+
/**
|
|
17
|
+
* Minimal integration with EFTimegroup's frame scheduling:
|
|
18
|
+
* - Waits for the target video element's frameTask to complete (ensuring it painted)
|
|
19
|
+
* - Copies the target's canvas into this element's canvas
|
|
20
|
+
*/
|
|
21
|
+
frameTask: Task<readonly [ContextMixinInterface | null], void>;
|
|
22
|
+
protected updated(): void;
|
|
23
|
+
private getSourceCanvas;
|
|
24
|
+
private copyFromTarget;
|
|
25
|
+
}
|
|
26
|
+
declare global {
|
|
27
|
+
interface HTMLElementTagNameMap {
|
|
28
|
+
"ef-surface": EFSurface;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TargetController } from "./TargetController.js";
|
|
2
|
+
import { Task } from "@lit/task";
|
|
3
|
+
import { LitElement, css, html } from "lit";
|
|
4
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
5
|
+
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
6
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
7
|
+
let EFSurface = class EFSurface$1 extends LitElement {
|
|
8
|
+
constructor(..._args) {
|
|
9
|
+
super(..._args);
|
|
10
|
+
this.canvasRef = createRef();
|
|
11
|
+
this.targetElement = null;
|
|
12
|
+
this.target = "";
|
|
13
|
+
this.frameTask = new Task(this, {
|
|
14
|
+
autoRun: false,
|
|
15
|
+
args: () => [this.targetElement],
|
|
16
|
+
task: async ([target]) => {
|
|
17
|
+
if (!target) return;
|
|
18
|
+
try {
|
|
19
|
+
const maybeTask = target.frameTask;
|
|
20
|
+
if (maybeTask && typeof maybeTask.run === "function") {
|
|
21
|
+
maybeTask.run();
|
|
22
|
+
await maybeTask.taskComplete;
|
|
23
|
+
}
|
|
24
|
+
} catch (_err) {}
|
|
25
|
+
this.copyFromTarget(target);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
static {
|
|
30
|
+
this.styles = [css`
|
|
31
|
+
:host {
|
|
32
|
+
display: block;
|
|
33
|
+
position: relative;
|
|
34
|
+
}
|
|
35
|
+
canvas {
|
|
36
|
+
all: inherit;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
display: block;
|
|
40
|
+
}
|
|
41
|
+
`];
|
|
42
|
+
}
|
|
43
|
+
#targetController = new TargetController(this);
|
|
44
|
+
render() {
|
|
45
|
+
return html`<canvas ${ref(this.canvasRef)}></canvas>`;
|
|
46
|
+
}
|
|
47
|
+
get rootTimegroup() {
|
|
48
|
+
const target = this.targetElement;
|
|
49
|
+
if (target && "rootTimegroup" in target) return target.rootTimegroup;
|
|
50
|
+
let root = this.closest("ef-timegroup");
|
|
51
|
+
while (root?.parentTimegroup) root = root.parentTimegroup;
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
54
|
+
get currentTimeMs() {
|
|
55
|
+
return this.rootTimegroup?.currentTimeMs ?? 0;
|
|
56
|
+
}
|
|
57
|
+
get durationMs() {
|
|
58
|
+
return this.rootTimegroup?.durationMs ?? 0;
|
|
59
|
+
}
|
|
60
|
+
get startTimeMs() {
|
|
61
|
+
return this.rootTimegroup?.startTimeMs ?? 0;
|
|
62
|
+
}
|
|
63
|
+
get endTimeMs() {
|
|
64
|
+
return this.startTimeMs + this.durationMs;
|
|
65
|
+
}
|
|
66
|
+
updated() {
|
|
67
|
+
if (this.targetElement) this.copyFromTarget(this.targetElement);
|
|
68
|
+
}
|
|
69
|
+
getSourceCanvas(from) {
|
|
70
|
+
const anyEl = from;
|
|
71
|
+
if ("canvasElement" in anyEl) return anyEl.canvasElement ?? null;
|
|
72
|
+
const sr = from.shadowRoot;
|
|
73
|
+
if (sr) {
|
|
74
|
+
const c = sr.querySelector("canvas");
|
|
75
|
+
return c ?? null;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
copyFromTarget(target) {
|
|
80
|
+
const dst = this.canvasRef.value;
|
|
81
|
+
const src = this.getSourceCanvas(target);
|
|
82
|
+
if (!dst || !src) return;
|
|
83
|
+
if (!src.width || !src.height) return;
|
|
84
|
+
if (dst.width !== src.width || dst.height !== src.height) {
|
|
85
|
+
dst.width = src.width;
|
|
86
|
+
dst.height = src.height;
|
|
87
|
+
}
|
|
88
|
+
const ctx = dst.getContext("2d");
|
|
89
|
+
if (!ctx) return;
|
|
90
|
+
ctx.drawImage(src, 0, 0, dst.width, dst.height);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
_decorate([state()], EFSurface.prototype, "targetElement", void 0);
|
|
94
|
+
_decorate([property({ type: String })], EFSurface.prototype, "target", void 0);
|
|
95
|
+
EFSurface = _decorate([customElement("ef-surface")], EFSurface);
|
|
96
|
+
export { EFSurface };
|
|
@@ -140,25 +140,26 @@ const EFTemporal = (superClass) => {
|
|
|
140
140
|
return void 0;
|
|
141
141
|
}
|
|
142
142
|
get hasOwnDuration() {
|
|
143
|
-
return
|
|
143
|
+
return this.intrinsicDurationMs !== void 0 || this.hasExplicitDuration;
|
|
144
144
|
}
|
|
145
145
|
get intrinsicDurationMs() {
|
|
146
146
|
return void 0;
|
|
147
147
|
}
|
|
148
148
|
get durationMs() {
|
|
149
|
-
|
|
149
|
+
const baseDurationMs = this.intrinsicDurationMs ?? this._durationMs ?? this.parentTimegroup?.durationMs ?? 0;
|
|
150
|
+
if (baseDurationMs === 0) return 0;
|
|
150
151
|
if (this.trimStartMs || this.trimEndMs) {
|
|
151
|
-
const trimmedDurationMs =
|
|
152
|
+
const trimmedDurationMs = baseDurationMs - (this.trimStartMs ?? 0) - (this.trimEndMs ?? 0);
|
|
152
153
|
if (trimmedDurationMs < 0) return 0;
|
|
153
154
|
return trimmedDurationMs;
|
|
154
155
|
}
|
|
155
156
|
if (this.sourceInMs || this.sourceOutMs) {
|
|
156
157
|
const sourceInMs = this.sourceInMs ?? 0;
|
|
157
|
-
const sourceOutMs = this.sourceOutMs ??
|
|
158
|
+
const sourceOutMs = this.sourceOutMs ?? baseDurationMs;
|
|
158
159
|
if (sourceInMs >= sourceOutMs) return 0;
|
|
159
160
|
return sourceOutMs - sourceInMs;
|
|
160
161
|
}
|
|
161
|
-
return
|
|
162
|
+
return baseDurationMs;
|
|
162
163
|
}
|
|
163
164
|
get sourceStartMs() {
|
|
164
165
|
return this.trimStartMs ?? this.sourceInMs ?? 0;
|
|
@@ -284,4 +285,4 @@ const EFTemporal = (superClass) => {
|
|
|
284
285
|
Object.defineProperty(TemporalMixinClass.prototype, EF_TEMPORAL, { value: true });
|
|
285
286
|
return TemporalMixinClass;
|
|
286
287
|
};
|
|
287
|
-
export { EFTemporal, deepGetElementsWithFrameTasks, deepGetTemporalElements, flushStartTimeMsCache,
|
|
288
|
+
export { EFTemporal, deepGetElementsWithFrameTasks, deepGetTemporalElements, flushStartTimeMsCache, resetTemporalCache, shallowGetTemporalElements, timegroupContext };
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import { EFVideo } from './EFVideo.js';
|
|
3
|
+
export declare class EFThumbnailStrip extends LitElement {
|
|
4
|
+
static styles: import('lit').CSSResult[];
|
|
5
|
+
canvasRef: import('lit-html/directives/ref').Ref<HTMLCanvasElement>;
|
|
6
|
+
private _targetController;
|
|
7
|
+
private _targetElement;
|
|
8
|
+
get targetElement(): EFVideo | null;
|
|
9
|
+
set targetElement(value: EFVideo | null);
|
|
10
|
+
target: string;
|
|
11
|
+
/**
|
|
12
|
+
* Desired thumbnail width in pixels (height is determined by aspect ratio)
|
|
13
|
+
* Number of thumbnails is derived from this and available strip width
|
|
14
|
+
*/
|
|
15
|
+
thumbnailWidth: number;
|
|
16
|
+
/**
|
|
17
|
+
* Custom start time in milliseconds relative to trimmed timeline (0 = start of trimmed portion)
|
|
18
|
+
* In trimmed mode: 0ms = sourceStartMs, 1000ms = sourceStartMs + 1000ms
|
|
19
|
+
* In intrinsic mode: 0ms = 0ms in source media
|
|
20
|
+
*/
|
|
21
|
+
startTimeMs?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Custom end time in milliseconds relative to trimmed timeline
|
|
24
|
+
* In trimmed mode: relative to sourceStartMs
|
|
25
|
+
* In intrinsic mode: relative to source media start (0ms)
|
|
26
|
+
*/
|
|
27
|
+
endTimeMs?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Use intrinsic duration instead of trimmed duration
|
|
30
|
+
* Accepts "true"/"false" string values or boolean
|
|
31
|
+
*/
|
|
32
|
+
useIntrinsicDuration: boolean;
|
|
33
|
+
private _stripWidth;
|
|
34
|
+
private _stripHeight;
|
|
35
|
+
private _pendingStripWidth;
|
|
36
|
+
private _thumbnailLayoutTask;
|
|
37
|
+
private set stripWidth(value);
|
|
38
|
+
private get stripWidth();
|
|
39
|
+
/**
|
|
40
|
+
* Run thumbnail render task directly with provided layout (bypasses task args dependency)
|
|
41
|
+
*/
|
|
42
|
+
private runThumbnailRenderTask;
|
|
43
|
+
private resizeObserver?;
|
|
44
|
+
private _thumbnailUpdateInProgress;
|
|
45
|
+
private _pendingThumbnailUpdate;
|
|
46
|
+
private _videoPropertyObserver?;
|
|
47
|
+
updated(changedProperties: Map<string | number | symbol, unknown>): void;
|
|
48
|
+
/**
|
|
49
|
+
* Run thumbnail update with responsive debouncing (based on EFTimegroup currentTime pattern)
|
|
50
|
+
*/
|
|
51
|
+
private runThumbnailUpdate;
|
|
52
|
+
private thumbnailLayoutTask;
|
|
53
|
+
/**
|
|
54
|
+
* Calculate layout with a ready media engine
|
|
55
|
+
*/
|
|
56
|
+
private calculateLayoutWithMediaEngine;
|
|
57
|
+
/**
|
|
58
|
+
* Generate layout from calculated time range
|
|
59
|
+
*/
|
|
60
|
+
private generateLayoutFromTimeRange;
|
|
61
|
+
private thumbnailRenderTask;
|
|
62
|
+
/**
|
|
63
|
+
* Render thumbnails with provided layout (main rendering logic)
|
|
64
|
+
*/
|
|
65
|
+
private renderThumbnails;
|
|
66
|
+
connectedCallback(): void;
|
|
67
|
+
disconnectedCallback(): void;
|
|
68
|
+
/**
|
|
69
|
+
* Draw thumbnails to the canvas with cache hits and placeholders
|
|
70
|
+
*/
|
|
71
|
+
private drawThumbnails;
|
|
72
|
+
/**
|
|
73
|
+
* Load missing thumbnails using MediaEngine batch extraction
|
|
74
|
+
*/
|
|
75
|
+
private loadMissingThumbnails;
|
|
76
|
+
/**
|
|
77
|
+
* Convert Canvas to ImageData for caching
|
|
78
|
+
*/
|
|
79
|
+
private canvasToImageData;
|
|
80
|
+
render(): import('lit-html').TemplateResult<1>;
|
|
81
|
+
}
|
|
82
|
+
declare global {
|
|
83
|
+
interface HTMLElementTagNameMap {
|
|
84
|
+
"ef-thumbnail-strip": EFThumbnailStrip;
|
|
85
|
+
}
|
|
86
|
+
}
|