@editframe/elements 0.16.7-beta.0 → 0.17.6-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/README.md +30 -0
- package/dist/DecoderResetFrequency.test.d.ts +1 -0
- package/dist/DecoderResetRecovery.test.d.ts +1 -0
- package/dist/DelayedLoadingState.d.ts +48 -0
- package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
- package/dist/DelayedLoadingState.js +113 -0
- package/dist/DelayedLoadingState.test.d.ts +1 -0
- package/dist/EF_FRAMEGEN.d.ts +10 -1
- package/dist/EF_FRAMEGEN.js +199 -179
- package/dist/EF_INTERACTIVE.js +2 -6
- package/dist/EF_RENDERING.js +1 -3
- package/dist/JitTranscodingClient.browsertest.d.ts +1 -0
- package/dist/JitTranscodingClient.d.ts +167 -0
- package/dist/JitTranscodingClient.js +373 -0
- package/dist/JitTranscodingClient.test.d.ts +1 -0
- package/dist/LoadingDebounce.test.d.ts +1 -0
- package/dist/LoadingIndicator.browsertest.d.ts +0 -0
- package/dist/ManualScrubTest.test.d.ts +1 -0
- package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
- package/dist/ScrubTrackIntegration.test.d.ts +1 -0
- package/dist/ScrubTrackManager.d.ts +96 -0
- package/dist/ScrubTrackManager.js +216 -0
- package/dist/ScrubTrackManager.test.d.ts +1 -0
- package/dist/SegmentSwitchLoading.test.d.ts +1 -0
- package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
- package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
- package/dist/elements/CrossUpdateController.js +13 -15
- package/dist/elements/EFAudio.browsertest.d.ts +0 -0
- package/dist/elements/EFAudio.d.ts +1 -1
- package/dist/elements/EFAudio.js +30 -43
- package/dist/elements/EFCaptions.js +337 -373
- package/dist/elements/EFImage.js +64 -90
- package/dist/elements/EFMedia.d.ts +98 -33
- package/dist/elements/EFMedia.js +1169 -678
- package/dist/elements/EFSourceMixin.js +31 -48
- package/dist/elements/EFTemporal.d.ts +1 -0
- package/dist/elements/EFTemporal.js +266 -360
- package/dist/elements/EFTimegroup.d.ts +3 -1
- package/dist/elements/EFTimegroup.js +262 -323
- package/dist/elements/EFVideo.browsertest.d.ts +0 -0
- package/dist/elements/EFVideo.d.ts +90 -2
- package/dist/elements/EFVideo.js +408 -111
- package/dist/elements/EFWaveform.js +375 -411
- package/dist/elements/FetchMixin.js +14 -24
- package/dist/elements/MediaController.d.ts +30 -0
- package/dist/elements/TargetController.js +130 -156
- package/dist/elements/TimegroupController.js +17 -19
- package/dist/elements/durationConverter.js +15 -4
- package/dist/elements/parseTimeToMs.js +4 -10
- package/dist/elements/printTaskStatus.d.ts +2 -0
- package/dist/elements/printTaskStatus.js +11 -0
- package/dist/elements/updateAnimations.js +39 -59
- package/dist/getRenderInfo.js +58 -67
- package/dist/gui/ContextMixin.js +203 -288
- package/dist/gui/EFConfiguration.js +27 -43
- package/dist/gui/EFFilmstrip.js +440 -620
- package/dist/gui/EFFitScale.js +112 -135
- package/dist/gui/EFFocusOverlay.js +45 -61
- package/dist/gui/EFPreview.js +30 -49
- package/dist/gui/EFScrubber.js +78 -99
- package/dist/gui/EFTimeDisplay.js +49 -70
- package/dist/gui/EFToggleLoop.js +17 -34
- package/dist/gui/EFTogglePlay.js +37 -58
- package/dist/gui/EFWorkbench.js +66 -88
- package/dist/gui/TWMixin.js +2 -48
- package/dist/gui/TWMixin2.js +31 -0
- package/dist/gui/efContext.js +2 -6
- package/dist/gui/fetchContext.js +1 -3
- package/dist/gui/focusContext.js +1 -3
- package/dist/gui/focusedElementContext.js +2 -6
- package/dist/gui/playingContext.js +1 -4
- package/dist/index.js +5 -30
- package/dist/msToTimeCode.js +11 -13
- package/dist/style.css +2 -1
- package/package.json +3 -3
- package/src/elements/EFAudio.browsertest.ts +569 -0
- package/src/elements/EFAudio.ts +4 -6
- package/src/elements/EFCaptions.browsertest.ts +0 -1
- package/src/elements/EFImage.browsertest.ts +0 -1
- package/src/elements/EFMedia.browsertest.ts +147 -115
- package/src/elements/EFMedia.ts +1339 -307
- package/src/elements/EFTemporal.browsertest.ts +0 -1
- package/src/elements/EFTemporal.ts +11 -0
- package/src/elements/EFTimegroup.ts +73 -10
- package/src/elements/EFVideo.browsertest.ts +680 -0
- package/src/elements/EFVideo.ts +729 -50
- package/src/elements/EFWaveform.ts +4 -4
- package/src/elements/MediaController.ts +108 -0
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/src/elements/printTaskStatus.ts +16 -0
- package/src/elements/updateAnimations.ts +6 -0
- package/src/gui/TWMixin.ts +10 -3
- package/test/EFVideo.frame-tasks.browsertest.ts +524 -0
- package/test/EFVideo.framegen.browsertest.ts +118 -0
- package/test/createJitTestClips.ts +293 -0
- package/test/useAssetMSW.ts +49 -0
- package/test/useMSW.ts +31 -0
- package/types.json +1 -1
- package/dist/gui/TWMixin.css.js +0 -4
- /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
- /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JIT Transcoding Client for Elements
|
|
3
|
+
* Handles communication with the JIT transcoding service for real-time video streaming
|
|
4
|
+
*/
|
|
5
|
+
export interface QualityPreset {
|
|
6
|
+
name: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
videoBitrate: number;
|
|
10
|
+
audioBitrate: number;
|
|
11
|
+
audioChannels: number;
|
|
12
|
+
audioSampleRate: number;
|
|
13
|
+
audioCodec: "aac";
|
|
14
|
+
}
|
|
15
|
+
export interface VideoMetadata {
|
|
16
|
+
url: string;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
streams: Array<{
|
|
19
|
+
index: number;
|
|
20
|
+
type: "video" | "audio" | "subtitle" | "other";
|
|
21
|
+
codecName: string;
|
|
22
|
+
duration: number;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
frameRate?: {
|
|
27
|
+
num: number;
|
|
28
|
+
den: number;
|
|
29
|
+
};
|
|
30
|
+
channels?: number;
|
|
31
|
+
sampleRate?: number;
|
|
32
|
+
}>;
|
|
33
|
+
presets: string[];
|
|
34
|
+
segmentDuration: number;
|
|
35
|
+
supportedFormats: string[];
|
|
36
|
+
extractedAt: string;
|
|
37
|
+
}
|
|
38
|
+
export interface JitTranscodingConfig {
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
defaultQuality: keyof QualityPresets;
|
|
41
|
+
segmentCacheSize: number;
|
|
42
|
+
enableNetworkAdaptation: boolean;
|
|
43
|
+
enablePrefetch: boolean;
|
|
44
|
+
prefetchSegments: number;
|
|
45
|
+
}
|
|
46
|
+
export interface QualityPresets {
|
|
47
|
+
low: QualityPreset;
|
|
48
|
+
medium: QualityPreset;
|
|
49
|
+
high: QualityPreset;
|
|
50
|
+
}
|
|
51
|
+
export interface NetworkCondition {
|
|
52
|
+
bandwidth: number;
|
|
53
|
+
rtt: number;
|
|
54
|
+
connectionType: string;
|
|
55
|
+
}
|
|
56
|
+
export interface SegmentInfo {
|
|
57
|
+
url: string;
|
|
58
|
+
startTimeMs: number;
|
|
59
|
+
durationMs: number;
|
|
60
|
+
quality: string;
|
|
61
|
+
cached: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare class JitTranscodingClient {
|
|
64
|
+
private config;
|
|
65
|
+
private segmentCache;
|
|
66
|
+
private metadataCache;
|
|
67
|
+
private qualityPresets;
|
|
68
|
+
private currentQuality;
|
|
69
|
+
private networkMonitor;
|
|
70
|
+
private pendingRequests;
|
|
71
|
+
private cacheHits;
|
|
72
|
+
private cacheMisses;
|
|
73
|
+
private totalRequests;
|
|
74
|
+
private cacheAccessOrder;
|
|
75
|
+
constructor(config?: Partial<JitTranscodingConfig>);
|
|
76
|
+
/**
|
|
77
|
+
* Check if a URL is eligible for JIT transcoding
|
|
78
|
+
*/
|
|
79
|
+
static isJitTranscodeEligible(url: string): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Instance method to check if a URL is eligible for JIT transcoding
|
|
82
|
+
*/
|
|
83
|
+
isJitTranscodeEligible(url: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Load quality presets from the JIT transcoding service
|
|
86
|
+
*/
|
|
87
|
+
loadQualityPresets(): Promise<QualityPresets>;
|
|
88
|
+
/**
|
|
89
|
+
* Internal method to actually fetch quality presets
|
|
90
|
+
*/
|
|
91
|
+
private fetchQualityPresets;
|
|
92
|
+
/**
|
|
93
|
+
* Load video metadata from the JIT transcoding service
|
|
94
|
+
*/
|
|
95
|
+
loadVideoMetadata(url: string): Promise<VideoMetadata>;
|
|
96
|
+
/**
|
|
97
|
+
* Internal method to actually fetch video metadata
|
|
98
|
+
*/
|
|
99
|
+
private fetchVideoMetadata;
|
|
100
|
+
/**
|
|
101
|
+
* Get the appropriate quality based on network conditions
|
|
102
|
+
*/
|
|
103
|
+
getAdaptiveQuality(): Promise<keyof QualityPresets>;
|
|
104
|
+
/**
|
|
105
|
+
* Generate segment URL for JIT transcoding
|
|
106
|
+
*/
|
|
107
|
+
generateSegmentUrl(videoUrl: string, startTimeMs: number, quality?: keyof QualityPresets): string;
|
|
108
|
+
/**
|
|
109
|
+
* Fetch a video segment with caching
|
|
110
|
+
*/
|
|
111
|
+
fetchSegment(videoUrl: string, startTimeMs: number, quality?: keyof QualityPresets): Promise<ArrayBuffer>;
|
|
112
|
+
/**
|
|
113
|
+
* Internal method to actually fetch a video segment
|
|
114
|
+
*/
|
|
115
|
+
private fetchVideoSegment;
|
|
116
|
+
/**
|
|
117
|
+
* Prefetch upcoming segments
|
|
118
|
+
*/
|
|
119
|
+
prefetchSegments(videoUrl: string, currentTimeMs: number, quality?: keyof QualityPresets): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Cache a segment with LRU eviction
|
|
122
|
+
*/
|
|
123
|
+
private cacheSegment;
|
|
124
|
+
/**
|
|
125
|
+
* Get comprehensive cache statistics
|
|
126
|
+
*/
|
|
127
|
+
getCacheStats(): {
|
|
128
|
+
size: number;
|
|
129
|
+
maxSize: number;
|
|
130
|
+
hitRate: number;
|
|
131
|
+
efficiency: number;
|
|
132
|
+
totalRequests: number;
|
|
133
|
+
recentKeys: string[];
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Clear all caches
|
|
137
|
+
*/
|
|
138
|
+
clearCache(): void;
|
|
139
|
+
/**
|
|
140
|
+
* Set manual quality override
|
|
141
|
+
*/
|
|
142
|
+
setQuality(quality: keyof QualityPresets): void;
|
|
143
|
+
/**
|
|
144
|
+
* Get current quality setting
|
|
145
|
+
*/
|
|
146
|
+
getCurrentQuality(): keyof QualityPresets;
|
|
147
|
+
/**
|
|
148
|
+
* Load a video segment for any quality preset including scrub
|
|
149
|
+
*/
|
|
150
|
+
loadSegment(videoUrl: string, quality: string, startTimeMs: number): Promise<ArrayBuffer>;
|
|
151
|
+
/**
|
|
152
|
+
* Load a scrub track segment (30s duration)
|
|
153
|
+
*/
|
|
154
|
+
private loadScrubSegment;
|
|
155
|
+
/**
|
|
156
|
+
* Internal method to fetch scrub segment
|
|
157
|
+
*/
|
|
158
|
+
private fetchScrubSegment;
|
|
159
|
+
/**
|
|
160
|
+
* Check if a segment is cached for a given quality
|
|
161
|
+
*/
|
|
162
|
+
hasSegmentInCache(videoUrl: string, quality: string, timeMs: number): boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Get video metadata
|
|
165
|
+
*/
|
|
166
|
+
getMetadata(videoUrl: string): Promise<VideoMetadata>;
|
|
167
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
var JitTranscodingClient = class JitTranscodingClient {
|
|
2
|
+
constructor(config = {}) {
|
|
3
|
+
this.segmentCache = /* @__PURE__ */ new Map();
|
|
4
|
+
this.metadataCache = /* @__PURE__ */ new Map();
|
|
5
|
+
this.qualityPresets = null;
|
|
6
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
7
|
+
this.cacheHits = 0;
|
|
8
|
+
this.cacheMisses = 0;
|
|
9
|
+
this.totalRequests = 0;
|
|
10
|
+
this.cacheAccessOrder = [];
|
|
11
|
+
this.config = {
|
|
12
|
+
baseUrl: "http://localhost:3000",
|
|
13
|
+
defaultQuality: "medium",
|
|
14
|
+
segmentCacheSize: 50,
|
|
15
|
+
enableNetworkAdaptation: true,
|
|
16
|
+
enablePrefetch: true,
|
|
17
|
+
prefetchSegments: 3,
|
|
18
|
+
...config
|
|
19
|
+
};
|
|
20
|
+
this.currentQuality = this.config.defaultQuality;
|
|
21
|
+
this.networkMonitor = new NetworkMonitor();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a URL is eligible for JIT transcoding
|
|
25
|
+
*/
|
|
26
|
+
static isJitTranscodeEligible(url) {
|
|
27
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Instance method to check if a URL is eligible for JIT transcoding
|
|
31
|
+
*/
|
|
32
|
+
isJitTranscodeEligible(url) {
|
|
33
|
+
return JitTranscodingClient.isJitTranscodeEligible(url);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Load quality presets from the JIT transcoding service
|
|
37
|
+
*/
|
|
38
|
+
async loadQualityPresets() {
|
|
39
|
+
if (this.qualityPresets) return this.qualityPresets;
|
|
40
|
+
const requestKey = "quality-presets";
|
|
41
|
+
const pendingRequest = this.pendingRequests.get(requestKey);
|
|
42
|
+
if (pendingRequest) return pendingRequest;
|
|
43
|
+
const requestPromise = this.fetchQualityPresets();
|
|
44
|
+
this.pendingRequests.set(requestKey, requestPromise);
|
|
45
|
+
try {
|
|
46
|
+
const result = await requestPromise;
|
|
47
|
+
this.pendingRequests.delete(requestKey);
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
this.pendingRequests.delete(requestKey);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Internal method to actually fetch quality presets
|
|
56
|
+
*/
|
|
57
|
+
async fetchQualityPresets() {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`${this.config.baseUrl}/api/v1/transcode/presets`);
|
|
60
|
+
if (!response.ok) throw new Error(`Failed to load quality presets: ${response.status}`);
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
this.qualityPresets = data.presets;
|
|
63
|
+
if (!this.qualityPresets) throw new Error("No quality presets found");
|
|
64
|
+
return this.qualityPresets;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Failed to load quality presets:", error);
|
|
67
|
+
this.qualityPresets = {
|
|
68
|
+
low: {
|
|
69
|
+
name: "low",
|
|
70
|
+
width: 480,
|
|
71
|
+
height: 270,
|
|
72
|
+
videoBitrate: 4e5,
|
|
73
|
+
audioBitrate: 64e3,
|
|
74
|
+
audioChannels: 2,
|
|
75
|
+
audioSampleRate: 48e3,
|
|
76
|
+
audioCodec: "aac"
|
|
77
|
+
},
|
|
78
|
+
medium: {
|
|
79
|
+
name: "medium",
|
|
80
|
+
width: 854,
|
|
81
|
+
height: 480,
|
|
82
|
+
videoBitrate: 1e6,
|
|
83
|
+
audioBitrate: 128e3,
|
|
84
|
+
audioChannels: 2,
|
|
85
|
+
audioSampleRate: 48e3,
|
|
86
|
+
audioCodec: "aac"
|
|
87
|
+
},
|
|
88
|
+
high: {
|
|
89
|
+
name: "high",
|
|
90
|
+
width: 1280,
|
|
91
|
+
height: 720,
|
|
92
|
+
videoBitrate: 25e5,
|
|
93
|
+
audioBitrate: 192e3,
|
|
94
|
+
audioChannels: 2,
|
|
95
|
+
audioSampleRate: 48e3,
|
|
96
|
+
audioCodec: "aac"
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return this.qualityPresets;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Load video metadata from the JIT transcoding service
|
|
104
|
+
*/
|
|
105
|
+
async loadVideoMetadata(url) {
|
|
106
|
+
const cached = this.metadataCache.get(url);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
const requestKey = `metadata:${url}`;
|
|
109
|
+
const pendingRequest = this.pendingRequests.get(requestKey);
|
|
110
|
+
if (pendingRequest) return pendingRequest;
|
|
111
|
+
const requestPromise = this.fetchVideoMetadata(url);
|
|
112
|
+
this.pendingRequests.set(requestKey, requestPromise);
|
|
113
|
+
try {
|
|
114
|
+
const result = await requestPromise;
|
|
115
|
+
this.pendingRequests.delete(requestKey);
|
|
116
|
+
return result;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.pendingRequests.delete(requestKey);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Internal method to actually fetch video metadata
|
|
124
|
+
*/
|
|
125
|
+
async fetchVideoMetadata(url) {
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(`${this.config.baseUrl}/api/v1/transcode/metadata?url=${encodeURIComponent(url)}`);
|
|
128
|
+
if (!response.ok) throw new Error(`Failed to load video metadata: ${response.status}`);
|
|
129
|
+
const metadata = await response.json();
|
|
130
|
+
this.metadataCache.set(url, metadata);
|
|
131
|
+
return metadata;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("Failed to load video metadata:", error);
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the appropriate quality based on network conditions
|
|
139
|
+
*/
|
|
140
|
+
async getAdaptiveQuality() {
|
|
141
|
+
if (!this.config.enableNetworkAdaptation) return this.currentQuality;
|
|
142
|
+
const networkCondition = await this.networkMonitor.getCurrentCondition();
|
|
143
|
+
if (networkCondition.bandwidth < 8e5) return "low";
|
|
144
|
+
if (networkCondition.bandwidth < 2e6) return "medium";
|
|
145
|
+
return "high";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generate segment URL for JIT transcoding
|
|
149
|
+
*/
|
|
150
|
+
generateSegmentUrl(videoUrl, startTimeMs, quality = this.currentQuality) {
|
|
151
|
+
const alignedStartTime = Math.floor(startTimeMs / 2e3) * 2e3;
|
|
152
|
+
return `${this.config.baseUrl}/api/v1/transcode/${quality}?url=${encodeURIComponent(videoUrl)}&start=${alignedStartTime}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fetch a video segment with caching
|
|
156
|
+
*/
|
|
157
|
+
async fetchSegment(videoUrl, startTimeMs, quality) {
|
|
158
|
+
const effectiveQuality = quality || await this.getAdaptiveQuality();
|
|
159
|
+
const segmentUrl = this.generateSegmentUrl(videoUrl, startTimeMs, effectiveQuality);
|
|
160
|
+
const cacheKey = `${videoUrl}:${startTimeMs}:${effectiveQuality}`;
|
|
161
|
+
const cached = this.segmentCache.get(cacheKey);
|
|
162
|
+
if (cached) {
|
|
163
|
+
this.cacheHits++;
|
|
164
|
+
return cached;
|
|
165
|
+
}
|
|
166
|
+
const requestKey = `segment:${cacheKey}`;
|
|
167
|
+
const pendingRequest = this.pendingRequests.get(requestKey);
|
|
168
|
+
if (pendingRequest) return pendingRequest;
|
|
169
|
+
const requestPromise = this.fetchVideoSegment(segmentUrl, cacheKey, effectiveQuality);
|
|
170
|
+
this.pendingRequests.set(requestKey, requestPromise);
|
|
171
|
+
try {
|
|
172
|
+
const result = await requestPromise;
|
|
173
|
+
this.pendingRequests.delete(requestKey);
|
|
174
|
+
return result;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
this.pendingRequests.delete(requestKey);
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Internal method to actually fetch a video segment
|
|
182
|
+
*/
|
|
183
|
+
async fetchVideoSegment(segmentUrl, cacheKey, effectiveQuality) {
|
|
184
|
+
try {
|
|
185
|
+
const response = await fetch(segmentUrl);
|
|
186
|
+
if (!response.ok) throw new Error(`Failed to fetch segment: ${response.status}`);
|
|
187
|
+
const buffer = await response.arrayBuffer();
|
|
188
|
+
this.cacheSegment(cacheKey, buffer);
|
|
189
|
+
this.currentQuality = effectiveQuality;
|
|
190
|
+
this.cacheMisses++;
|
|
191
|
+
return buffer;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error("Failed to fetch segment:", error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Prefetch upcoming segments
|
|
199
|
+
*/
|
|
200
|
+
async prefetchSegments(videoUrl, currentTimeMs, quality) {
|
|
201
|
+
if (!this.config.enablePrefetch) return;
|
|
202
|
+
const effectiveQuality = quality || this.currentQuality;
|
|
203
|
+
const segmentDuration = 2e3;
|
|
204
|
+
const prefetchPromises = [];
|
|
205
|
+
for (let i = 1; i <= this.config.prefetchSegments; i++) {
|
|
206
|
+
const nextSegmentStart = currentTimeMs + i * segmentDuration;
|
|
207
|
+
const cacheKey = `${videoUrl}:${nextSegmentStart}:${effectiveQuality}`;
|
|
208
|
+
if (!this.segmentCache.has(cacheKey)) prefetchPromises.push(this.fetchSegment(videoUrl, nextSegmentStart, effectiveQuality).catch((error) => {
|
|
209
|
+
console.warn(`Failed to prefetch segment at ${nextSegmentStart}ms:`, error);
|
|
210
|
+
return /* @__PURE__ */ new ArrayBuffer(0);
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
Promise.allSettled(prefetchPromises).then(() => {
|
|
214
|
+
console.debug(`Prefetched ${prefetchPromises.length} segments`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Cache a segment with LRU eviction
|
|
219
|
+
*/
|
|
220
|
+
cacheSegment(cacheKey, buffer) {
|
|
221
|
+
if (this.segmentCache.size >= this.config.segmentCacheSize) {
|
|
222
|
+
const firstKey = this.segmentCache.keys().next().value;
|
|
223
|
+
if (firstKey) {
|
|
224
|
+
this.segmentCache.delete(firstKey);
|
|
225
|
+
const index = this.cacheAccessOrder.indexOf(firstKey);
|
|
226
|
+
if (index > -1) this.cacheAccessOrder.splice(index, 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
this.segmentCache.set(cacheKey, buffer);
|
|
230
|
+
const existingIndex = this.cacheAccessOrder.indexOf(cacheKey);
|
|
231
|
+
if (existingIndex > -1) this.cacheAccessOrder.splice(existingIndex, 1);
|
|
232
|
+
this.cacheAccessOrder.push(cacheKey);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get comprehensive cache statistics
|
|
236
|
+
*/
|
|
237
|
+
getCacheStats() {
|
|
238
|
+
this.totalRequests = this.cacheHits + this.cacheMisses;
|
|
239
|
+
const hitRate = this.totalRequests > 0 ? this.cacheHits / this.totalRequests : 0;
|
|
240
|
+
const efficiency = this.segmentCache.size > 0 ? this.cacheHits / this.segmentCache.size : 0;
|
|
241
|
+
return {
|
|
242
|
+
size: this.segmentCache.size,
|
|
243
|
+
maxSize: this.config.segmentCacheSize,
|
|
244
|
+
hitRate,
|
|
245
|
+
efficiency,
|
|
246
|
+
totalRequests: this.totalRequests,
|
|
247
|
+
recentKeys: this.cacheAccessOrder.slice(-5)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Clear all caches
|
|
252
|
+
*/
|
|
253
|
+
clearCache() {
|
|
254
|
+
this.segmentCache.clear();
|
|
255
|
+
this.metadataCache.clear();
|
|
256
|
+
this.pendingRequests.clear();
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Set manual quality override
|
|
260
|
+
*/
|
|
261
|
+
setQuality(quality) {
|
|
262
|
+
this.currentQuality = quality;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get current quality setting
|
|
266
|
+
*/
|
|
267
|
+
getCurrentQuality() {
|
|
268
|
+
return this.currentQuality;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Load a video segment for any quality preset including scrub
|
|
272
|
+
*/
|
|
273
|
+
async loadSegment(videoUrl, quality, startTimeMs) {
|
|
274
|
+
if (quality === "scrub") return this.loadScrubSegment(videoUrl, startTimeMs);
|
|
275
|
+
return this.fetchSegment(videoUrl, startTimeMs, quality);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Load a scrub track segment (30s duration)
|
|
279
|
+
*/
|
|
280
|
+
async loadScrubSegment(videoUrl, startTimeMs) {
|
|
281
|
+
const alignedStartTime = Math.floor(startTimeMs / 3e4) * 3e4;
|
|
282
|
+
const segmentUrl = `${this.config.baseUrl}/api/v1/transcode/scrub?url=${encodeURIComponent(videoUrl)}&start=${alignedStartTime}`;
|
|
283
|
+
const cacheKey = `${videoUrl}:${alignedStartTime}:scrub`;
|
|
284
|
+
const cached = this.segmentCache.get(cacheKey);
|
|
285
|
+
if (cached) {
|
|
286
|
+
this.cacheHits++;
|
|
287
|
+
return cached;
|
|
288
|
+
}
|
|
289
|
+
const requestKey = `scrub:${cacheKey}`;
|
|
290
|
+
const pendingRequest = this.pendingRequests.get(requestKey);
|
|
291
|
+
if (pendingRequest) return pendingRequest;
|
|
292
|
+
const requestPromise = this.fetchScrubSegment(segmentUrl, cacheKey);
|
|
293
|
+
this.pendingRequests.set(requestKey, requestPromise);
|
|
294
|
+
try {
|
|
295
|
+
const result = await requestPromise;
|
|
296
|
+
this.pendingRequests.delete(requestKey);
|
|
297
|
+
return result;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
this.pendingRequests.delete(requestKey);
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Internal method to fetch scrub segment
|
|
305
|
+
*/
|
|
306
|
+
async fetchScrubSegment(segmentUrl, cacheKey) {
|
|
307
|
+
try {
|
|
308
|
+
const response = await fetch(segmentUrl);
|
|
309
|
+
if (!response.ok) throw new Error(`Failed to fetch scrub segment: ${response.status}`);
|
|
310
|
+
const buffer = await response.arrayBuffer();
|
|
311
|
+
this.cacheSegment(cacheKey, buffer);
|
|
312
|
+
this.cacheMisses++;
|
|
313
|
+
return buffer;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error("Failed to fetch scrub segment:", error);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Check if a segment is cached for a given quality
|
|
321
|
+
*/
|
|
322
|
+
hasSegmentInCache(videoUrl, quality, timeMs) {
|
|
323
|
+
let alignedTime;
|
|
324
|
+
let cacheKey;
|
|
325
|
+
if (quality === "scrub") {
|
|
326
|
+
alignedTime = Math.floor(timeMs / 3e4) * 3e4;
|
|
327
|
+
cacheKey = `${videoUrl}:${alignedTime}:scrub`;
|
|
328
|
+
} else {
|
|
329
|
+
alignedTime = Math.floor(timeMs / 2e3) * 2e3;
|
|
330
|
+
cacheKey = `${videoUrl}:${alignedTime}:${quality}`;
|
|
331
|
+
}
|
|
332
|
+
return this.segmentCache.has(cacheKey);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Get video metadata
|
|
336
|
+
*/
|
|
337
|
+
async getMetadata(videoUrl) {
|
|
338
|
+
return this.loadVideoMetadata(videoUrl);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Network condition monitoring
|
|
343
|
+
*/
|
|
344
|
+
var NetworkMonitor = class {
|
|
345
|
+
constructor() {
|
|
346
|
+
this.measurementHistory = [];
|
|
347
|
+
}
|
|
348
|
+
async getCurrentCondition() {
|
|
349
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
350
|
+
if (connection) return {
|
|
351
|
+
bandwidth: (connection.downlink || 1) * 1e6,
|
|
352
|
+
rtt: connection.rtt || 100,
|
|
353
|
+
connectionType: connection.effectiveType || "unknown"
|
|
354
|
+
};
|
|
355
|
+
const estimatedBandwidth = this.estimateBandwidth();
|
|
356
|
+
return {
|
|
357
|
+
bandwidth: estimatedBandwidth,
|
|
358
|
+
rtt: 100,
|
|
359
|
+
connectionType: "unknown"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
estimateBandwidth() {
|
|
363
|
+
if (this.measurementHistory.length === 0) return 1e6;
|
|
364
|
+
const average = this.measurementHistory.reduce((a, b) => a + b, 0) / this.measurementHistory.length;
|
|
365
|
+
return Math.max(average, 4e5);
|
|
366
|
+
}
|
|
367
|
+
measureBandwidth(bytes, timeMs) {
|
|
368
|
+
const bandwidth = bytes * 8 / (timeMs / 1e3);
|
|
369
|
+
this.measurementHistory.push(bandwidth);
|
|
370
|
+
if (this.measurementHistory.length > 10) this.measurementHistory.shift();
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
export { JitTranscodingClient };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { JitTranscodingClient } from './JitTranscodingClient.js';
|
|
2
|
+
export interface ScrubTrackConfig {
|
|
3
|
+
maxScrubCacheSegments?: number;
|
|
4
|
+
prefetchCount?: number;
|
|
5
|
+
fastSeekThresholdMs?: number;
|
|
6
|
+
onLoadingStateChange?: (isLoading: boolean, message?: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export interface CacheStats {
|
|
9
|
+
hits: number;
|
|
10
|
+
misses: number;
|
|
11
|
+
hitRate: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class ScrubTrackManager {
|
|
14
|
+
readonly videoUrl: string;
|
|
15
|
+
private jitClient;
|
|
16
|
+
private config;
|
|
17
|
+
private scrubCache;
|
|
18
|
+
private metadata;
|
|
19
|
+
private cacheHits;
|
|
20
|
+
private cacheMisses;
|
|
21
|
+
private isInitialized;
|
|
22
|
+
private preloadingSegments;
|
|
23
|
+
constructor(videoUrl: string, jitClient: JitTranscodingClient, config?: ScrubTrackConfig);
|
|
24
|
+
/**
|
|
25
|
+
* Initialize scrub track manager and start preloading
|
|
26
|
+
*/
|
|
27
|
+
initialize(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Determine if scrub track should be used instead of normal video
|
|
30
|
+
*/
|
|
31
|
+
shouldUseScrubTrack(seekTimeMs: number): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Detect if this is a fast seeking operation
|
|
34
|
+
*/
|
|
35
|
+
isFastSeeking(currentTimeMs: number, seekTimeMs: number): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Align seek time to 30s scrub segment boundary
|
|
38
|
+
*/
|
|
39
|
+
alignToScrubBoundary(timeMs: number): number;
|
|
40
|
+
/**
|
|
41
|
+
* Get scrub frame for the given seek time
|
|
42
|
+
*/
|
|
43
|
+
getScrubFrame(seekTimeMs: number): Promise<VideoFrame | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Record cache hit for statistics
|
|
46
|
+
*/
|
|
47
|
+
recordCacheHit(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Record cache miss for statistics
|
|
50
|
+
*/
|
|
51
|
+
recordCacheMiss(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Get cache performance statistics
|
|
54
|
+
*/
|
|
55
|
+
getCacheStats(): CacheStats;
|
|
56
|
+
/**
|
|
57
|
+
* Check if scrub segment is cached for the given time
|
|
58
|
+
*/
|
|
59
|
+
isScrubSegmentCached(seekTimeMs: number): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Get current scrub cache size
|
|
62
|
+
*/
|
|
63
|
+
getScrubCacheSize(): number;
|
|
64
|
+
/**
|
|
65
|
+
* Get total number of scrub segments for video
|
|
66
|
+
*/
|
|
67
|
+
getTotalScrubSegments(): number;
|
|
68
|
+
/**
|
|
69
|
+
* Update metadata (e.g., when video duration changes)
|
|
70
|
+
*/
|
|
71
|
+
updateMetadata(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Preload initial scrub segments
|
|
74
|
+
*/
|
|
75
|
+
private preloadInitialSegments;
|
|
76
|
+
/**
|
|
77
|
+
* Preload nearby segments around a given time
|
|
78
|
+
*/
|
|
79
|
+
private preloadNearbySegments;
|
|
80
|
+
/**
|
|
81
|
+
* Preload a single scrub segment (background operation)
|
|
82
|
+
*/
|
|
83
|
+
private preloadSegment;
|
|
84
|
+
/**
|
|
85
|
+
* Cache a scrub segment with LRU eviction
|
|
86
|
+
*/
|
|
87
|
+
private cacheSegment;
|
|
88
|
+
/**
|
|
89
|
+
* Clear cache and reset state
|
|
90
|
+
*/
|
|
91
|
+
reset(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Clean up resources and cancel any pending operations
|
|
94
|
+
*/
|
|
95
|
+
cleanup(): void;
|
|
96
|
+
}
|