@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
|
@@ -1,14 +1,102 @@
|
|
|
1
|
+
import { VideoAsset } from '../../../assets/src/EncodedAsset.ts';
|
|
1
2
|
import { Task } from '@lit/task';
|
|
3
|
+
import { PropertyValueMap } from 'lit';
|
|
4
|
+
import { CacheStats, ScrubTrackManager } from '../ScrubTrackManager.js';
|
|
2
5
|
import { EFMedia } from './EFMedia.js';
|
|
6
|
+
declare global {
|
|
7
|
+
var EF_FRAMEGEN: import("../EF_FRAMEGEN.js").EFFramegen;
|
|
8
|
+
}
|
|
9
|
+
interface LoadingState {
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
operation: "scrub-segment" | "video-segment" | "seeking" | "decoding" | null;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
3
14
|
declare const EFVideo_base: typeof EFMedia;
|
|
4
15
|
export declare class EFVideo extends EFVideo_base {
|
|
5
16
|
#private;
|
|
6
17
|
static styles: import('lit').CSSResult[];
|
|
7
18
|
canvasRef: import('lit-html/directives/ref.js').Ref<HTMLCanvasElement>;
|
|
19
|
+
/**
|
|
20
|
+
* Scrub track manager for fast timeline navigation
|
|
21
|
+
*/
|
|
22
|
+
scrubTrackManager?: ScrubTrackManager;
|
|
23
|
+
/**
|
|
24
|
+
* Track last seek time for fast seeking detection
|
|
25
|
+
*/
|
|
26
|
+
private lastSeekTimeMs;
|
|
27
|
+
/**
|
|
28
|
+
* Delayed loading state manager for user feedback
|
|
29
|
+
*/
|
|
30
|
+
private delayedLoadingState;
|
|
31
|
+
/**
|
|
32
|
+
* Loading state for user feedback
|
|
33
|
+
*/
|
|
34
|
+
loadingState: {
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
operation: LoadingState["operation"];
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
constructor();
|
|
8
40
|
render(): import('lit-html').TemplateResult<1>;
|
|
9
41
|
get canvasElement(): HTMLCanvasElement | undefined;
|
|
10
|
-
frameTask: Task<readonly [
|
|
11
|
-
|
|
42
|
+
frameTask: Task<readonly [number], void>;
|
|
43
|
+
get frameTaskStatus(): {
|
|
44
|
+
desiredSeekTimeMs: number;
|
|
45
|
+
fragmentIndexTask: string;
|
|
46
|
+
seekTask: string;
|
|
47
|
+
mediaSegmentsTask: string;
|
|
48
|
+
assetSegmentLoader: string;
|
|
49
|
+
assetSegmentKeysTask: string;
|
|
50
|
+
assetInitSegmentsTask: string;
|
|
51
|
+
videoAssetTask: string;
|
|
52
|
+
paintTask: string;
|
|
53
|
+
frameTask: string;
|
|
54
|
+
};
|
|
55
|
+
protected updated(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void;
|
|
56
|
+
/**
|
|
57
|
+
* Initialize scrub track manager if needed
|
|
58
|
+
*/
|
|
59
|
+
private initializeScrubTrackManager;
|
|
60
|
+
/**
|
|
61
|
+
* Start a delayed loading operation for testing
|
|
62
|
+
*/
|
|
63
|
+
startDelayedLoading(operationId: string, message: string, options?: {
|
|
64
|
+
background?: boolean;
|
|
65
|
+
}): void;
|
|
66
|
+
/**
|
|
67
|
+
* Clear a delayed loading operation for testing
|
|
68
|
+
*/
|
|
69
|
+
clearDelayedLoading(operationId: string): void;
|
|
70
|
+
/**
|
|
71
|
+
* Set loading state for user feedback
|
|
72
|
+
*/
|
|
73
|
+
private setLoadingState;
|
|
74
|
+
videoAssetTask: Task<readonly ["asset" | "jit-transcode", Record<string, File> | null | undefined], VideoAsset | undefined>;
|
|
75
|
+
paintTask: Task<readonly [number], number | undefined>;
|
|
76
|
+
/**
|
|
77
|
+
* Render normal video using existing logic
|
|
78
|
+
*/
|
|
79
|
+
private renderNormalVideo;
|
|
80
|
+
/**
|
|
81
|
+
* Display a video frame on the canvas
|
|
82
|
+
*/
|
|
83
|
+
private displayFrame;
|
|
84
|
+
/**
|
|
85
|
+
* Check if we're in production rendering mode (EF_FRAMEGEN active) vs preview mode
|
|
86
|
+
*/
|
|
87
|
+
private isInProductionRenderingMode;
|
|
88
|
+
/**
|
|
89
|
+
* Check if EF_FRAMEGEN has explicitly started frame rendering (not just initialization)
|
|
90
|
+
*/
|
|
91
|
+
private isFrameRenderingActive;
|
|
92
|
+
/**
|
|
93
|
+
* Get scrub track performance statistics
|
|
94
|
+
*/
|
|
95
|
+
getScrubTrackStats(): CacheStats | null;
|
|
96
|
+
/**
|
|
97
|
+
* Clean up resources when component is disconnected
|
|
98
|
+
*/
|
|
99
|
+
disconnectedCallback(): void;
|
|
12
100
|
}
|
|
13
101
|
declare global {
|
|
14
102
|
interface HTMLElementTagNameMap {
|
package/dist/elements/EFVideo.js
CHANGED
|
@@ -1,112 +1,22 @@
|
|
|
1
|
+
import { EFMedia } from "./EFMedia.js";
|
|
2
|
+
import { DelayedLoadingState } from "../DelayedLoadingState.js";
|
|
3
|
+
import { TWMixin } from "../gui/TWMixin2.js";
|
|
4
|
+
import { ScrubTrackManager } from "../ScrubTrackManager.js";
|
|
5
|
+
import { printTaskStatus } from "./printTaskStatus.js";
|
|
1
6
|
import { Task } from "@lit/task";
|
|
7
|
+
import debug from "debug";
|
|
2
8
|
import { css, html } from "lit";
|
|
3
|
-
import { customElement } from "lit/decorators.js";
|
|
9
|
+
import { customElement, state } from "lit/decorators.js";
|
|
10
|
+
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
11
|
+
import { VideoAsset } from "@editframe/assets/EncodedAsset.js";
|
|
4
12
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw TypeError(msg);
|
|
10
|
-
};
|
|
11
|
-
var __decorateClass = (decorators, target, key, kind) => {
|
|
12
|
-
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
13
|
-
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
14
|
-
if (decorator = decorators[i])
|
|
15
|
-
result = decorator(result) || result;
|
|
16
|
-
return result;
|
|
17
|
-
};
|
|
18
|
-
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
19
|
-
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), member.get(obj));
|
|
20
|
-
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
21
|
-
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
|
|
22
|
-
var _decoderLock;
|
|
23
|
-
let EFVideo = class extends TWMixin(EFMedia) {
|
|
24
|
-
constructor() {
|
|
25
|
-
super(...arguments);
|
|
26
|
-
this.canvasRef = createRef();
|
|
27
|
-
__privateAdd(this, _decoderLock, false);
|
|
28
|
-
this.frameTask = new Task(this, {
|
|
29
|
-
args: () => [
|
|
30
|
-
this.trackFragmentIndexLoader.status,
|
|
31
|
-
this.initSegmentsLoader.status,
|
|
32
|
-
this.seekTask.status,
|
|
33
|
-
this.fetchSeekTask.status,
|
|
34
|
-
this.videoAssetTask.status,
|
|
35
|
-
this.paintTask.status
|
|
36
|
-
],
|
|
37
|
-
task: async () => {
|
|
38
|
-
await this.trackFragmentIndexLoader.taskComplete;
|
|
39
|
-
await this.initSegmentsLoader.taskComplete;
|
|
40
|
-
await this.seekTask.taskComplete;
|
|
41
|
-
await this.fetchSeekTask.taskComplete;
|
|
42
|
-
await this.videoAssetTask.taskComplete;
|
|
43
|
-
await this.paintTask.taskComplete;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
this.paintTask = new Task(this, {
|
|
47
|
-
args: () => [this.videoAssetTask.value, this.desiredSeekTimeMs],
|
|
48
|
-
task: async ([videoAsset, seekToMs], {
|
|
49
|
-
signal: _signal
|
|
50
|
-
}) => {
|
|
51
|
-
if (!videoAsset) {
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (__privateGet(this, _decoderLock)) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
__privateSet(this, _decoderLock, true);
|
|
59
|
-
const frame = await videoAsset.seekToTime(seekToMs / 1e3);
|
|
60
|
-
if (!this.canvasElement) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const ctx = this.canvasElement.getContext("2d");
|
|
64
|
-
if (!(frame && ctx)) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (frame?.codedWidth && frame?.codedHeight) {
|
|
68
|
-
if (this.canvasElement.width !== frame.codedWidth || this.canvasElement.height !== frame.codedHeight) {
|
|
69
|
-
this.canvasElement.width = frame.codedWidth;
|
|
70
|
-
this.canvasElement.height = frame.codedHeight;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
if (frame.format === null) {
|
|
74
|
-
console.warn("Frame format is null", frame);
|
|
75
|
-
return seekToMs;
|
|
76
|
-
}
|
|
77
|
-
ctx.drawImage(
|
|
78
|
-
frame,
|
|
79
|
-
0,
|
|
80
|
-
0,
|
|
81
|
-
this.canvasElement.width,
|
|
82
|
-
this.canvasElement.height
|
|
83
|
-
);
|
|
84
|
-
return seekToMs;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.trace("Unexpected error while seeking video", error);
|
|
87
|
-
} finally {
|
|
88
|
-
__privateSet(this, _decoderLock, false);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
render() {
|
|
94
|
-
return html`
|
|
95
|
-
<canvas ${ref(this.canvasRef)}></canvas>
|
|
96
|
-
`;
|
|
97
|
-
}
|
|
98
|
-
get canvasElement() {
|
|
99
|
-
return this.canvasRef.value;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
_decoderLock = /* @__PURE__ */ new WeakMap();
|
|
103
|
-
EFVideo.styles = [
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
*/
|
|
107
|
-
css`
|
|
13
|
+
const log = debug("ef:elements:EFVideo");
|
|
14
|
+
let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
15
|
+
static {
|
|
16
|
+
this.styles = [css`
|
|
108
17
|
:host {
|
|
109
18
|
display: block;
|
|
19
|
+
position: relative;
|
|
110
20
|
}
|
|
111
21
|
canvas {
|
|
112
22
|
all: inherit;
|
|
@@ -121,11 +31,398 @@ EFVideo.styles = [
|
|
|
121
31
|
outline: none;
|
|
122
32
|
box-shadow: none;
|
|
123
33
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
34
|
+
.loading-overlay {
|
|
35
|
+
position: absolute;
|
|
36
|
+
top: 0;
|
|
37
|
+
left: 0;
|
|
38
|
+
right: 0;
|
|
39
|
+
bottom: 0;
|
|
40
|
+
background: rgba(0, 0, 0, 0.6);
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
z-index: 10;
|
|
45
|
+
backdrop-filter: blur(2px);
|
|
46
|
+
}
|
|
47
|
+
.loading-content {
|
|
48
|
+
background: rgba(0, 0, 0, 0.8);
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
padding: 16px 24px;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
gap: 12px;
|
|
54
|
+
color: white;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
}
|
|
58
|
+
.loading-spinner {
|
|
59
|
+
width: 20px;
|
|
60
|
+
height: 20px;
|
|
61
|
+
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
62
|
+
border-left: 2px solid #fff;
|
|
63
|
+
border-radius: 50%;
|
|
64
|
+
animation: spin 1s linear infinite;
|
|
65
|
+
}
|
|
66
|
+
@keyframes spin {
|
|
67
|
+
0% { transform: rotate(0deg); }
|
|
68
|
+
100% { transform: rotate(360deg); }
|
|
69
|
+
}
|
|
70
|
+
.loading-message {
|
|
71
|
+
font-size: 12px;
|
|
72
|
+
opacity: 0.8;
|
|
73
|
+
}
|
|
74
|
+
`];
|
|
75
|
+
}
|
|
76
|
+
constructor() {
|
|
77
|
+
super();
|
|
78
|
+
this.canvasRef = createRef();
|
|
79
|
+
this.lastSeekTimeMs = 0;
|
|
80
|
+
this.loadingState = {
|
|
81
|
+
isLoading: false,
|
|
82
|
+
operation: null,
|
|
83
|
+
message: ""
|
|
84
|
+
};
|
|
85
|
+
this.frameTask = new Task(this, {
|
|
86
|
+
args: () => [this.desiredSeekTimeMs],
|
|
87
|
+
onError: (error) => {
|
|
88
|
+
console.error("frameTask error", error);
|
|
89
|
+
},
|
|
90
|
+
task: async ([_desiredSeekTimeMs], { signal }) => {
|
|
91
|
+
await this.seekTask.taskComplete;
|
|
92
|
+
if (signal.aborted) return;
|
|
93
|
+
await this.fragmentIndexTask.taskComplete;
|
|
94
|
+
if (signal.aborted) return;
|
|
95
|
+
await this.mediaSegmentsTask.taskComplete;
|
|
96
|
+
if (signal.aborted) return;
|
|
97
|
+
await this.videoAssetTask.taskComplete;
|
|
98
|
+
if (signal.aborted) return;
|
|
99
|
+
await this.paintTask.taskComplete;
|
|
100
|
+
if (signal.aborted) return;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this.videoAssetTask = new Task(this, {
|
|
104
|
+
autoRun: true,
|
|
105
|
+
args: () => [this.effectiveMode, this.mediaSegmentsTask.value],
|
|
106
|
+
onError: (error) => {
|
|
107
|
+
console.error("videoAsset task error", error);
|
|
108
|
+
},
|
|
109
|
+
task: async ([mode, _files], { signal: _signal }) => {
|
|
110
|
+
await this.mediaSegmentsTask.taskComplete;
|
|
111
|
+
if (_signal.aborted) return void 0;
|
|
112
|
+
await this.fragmentIndexTask.taskComplete;
|
|
113
|
+
if (_signal.aborted) return void 0;
|
|
114
|
+
const files = this.mediaSegmentsTask.value;
|
|
115
|
+
const fragmentIndex = this.fragmentIndexTask.value;
|
|
116
|
+
if (!files) {
|
|
117
|
+
log("trace: videoAsset task aborted - no files");
|
|
118
|
+
throw new Error(`Video asset creation failed: No media segment files available. This indicates a problem with media segment loading for source: "${this.src}"`);
|
|
119
|
+
}
|
|
120
|
+
const computedVideoTrackId = Object.values(fragmentIndex ?? {}).find((track) => track.type === "video")?.track;
|
|
121
|
+
if (computedVideoTrackId === void 0) {
|
|
122
|
+
log("trace: videoAsset task aborted - no video track");
|
|
123
|
+
throw new Error(`Video asset creation failed: No video track found in media segments. Source may not contain video content: "${this.src}"`);
|
|
124
|
+
}
|
|
125
|
+
const videoFile = files[computedVideoTrackId];
|
|
126
|
+
if (!videoFile) {
|
|
127
|
+
log("trace: videoAsset task aborted - no video file");
|
|
128
|
+
throw new Error(`Video asset creation failed: Video file not available for track ${computedVideoTrackId}. Media segment loading may have failed for source: "${this.src}"`);
|
|
129
|
+
}
|
|
130
|
+
const existingAsset = this.videoAssetTask.value;
|
|
131
|
+
if (existingAsset) {
|
|
132
|
+
for (const frame of existingAsset?.decodedFrames || []) frame.close();
|
|
133
|
+
const decoder = existingAsset?.videoDecoder;
|
|
134
|
+
if (decoder && decoder.state !== "closed") decoder.close();
|
|
135
|
+
}
|
|
136
|
+
if (_signal.aborted) return void 0;
|
|
137
|
+
log("trace: creating video asset", { mode });
|
|
138
|
+
const videoTrackFragmentIndex = Object.values(fragmentIndex ?? {}).find((track) => track.type === "video");
|
|
139
|
+
const startTimeOffsetMs = Number((videoTrackFragmentIndex?.startTimeOffsetMs ?? 0).toFixed(5));
|
|
140
|
+
if (mode === "jit-transcode") {
|
|
141
|
+
const result$1 = await VideoAsset.createFromCompleteMP4(`jit-segment-${computedVideoTrackId}`, videoFile, { startTimeOffsetMs });
|
|
142
|
+
return result$1;
|
|
143
|
+
}
|
|
144
|
+
const result = await VideoAsset.createFromReadableStream("video.mp4", videoFile.stream(), videoFile, { startTimeOffsetMs });
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
this.paintTask = new Task(this, {
|
|
149
|
+
args: () => [this.desiredSeekTimeMs],
|
|
150
|
+
onError: (error) => {
|
|
151
|
+
console.error("paintTask error", error);
|
|
152
|
+
},
|
|
153
|
+
task: async ([_seekToMs], { signal }) => {
|
|
154
|
+
const isProductionRendering = this.isInProductionRenderingMode();
|
|
155
|
+
if (!isProductionRendering) {
|
|
156
|
+
if (!this.rootTimegroup || this.rootTimegroup.currentTimeMs === 0 && this.desiredSeekTimeMs === 0) return;
|
|
157
|
+
} else {
|
|
158
|
+
if (!this.rootTimegroup) return;
|
|
159
|
+
if (!this.isFrameRenderingActive()) return;
|
|
160
|
+
}
|
|
161
|
+
if (signal.aborted) return;
|
|
162
|
+
await this.mediaSegmentsTask.taskComplete;
|
|
163
|
+
if (signal.aborted) return;
|
|
164
|
+
await this.videoAssetTask.taskComplete;
|
|
165
|
+
if (signal.aborted) return;
|
|
166
|
+
const videoAsset = this.videoAssetTask.value;
|
|
167
|
+
const currentSeekToMs = this.desiredSeekTimeMs;
|
|
168
|
+
if (!videoAsset) {
|
|
169
|
+
log("trace: paintTask aborted - no video asset");
|
|
170
|
+
throw new Error(`Frame rendering failed: No video asset available. This may indicate a problem with video loading or an invalid source: "${this.src}"`);
|
|
171
|
+
}
|
|
172
|
+
if (this.#decoderNeedsReset) try {
|
|
173
|
+
if (videoAsset?.videoDecoder) videoAsset.configureDecoder();
|
|
174
|
+
else console.warn("No video decoder available for reset");
|
|
175
|
+
this.#decoderNeedsReset = false;
|
|
176
|
+
} catch (resetError) {
|
|
177
|
+
console.error("reset error", resetError);
|
|
178
|
+
throw new Error(`Frame rendering failed: Unable to reset video decoder after previous error. Decoder state: ${resetError instanceof Error ? resetError.message : "Unknown error"}. Try refreshing the page or reloading the video.`);
|
|
179
|
+
}
|
|
180
|
+
if (signal.aborted) return;
|
|
181
|
+
if (this.#decoderLock) return;
|
|
182
|
+
try {
|
|
183
|
+
this.#decoderLock = true;
|
|
184
|
+
const currentVideoAsset = this.videoAssetTask.value;
|
|
185
|
+
if (videoAsset !== currentVideoAsset) return;
|
|
186
|
+
const decoderState = videoAsset?.videoDecoder?.state;
|
|
187
|
+
if (decoderState === "closed") return;
|
|
188
|
+
if (this.effectiveMode === "jit-transcode" && this.scrubTrackManager) {
|
|
189
|
+
const shouldUseScrub = this.scrubTrackManager.shouldUseScrubTrack(currentSeekToMs);
|
|
190
|
+
const isFastSeeking = this.scrubTrackManager.isFastSeeking(this.lastSeekTimeMs, currentSeekToMs);
|
|
191
|
+
if (shouldUseScrub || isFastSeeking) try {
|
|
192
|
+
this.startDelayedLoading("scrub-segment-load", "Loading scrub segment...");
|
|
193
|
+
const scrubFrame = await this.scrubTrackManager.getScrubFrame(currentSeekToMs);
|
|
194
|
+
if (scrubFrame && this.canvasElement) {
|
|
195
|
+
this.scrubTrackManager.recordCacheMiss();
|
|
196
|
+
this.lastSeekTimeMs = currentSeekToMs;
|
|
197
|
+
this.clearDelayedLoading("scrub-segment-load");
|
|
198
|
+
return this.displayFrame(scrubFrame, currentSeekToMs);
|
|
199
|
+
}
|
|
200
|
+
console.warn("Scrub track returned null frame, falling back to normal video");
|
|
201
|
+
this.clearDelayedLoading("scrub-segment-load");
|
|
202
|
+
this.startDelayedLoading("video-segment-fallback", "Loading high quality video...");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.clearDelayedLoading("scrub-segment-load");
|
|
205
|
+
console.warn("Scrub track failed, falling back to normal video:", error);
|
|
206
|
+
this.startDelayedLoading("video-segment-fallback", "Loading high quality video...");
|
|
207
|
+
}
|
|
208
|
+
else this.scrubTrackManager?.recordCacheHit();
|
|
209
|
+
}
|
|
210
|
+
const shouldShowLoading = !this.delayedLoadingState.isLoading && (this.effectiveMode !== "asset" || !videoAsset);
|
|
211
|
+
if (shouldShowLoading) this.startDelayedLoading("video-segment", "Loading video segment...");
|
|
212
|
+
this.lastSeekTimeMs = currentSeekToMs;
|
|
213
|
+
const result = await this.renderNormalVideo(videoAsset, currentSeekToMs);
|
|
214
|
+
this.clearDelayedLoading("video-segment");
|
|
215
|
+
this.clearDelayedLoading("video-segment-fallback");
|
|
216
|
+
return result;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.clearDelayedLoading("scrub-segment-load");
|
|
219
|
+
this.clearDelayedLoading("video-segment");
|
|
220
|
+
this.clearDelayedLoading("video-segment-fallback");
|
|
221
|
+
if (error instanceof Error) {
|
|
222
|
+
if (error.name === "DataError" && error.message.includes("key frame is required")) {
|
|
223
|
+
console.warn("Decoder reset during VideoAsset due to key frame requirement");
|
|
224
|
+
this.#decoderNeedsReset = true;
|
|
225
|
+
if (this.effectiveMode === "jit-transcode") this.requestUpdate();
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
if (error.name === "AbortError") throw new Error("Frame rendering cancelled: Operation was aborted, likely due to a new seek request or component unmounting.");
|
|
229
|
+
if (error.message.includes("VideoAsset decoder closed") || error.message.includes("recreation in progress")) return;
|
|
230
|
+
if (error.name === "InvalidStateError" && error.message.includes("closed codec")) return;
|
|
231
|
+
console.warn("Decoder reset during VideoAsset recreation", error);
|
|
232
|
+
this.#decoderNeedsReset = true;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
throw new Error(`Frame rendering failed: Unknown error during video rendering at ${currentSeekToMs}ms. Error: ${String(error)}`);
|
|
236
|
+
} finally {
|
|
237
|
+
this.#decoderLock = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
this.delayedLoadingState = new DelayedLoadingState(250, (isLoading, message) => {
|
|
242
|
+
this.setLoadingState(isLoading, null, message);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
render() {
|
|
246
|
+
return html`
|
|
247
|
+
<canvas ${ref(this.canvasRef)}></canvas>
|
|
248
|
+
${this.loadingState.isLoading ? html`
|
|
249
|
+
<div class="loading-overlay">
|
|
250
|
+
<div class="loading-content">
|
|
251
|
+
<div class="loading-spinner"></div>
|
|
252
|
+
<div>
|
|
253
|
+
<div>Loading Video...</div>
|
|
254
|
+
<div class="loading-message">${this.loadingState.message}</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
` : ""}
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
get canvasElement() {
|
|
262
|
+
return this.canvasRef.value;
|
|
263
|
+
}
|
|
264
|
+
#decoderLock = false;
|
|
265
|
+
#decoderNeedsReset = false;
|
|
266
|
+
get frameTaskStatus() {
|
|
267
|
+
return {
|
|
268
|
+
desiredSeekTimeMs: this.desiredSeekTimeMs,
|
|
269
|
+
fragmentIndexTask: printTaskStatus(this.fragmentIndexTask.status),
|
|
270
|
+
seekTask: printTaskStatus(this.seekTask.status),
|
|
271
|
+
mediaSegmentsTask: printTaskStatus(this.mediaSegmentsTask.status),
|
|
272
|
+
assetSegmentLoader: printTaskStatus(this.assetSegmentLoader.status),
|
|
273
|
+
assetSegmentKeysTask: printTaskStatus(this.assetSegmentKeysTask.status),
|
|
274
|
+
assetInitSegmentsTask: printTaskStatus(this.assetInitSegmentsTask.status),
|
|
275
|
+
videoAssetTask: printTaskStatus(this.videoAssetTask.status),
|
|
276
|
+
paintTask: printTaskStatus(this.paintTask.status),
|
|
277
|
+
frameTask: printTaskStatus(this.frameTask.status)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
#lastVideoAsset = null;
|
|
281
|
+
updated(changedProperties) {
|
|
282
|
+
super.updated(changedProperties);
|
|
283
|
+
const currentVideoAsset = this.videoAssetTask.value;
|
|
284
|
+
if (currentVideoAsset !== this.#lastVideoAsset) this.#lastVideoAsset = currentVideoAsset;
|
|
285
|
+
this.initializeScrubTrackManager();
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Initialize scrub track manager if needed
|
|
289
|
+
*/
|
|
290
|
+
async initializeScrubTrackManager() {
|
|
291
|
+
const mode = this.effectiveMode;
|
|
292
|
+
if (mode === "jit-transcode" && this.src && !this.scrubTrackManager) {
|
|
293
|
+
const jitClient = this.jitClientTask.value;
|
|
294
|
+
if (jitClient) try {
|
|
295
|
+
this.scrubTrackManager = new ScrubTrackManager(this.src, jitClient, { onLoadingStateChange: (isLoading, message) => {
|
|
296
|
+
if (isLoading) this.startDelayedLoading("scrub-segment", message || "Loading scrub track...");
|
|
297
|
+
else this.clearDelayedLoading("scrub-segment");
|
|
298
|
+
} });
|
|
299
|
+
await this.scrubTrackManager.initialize();
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn("Failed to initialize scrub track manager:", error);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Start a delayed loading operation for testing
|
|
307
|
+
*/
|
|
308
|
+
startDelayedLoading(operationId, message, options = {}) {
|
|
309
|
+
this.delayedLoadingState.startLoading(operationId, message, options);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Clear a delayed loading operation for testing
|
|
313
|
+
*/
|
|
314
|
+
clearDelayedLoading(operationId) {
|
|
315
|
+
this.delayedLoadingState.clearLoading(operationId);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Set loading state for user feedback
|
|
319
|
+
*/
|
|
320
|
+
setLoadingState(isLoading, operation = null, message = "") {
|
|
321
|
+
this.loadingState = {
|
|
322
|
+
isLoading,
|
|
323
|
+
operation,
|
|
324
|
+
message
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Render normal video using existing logic
|
|
329
|
+
*/
|
|
330
|
+
async renderNormalVideo(videoAsset, seekToMs) {
|
|
331
|
+
let targetSeekTimeSeconds = seekToMs / 1e3;
|
|
332
|
+
try {
|
|
333
|
+
const currentVideoAsset = this.videoAssetTask.value;
|
|
334
|
+
if (videoAsset !== currentVideoAsset) throw new Error("VideoAsset decoder closed during seek - recreation in progress");
|
|
335
|
+
const decoderState = videoAsset?.videoDecoder?.state;
|
|
336
|
+
if (decoderState === "closed") throw new Error("VideoAsset decoder closed during seek - recreation in progress");
|
|
337
|
+
if (this.effectiveMode === "jit-transcode") targetSeekTimeSeconds %= 2;
|
|
338
|
+
const frame = await videoAsset.seekToTime(targetSeekTimeSeconds);
|
|
339
|
+
if (frame) {
|
|
340
|
+
const finalVideoAsset = this.videoAssetTask.value;
|
|
341
|
+
if (videoAsset !== finalVideoAsset) {
|
|
342
|
+
frame.close();
|
|
343
|
+
throw new Error("VideoAsset decoder closed during seek - recreation in progress");
|
|
344
|
+
}
|
|
345
|
+
const finalSeekToMs = this.desiredSeekTimeMs;
|
|
346
|
+
return this.displayFrame(frame, finalSeekToMs);
|
|
347
|
+
}
|
|
348
|
+
log("trace: no frame returned from seekToTime");
|
|
349
|
+
throw new Error(`Frame rendering failed: No frame available at time ${seekToMs}ms (${targetSeekTimeSeconds}s). This may indicate seeking beyond video duration, corrupted video data, or an incompatible video format.`);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (error instanceof Error && (error.message.includes("VideoAsset decoder closed") || error.message.includes("recreation in progress"))) throw error;
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Display a video frame on the canvas
|
|
357
|
+
*/
|
|
358
|
+
displayFrame(frame, seekToMs) {
|
|
359
|
+
log("trace: displayFrame start", {
|
|
360
|
+
seekToMs,
|
|
361
|
+
frameFormat: frame.format
|
|
362
|
+
});
|
|
363
|
+
if (!this.canvasElement) {
|
|
364
|
+
log("trace: displayFrame aborted - no canvas element");
|
|
365
|
+
throw new Error(`Frame display failed: Canvas element is not available at time ${seekToMs}ms. The video component may not be properly initialized.`);
|
|
366
|
+
}
|
|
367
|
+
const ctx = this.canvasElement.getContext("2d");
|
|
368
|
+
if (!ctx) {
|
|
369
|
+
log("trace: displayFrame aborted - no canvas context");
|
|
370
|
+
throw new Error(`Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`);
|
|
371
|
+
}
|
|
372
|
+
if (frame?.codedWidth && frame?.codedHeight) {
|
|
373
|
+
if (this.canvasElement.width !== frame.codedWidth || this.canvasElement.height !== frame.codedHeight) {
|
|
374
|
+
log("trace: updating canvas dimensions", {
|
|
375
|
+
width: frame.codedWidth,
|
|
376
|
+
height: frame.codedHeight
|
|
377
|
+
});
|
|
378
|
+
this.canvasElement.width = frame.codedWidth;
|
|
379
|
+
this.canvasElement.height = frame.codedHeight;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (frame.format === null) {
|
|
383
|
+
log("trace: displayFrame aborted - null frame format");
|
|
384
|
+
throw new Error(`Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`);
|
|
385
|
+
}
|
|
386
|
+
log("trace: drawing frame to canvas");
|
|
387
|
+
ctx.drawImage(frame, 0, 0, this.canvasElement.width, this.canvasElement.height);
|
|
388
|
+
log("trace: frame drawn to canvas", { seekToMs });
|
|
389
|
+
return seekToMs;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Check if we're in production rendering mode (EF_FRAMEGEN active) vs preview mode
|
|
393
|
+
*/
|
|
394
|
+
isInProductionRenderingMode() {
|
|
395
|
+
if (typeof window.EF_RENDERING === "function") return window.EF_RENDERING();
|
|
396
|
+
const workbench = document.querySelector("ef-workbench");
|
|
397
|
+
if (workbench?.rendering) return true;
|
|
398
|
+
if (window.EF_FRAMEGEN?.renderOptions) return true;
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check if EF_FRAMEGEN has explicitly started frame rendering (not just initialization)
|
|
403
|
+
*/
|
|
404
|
+
isFrameRenderingActive() {
|
|
405
|
+
if (!window.EF_FRAMEGEN?.renderOptions) return false;
|
|
406
|
+
const renderOptions = window.EF_FRAMEGEN.renderOptions;
|
|
407
|
+
const renderStartTime = renderOptions.encoderOptions.fromMs;
|
|
408
|
+
const currentTime = this.rootTimegroup?.currentTimeMs || 0;
|
|
409
|
+
return currentTime >= renderStartTime;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get scrub track performance statistics
|
|
413
|
+
*/
|
|
414
|
+
getScrubTrackStats() {
|
|
415
|
+
return this.scrubTrackManager?.getCacheStats() || null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Clean up resources when component is disconnected
|
|
419
|
+
*/
|
|
420
|
+
disconnectedCallback() {
|
|
421
|
+
super.disconnectedCallback();
|
|
422
|
+
this.scrubTrackManager?.cleanup();
|
|
423
|
+
this.delayedLoadingState.clearAllLoading();
|
|
424
|
+
}
|
|
131
425
|
};
|
|
426
|
+
_decorate([state()], EFVideo.prototype, "loadingState", void 0);
|
|
427
|
+
EFVideo = _decorate([customElement("ef-video")], EFVideo);
|
|
428
|
+
export { EFVideo };
|