demuxe 0.3.0-beta.3
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/LICENSE +352 -0
- package/README.md +75 -0
- package/bin/demuxe.mjs +30 -0
- package/docs/API-MIGRATION.md +54 -0
- package/docs/BETA.md +98 -0
- package/docs/BRANDING-MIGRATION.md +77 -0
- package/docs/COMPATIBILITY-EXPANSION.md +181 -0
- package/docs/LICENSING.md +83 -0
- package/docs/OPTIMIZATION-COMPLETION.md +511 -0
- package/docs/OPTIMIZATION-FLAC.md +178 -0
- package/docs/OPTIMIZATION-INTEGRATION.md +221 -0
- package/docs/OPTIMIZATION-REVIEW-FIXES.md +78 -0
- package/docs/PLAYER-COMPONENT.md +247 -0
- package/docs/PUBLIC-API-VALIDATION.md +163 -0
- package/docs/PUBLIC-API.md +245 -0
- package/docs/RELEASE.md +181 -0
- package/docs/RUNTIME-ASSETS.md +127 -0
- package/engine-build.json +8781 -0
- package/examples/custom-controls.html +14 -0
- package/examples/player-element.html +5 -0
- package/fixtures/DejaVuSans.ttf +0 -0
- package/fixtures/FONT-LICENSE.txt +187 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +46 -0
- package/player.d.ts +1 -0
- package/player.js +1 -0
- package/release-manifest.json +541 -0
- package/sources.lock.json +96 -0
- package/third_party/notices/dav1d/COPYING +23 -0
- package/third_party/notices/emscripten/LICENSE +102 -0
- package/third_party/notices/emscripten/system/lib/compiler-rt/LICENSE.TXT +311 -0
- package/third_party/notices/emscripten/system/lib/libc/musl/COPYRIGHT +193 -0
- package/third_party/notices/emscripten/system/lib/libcxx/LICENSE.TXT +311 -0
- package/third_party/notices/emscripten/system/lib/libcxxabi/LICENSE.TXT +311 -0
- package/third_party/notices/emsdk/LICENSE +27 -0
- package/third_party/notices/ffmpeg/COPYING.GPLv2 +339 -0
- package/third_party/notices/ffmpeg/COPYING.GPLv3 +674 -0
- package/third_party/notices/ffmpeg/COPYING.LGPLv2.1 +502 -0
- package/third_party/notices/ffmpeg/LICENSE.md +129 -0
- package/third_party/notices/ffmpeg-adaptation/COPYING.LGPLv2.1 +502 -0
- package/third_party/notices/ffmpeg-adaptation/LICENSE.md +127 -0
- package/third_party/notices/freetype/LICENSE.TXT +46 -0
- package/third_party/notices/freetype/docs/FTL.TXT +169 -0
- package/third_party/notices/freetype/docs/GPLv2.TXT +340 -0
- package/third_party/notices/fribidi/COPYING +504 -0
- package/third_party/notices/harfbuzz/COPYING +42 -0
- package/third_party/notices/libass/COPYING +15 -0
- package/third_party/notices/libplacebo/LICENSE +458 -0
- package/third_party/notices/libxml2/Copyright +23 -0
- package/third_party/notices/libxml2/dict.c.notice +17 -0
- package/third_party/notices/libxml2/list.c.notice +16 -0
- package/third_party/notices/mpv/Copyright +78 -0
- package/third_party/notices/mpv/LICENSE.GPL +338 -0
- package/third_party/notices/mpv/LICENSE.LGPL +501 -0
- package/third_party/notices/vulkan-headers/LICENSE.txt +202 -0
- package/third_party/notices/zimg/COPYING +14 -0
- package/third_party/notices/zlib/LICENSE +22 -0
- package/third_party/notices.json +166 -0
- package/toolchain.lock.json +204 -0
- package/web/audio-worklet.js +45 -0
- package/web/cheap-mp4-probe.js +82 -0
- package/web/engine-adaptation/manifest.json +75 -0
- package/web/engine-adaptation/remux.mjs +2 -0
- package/web/engine-adaptation/remux.wasm +0 -0
- package/web/engine-ass/manifest.json +55 -0
- package/web/engine-ass/subtitles.mjs +2 -0
- package/web/engine-ass/subtitles.wasm +0 -0
- package/web/engine-hybrid/player.mjs +2 -0
- package/web/engine-hybrid/player.wasm +0 -0
- package/web/engine-remux/remux.mjs +2 -0
- package/web/engine-remux/remux.wasm +0 -0
- package/web/engine-software-full/player.mjs +2 -0
- package/web/engine-software-full/player.wasm +0 -0
- package/web/file-reader.js +34 -0
- package/web/filter-retained-engine-worker.js +292 -0
- package/web/generated/index.d.ts +6 -0
- package/web/generated/index.js +4 -0
- package/web/generated/internal/assets.d.ts +2 -0
- package/web/generated/internal/assets.js +7 -0
- package/web/generated/internal/backend.d.ts +27 -0
- package/web/generated/internal/backend.js +1 -0
- package/web/generated/internal/errors.d.ts +13 -0
- package/web/generated/internal/errors.js +54 -0
- package/web/generated/internal/native-ass.d.ts +38 -0
- package/web/generated/internal/native-ass.js +206 -0
- package/web/generated/internal/native-player.d.ts +95 -0
- package/web/generated/internal/native-player.js +502 -0
- package/web/generated/internal/playback-plans.d.ts +526 -0
- package/web/generated/internal/playback-plans.js +110 -0
- package/web/generated/internal/selection.d.ts +45 -0
- package/web/generated/internal/selection.js +45 -0
- package/web/generated/internal/state.d.ts +8 -0
- package/web/generated/internal/state.js +56 -0
- package/web/generated/internal/wasm-player.d.ts +127 -0
- package/web/generated/internal/wasm-player.js +372 -0
- package/web/generated/player/index.d.ts +198 -0
- package/web/generated/player/index.js +873 -0
- package/web/generated/player/interaction.d.ts +6 -0
- package/web/generated/player/interaction.js +14 -0
- package/web/generated/player/styles.d.ts +1 -0
- package/web/generated/player/styles.js +86 -0
- package/web/generated/types.d.ts +227 -0
- package/web/generated/types.js +2 -0
- package/web/generated/unified-player.d.ts +129 -0
- package/web/generated/unified-player.js +1074 -0
- package/web/io-worker.js +65 -0
- package/web/native-ass-worker.js +26 -0
- package/web/native-remux-player.js +301 -0
- package/web/native-remux-source-worker.js +25 -0
- package/web/native-remux-worker.js +46 -0
- package/web/range-reader.js +130 -0
- package/web/remux-packaging.js +11 -0
- package/web/resource-loader.js +173 -0
- package/web/retained-decoder-worker.js +200 -0
- package/web/retained-video.js +15 -0
- package/web/segmented-subtitles.js +37 -0
- package/web/software-full-engine-worker.js +234 -0
- package/web/source-probe.js +28 -0
- package/web/split-mp4.js +159 -0
- package/web/streaming-manifest.js +136 -0
- package/web/subtitle-overlay.js +26 -0
- package/web/video-codec-config.js +59 -0
- package/web/vod-manifest.js +54 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
import { Player } from '../unified-player.js';
|
|
2
|
+
import { PLAYER_EVENTS } from '../types.js';
|
|
3
|
+
import { PlayerError, playerError } from '../internal/errors.js';
|
|
4
|
+
import { formatTime, outputDimensions, shortcut } from './interaction.js';
|
|
5
|
+
import { styles } from './styles.js';
|
|
6
|
+
// Mirror only the arrow so both directions retain upright, centered numerals.
|
|
7
|
+
const seekArrow = '<path d="M10 5h2a8 8 0 1 1-8 8M13 2l-3 3 3 3"/>';
|
|
8
|
+
const seekSeconds = '<text x="12" y="16" text-anchor="middle" fill="currentColor" stroke="none" font-size="8.5" font-weight="450" font-family="system-ui,sans-serif">10</text>';
|
|
9
|
+
const icons = {
|
|
10
|
+
previous: '<path d="M5 5v14m14-14L8 12l11 7Z" fill="currentColor"/>',
|
|
11
|
+
next: '<path d="M19 5v14M5 5l11 7-11 7Z" fill="currentColor"/>',
|
|
12
|
+
eyeOff: '<path d="m3 3 18 18M10.6 5.1A11 11 0 0 1 12 5c6.5 0 10 7 10 7a18 18 0 0 1-3.2 4.1M6.1 6.1A19 19 0 0 0 2 12s3.5 7 10 7a12 12 0 0 0 5.1-1.2M9.9 9.9a3 3 0 0 0 4.2 4.2"/>',
|
|
13
|
+
eye: '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/>',
|
|
14
|
+
back: `${seekArrow}${seekSeconds}`,
|
|
15
|
+
forward: `<g transform="translate(24 0) scale(-1 1)">${seekArrow}</g>${seekSeconds}`,
|
|
16
|
+
play: '<path d="m9 5 11 7-11 7Z" fill="currentColor" stroke="none"/>',
|
|
17
|
+
pause: '<path d="M8 5v14M16 5v14" stroke-width="4"/>',
|
|
18
|
+
volume: '<path d="m11 5-6 4H2v6h3l6 4Z"/><path d="M15 8a6 6 0 0 1 0 8m3-11a10 10 0 0 1 0 14"/>',
|
|
19
|
+
muted: '<path d="m11 5-6 4H2v6h3l6 4Z"/><path d="m16 9 6 6m0-6-6 6"/>',
|
|
20
|
+
settings: '<path d="M9 3h6l.6 2.3 2 1.2 2.3-.6 3 5.2-1.7 1.7v2.4l1.7 1.7-3 5.2-2.3-.6-2 1.2L15 24H9l-.6-2.3-2-1.2-2.3.6-3-5.2 1.7-1.7v-2.4L1.1 11l3-5.2 2.3.6 2-1.2Z" transform="translate(1 0) scale(.9)"/><circle cx="12" cy="12" r="3"/>',
|
|
21
|
+
expand: '<path d="M8 3H3v5m13-5h5v5M3 16v5h5m13-5v5h-5"/>',
|
|
22
|
+
collapse: '<path d="M3 8h5V3m8 0v5h5M8 21v-5H3m13 5v-5h5"/>',
|
|
23
|
+
folder: '<path d="M3 7V5a1 1 0 0 1 1-1h5l2 3h9a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7Z"/>',
|
|
24
|
+
folderOpen: '<path d="M3 8V5a1 1 0 0 1 1-1h5l2 3h9a1 1 0 0 1 1 1v2M3 8h17a1 1 0 0 1 1 1l-2 10H3L1 9a1 1 0 0 1 1-1Z"/>',
|
|
25
|
+
close: '<path d="m6 6 12 12M18 6 6 18"/>'
|
|
26
|
+
};
|
|
27
|
+
const Base = (typeof HTMLElement === 'undefined' ? class {
|
|
28
|
+
} : HTMLElement);
|
|
29
|
+
export const defaultLabels = Object.freeze({ diagnostics: 'Session diagnostics', moreOptions: 'More options', back: 'Seek backward 10 seconds', forward: 'Seek forward 10 seconds', play: 'Play', pause: 'Pause', mute: 'Mute', unmute: 'Unmute', seek: 'Playback position', volume: 'Volume', settings: 'Playback settings', closeSettings: 'Close settings', speed: 'Playback speed', audio: 'Audio', subtitles: 'Subtitles', automatic: 'Automatic', off: 'Off', fullscreen: 'Fullscreen', exitFullscreen: 'Exit fullscreen', open: 'Open media', addSubtitle: 'Add subtitles', empty: 'Something good to watch?', drop: 'Open a video or audio file from your device.', loading: 'Opening media…', switching: 'Updating playback…', seeking: 'Seeking…', buffering: 'Buffering…', live: 'LIVE', unknown: 'Unknown duration', retry: 'Retry', resume: 'Press Play to continue', shortcuts: 'K / Space: play · ← → / J L: seek · ↑ ↓: volume · M: mute · C: subtitles · [ ]: speed · 0–9 / Home / End: position · F: fullscreen', noFullscreen: 'Fullscreen is unavailable here. Open this page in a browser tab.', noWindow: 'Live playback · seek window unavailable', openURL: 'Open URL', closeMedia: 'Close media', url: 'Media URL', format: 'Source format', streamLive: 'Live stream', addFiles: 'Add files', queue: 'Queue', clearQueue: 'Clear queue', previous: 'Previous file', next: 'Next file', remove: 'Remove', unnamed: 'Unnamed media', mediaFile: 'Media file', noMedia: 'No media loaded', loadedMedia: 'Media loaded', subtitleFile: 'Subtitle file' });
|
|
30
|
+
// Never display opaque URL payloads, origins, credentials, queries or fragments.
|
|
31
|
+
function sourceTitle(source) {
|
|
32
|
+
if (typeof File !== 'undefined' && source instanceof File)
|
|
33
|
+
return source.name;
|
|
34
|
+
const value = typeof source === 'string' || source instanceof URL ? source :
|
|
35
|
+
'url' in source ? source.url : undefined;
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
return '';
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(String(value), document.baseURI);
|
|
40
|
+
if (!['http:', 'https:', 'file:'].includes(url.protocol))
|
|
41
|
+
return '';
|
|
42
|
+
const filename = url.pathname.split('/').at(-1) ?? '';
|
|
43
|
+
try {
|
|
44
|
+
return decodeURIComponent(filename);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return filename;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class DemuxePlayerElement extends Base {
|
|
55
|
+
static observedAttributes = ['src', 'controls', 'poster', 'autoplay', 'muted', 'asset-base', 'title', 'title-mode'];
|
|
56
|
+
core;
|
|
57
|
+
queueItems = [];
|
|
58
|
+
queueIndex = -1;
|
|
59
|
+
queueOperation;
|
|
60
|
+
queuePlayIntent;
|
|
61
|
+
queueSourceId = null;
|
|
62
|
+
queueEndedId = null;
|
|
63
|
+
queueSignature = '';
|
|
64
|
+
queueRevision = 0;
|
|
65
|
+
queueRenderSignature = '';
|
|
66
|
+
queueItem(source, options = {}) {
|
|
67
|
+
let name = '';
|
|
68
|
+
try {
|
|
69
|
+
name = sourceTitle(source);
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
return { source, options: { ...options, signal: undefined }, name };
|
|
73
|
+
}
|
|
74
|
+
resetQueue() {
|
|
75
|
+
this.queueRevision++;
|
|
76
|
+
this.queueItems = [];
|
|
77
|
+
this.queueIndex = -1;
|
|
78
|
+
this.queueOperation = undefined;
|
|
79
|
+
this.queueSourceId = null;
|
|
80
|
+
this.queueEndedId = null;
|
|
81
|
+
this.renderQueue();
|
|
82
|
+
}
|
|
83
|
+
async activateQueue(index, playAfter = this.core?.state.playbackIntent === 'play' || this.core?.state.status === 'ended', options, closePreviousOnFailure = false) {
|
|
84
|
+
if (this.terminal)
|
|
85
|
+
throw new PlayerError('ABORTED', 'Player element is destroyed');
|
|
86
|
+
const item = this.queueItems[index];
|
|
87
|
+
if (!item)
|
|
88
|
+
return;
|
|
89
|
+
const operation = this.queueOperation = Symbol();
|
|
90
|
+
this.queuePlayIntent = undefined;
|
|
91
|
+
this.queueIndex = index;
|
|
92
|
+
this.queueSourceId = null;
|
|
93
|
+
this.queueEndedId = null;
|
|
94
|
+
this.renderQueue();
|
|
95
|
+
try {
|
|
96
|
+
await this.openSource(item.source, options ?? item.options);
|
|
97
|
+
if (this.queueOperation !== operation)
|
|
98
|
+
throw new PlayerError('ABORTED', 'Queue selection superseded');
|
|
99
|
+
this.queueSourceId = this.core?.state.sourceId ?? null;
|
|
100
|
+
if (this.queuePlayIntent ?? (typeof playAfter === 'function' ? playAfter() : playAfter))
|
|
101
|
+
await this.core?.play();
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// A removed item must not survive as the core's rollback source.
|
|
105
|
+
if (closePreviousOnFailure && this.queueOperation === operation && this.queueSourceId === null)
|
|
106
|
+
await this.core?.close();
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (this.queueOperation === operation) {
|
|
111
|
+
this.queueOperation = undefined;
|
|
112
|
+
this.renderQueue();
|
|
113
|
+
queueMicrotask(() => this.advanceQueue());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
addFiles(files) {
|
|
118
|
+
if (this.terminal || !files.length)
|
|
119
|
+
return;
|
|
120
|
+
const start = this.queueItems.length;
|
|
121
|
+
this.queueRevision++;
|
|
122
|
+
for (const file of files)
|
|
123
|
+
this.queueItems.push(this.queueItem(file));
|
|
124
|
+
this.settings(false, false);
|
|
125
|
+
this.$('stage').focus({ preventScroll: true });
|
|
126
|
+
this.renderQueue();
|
|
127
|
+
if (start === 0)
|
|
128
|
+
this.run(this.activateQueue(0, () => this.autoplay));
|
|
129
|
+
}
|
|
130
|
+
selectQueue(index) {
|
|
131
|
+
if (this.terminal || this.queueOperation || this.core?.state.pendingOperation)
|
|
132
|
+
return;
|
|
133
|
+
this.settings(false, false);
|
|
134
|
+
this.$('stage').focus({ preventScroll: true });
|
|
135
|
+
this.run(this.activateQueue(index));
|
|
136
|
+
}
|
|
137
|
+
removeQueueItem(index) {
|
|
138
|
+
if (!this.showSourceControls || this.queueOperation || this.core?.state.pendingOperation || index < 0 || index >= this.queueItems.length)
|
|
139
|
+
return;
|
|
140
|
+
const wasCurrent = index === this.queueIndex;
|
|
141
|
+
this.queueRevision++;
|
|
142
|
+
this.queueItems.splice(index, 1);
|
|
143
|
+
if (!this.queueItems.length) {
|
|
144
|
+
this.settings(false, false);
|
|
145
|
+
this.$('stage').focus({ preventScroll: true });
|
|
146
|
+
this.run(this.close());
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (index < this.queueIndex)
|
|
150
|
+
this.queueIndex--;
|
|
151
|
+
if (wasCurrent) {
|
|
152
|
+
this.settings(false, false);
|
|
153
|
+
this.$('stage').focus({ preventScroll: true });
|
|
154
|
+
this.run(this.activateQueue(Math.min(index, this.queueItems.length - 1), undefined, undefined, true));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.renderQueue();
|
|
158
|
+
this.$('queue-list').querySelector('button')?.focus();
|
|
159
|
+
}
|
|
160
|
+
advanceQueue() {
|
|
161
|
+
const state = this.core?.state;
|
|
162
|
+
if (!state || this.terminal || this.queueOperation || state.pendingOperation || state.status !== 'ended' || state.sourceId !== this.queueSourceId || this.queueEndedId === state.sourceId || this.queueIndex >= this.queueItems.length - 1)
|
|
163
|
+
return;
|
|
164
|
+
this.queueEndedId = state.sourceId;
|
|
165
|
+
this.run(this.activateQueue(this.queueIndex + 1, true));
|
|
166
|
+
}
|
|
167
|
+
renderQueue() {
|
|
168
|
+
if (!this.shadowRoot?.getElementById('queue-list'))
|
|
169
|
+
return;
|
|
170
|
+
const busy = !!this.queueOperation || !!this.core?.state.pendingOperation, labels = this.labels;
|
|
171
|
+
const renderSignature = JSON.stringify([this.queueRevision, this.queueIndex, busy, this.showSourceControls, labels.queue, labels.clearQueue, labels.previous, labels.next, labels.remove, labels.unnamed, labels.open, labels.addFiles]);
|
|
172
|
+
if (renderSignature === this.queueRenderSignature)
|
|
173
|
+
return;
|
|
174
|
+
this.queueRenderSignature = renderSignature;
|
|
175
|
+
this.$('queue-section').hidden = !this.queueItems.length;
|
|
176
|
+
this.$('queue-navigation').hidden = this.queueItems.length < 2;
|
|
177
|
+
this.$('queue-count').textContent = `${this.queueIndex + 1} / ${this.queueItems.length}`;
|
|
178
|
+
this.$('choose-file').textContent = this.queueItems.length ? this.labels.addFiles : this.labels.open;
|
|
179
|
+
this.$('queue-heading').textContent = this.labels.queue;
|
|
180
|
+
this.$('clear-queue').textContent = this.labels.clearQueue;
|
|
181
|
+
this.$('clear-queue').disabled = !this.showSourceControls || busy;
|
|
182
|
+
this.iconButton('previous-file', 'previous', this.labels.previous);
|
|
183
|
+
this.iconButton('next-file', 'next', this.labels.next);
|
|
184
|
+
this.$('previous-file').disabled = busy || this.queueIndex <= 0;
|
|
185
|
+
this.$('next-file').disabled = busy || this.queueIndex >= this.queueItems.length - 1;
|
|
186
|
+
const signature = JSON.stringify([this.queueRevision, labels.remove, labels.unnamed]);
|
|
187
|
+
if (signature !== this.queueSignature) {
|
|
188
|
+
this.queueSignature = signature;
|
|
189
|
+
this.$('queue-list').replaceChildren();
|
|
190
|
+
for (const item of this.queueItems) {
|
|
191
|
+
const row = document.createElement('li'), choose = document.createElement('button'), remove = document.createElement('button');
|
|
192
|
+
choose.type = remove.type = 'button';
|
|
193
|
+
choose.className = 'queue-item';
|
|
194
|
+
remove.className = 'queue-remove';
|
|
195
|
+
choose.textContent = item.name || this.labels.unnamed;
|
|
196
|
+
choose.title = choose.textContent;
|
|
197
|
+
choose.onclick = () => { if (this.showSourceControls)
|
|
198
|
+
this.selectQueue(this.queueItems.indexOf(item)); };
|
|
199
|
+
remove.textContent = '×';
|
|
200
|
+
remove.setAttribute('aria-label', `${this.labels.remove} ${item.name || this.labels.unnamed}`);
|
|
201
|
+
remove.onclick = () => this.removeQueueItem(this.queueItems.indexOf(item));
|
|
202
|
+
row.append(choose, remove);
|
|
203
|
+
this.$('queue-list').append(row);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
Array.from(this.$('queue-list').children).forEach((row, index) => {
|
|
207
|
+
const choose = row.querySelector('.queue-item');
|
|
208
|
+
if (index === this.queueIndex)
|
|
209
|
+
choose.setAttribute('aria-current', 'true');
|
|
210
|
+
else
|
|
211
|
+
choose.removeAttribute('aria-current');
|
|
212
|
+
row.querySelectorAll('button').forEach(button => button.disabled = busy || !this.showSourceControls);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
sourceName = '';
|
|
216
|
+
sourceNameId = null;
|
|
217
|
+
sourceControls = true;
|
|
218
|
+
diagnosticsControl = true;
|
|
219
|
+
fileDrop = true;
|
|
220
|
+
seekSeconds = 10;
|
|
221
|
+
autoHideDelay = 2800;
|
|
222
|
+
get titleMode() {
|
|
223
|
+
const mode = this.getAttribute('title-mode');
|
|
224
|
+
return mode === 'custom' || mode === 'source' || mode === 'none' ? mode : 'auto';
|
|
225
|
+
}
|
|
226
|
+
set titleMode(value) {
|
|
227
|
+
if (!['auto', 'custom', 'source', 'none'].includes(value))
|
|
228
|
+
throw new PlayerError('INVALID_ARGUMENT', 'Invalid titleMode');
|
|
229
|
+
this.setAttribute('title-mode', value);
|
|
230
|
+
}
|
|
231
|
+
get showSourceControls() { return this.sourceControls; }
|
|
232
|
+
set showSourceControls(value) { this.sourceControls = !!value; this.updateUtilities(); }
|
|
233
|
+
get showDiagnostics() { return this.diagnosticsControl; }
|
|
234
|
+
set showDiagnostics(value) { this.diagnosticsControl = !!value; this.updateUtilities(); }
|
|
235
|
+
get allowFileDrop() { return this.fileDrop; }
|
|
236
|
+
set allowFileDrop(value) { this.fileDrop = !!value; }
|
|
237
|
+
get seekStep() { return this.seekSeconds; }
|
|
238
|
+
set seekStep(value) {
|
|
239
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
240
|
+
throw new PlayerError('INVALID_ARGUMENT', 'seekStep must be a positive finite number');
|
|
241
|
+
this.seekSeconds = value;
|
|
242
|
+
this.labelControls();
|
|
243
|
+
if (this.core)
|
|
244
|
+
this.update(this.core.state);
|
|
245
|
+
}
|
|
246
|
+
get controlsAutoHideDelay() { return this.autoHideDelay; }
|
|
247
|
+
set controlsAutoHideDelay(value) {
|
|
248
|
+
if (!Number.isFinite(value) || value < 0 || value > 2147483647)
|
|
249
|
+
throw new PlayerError('INVALID_ARGUMENT', 'controlsAutoHideDelay must be between 0 and 2147483647 milliseconds');
|
|
250
|
+
this.autoHideDelay = value;
|
|
251
|
+
this.revealControls();
|
|
252
|
+
}
|
|
253
|
+
updateTitle() {
|
|
254
|
+
const text = this.titleMode === 'none' ? '' : this.titleMode === 'custom' ? this.title :
|
|
255
|
+
this.titleMode === 'source' ? this.sourceName : this.title || this.sourceName;
|
|
256
|
+
this.$('title').textContent = text;
|
|
257
|
+
this.$('title').hidden = !text;
|
|
258
|
+
this.updateSourceLabel();
|
|
259
|
+
}
|
|
260
|
+
updateSourceLabel() {
|
|
261
|
+
const text = this.sourceName || (!this.terminal && this.isConnected && this.core?.state.sourceId ? this.labels.loadedMedia : this.labels.noMedia);
|
|
262
|
+
if (this.$('current-source').textContent !== text)
|
|
263
|
+
this.$('current-source').textContent = text;
|
|
264
|
+
}
|
|
265
|
+
updateUtilities() {
|
|
266
|
+
if (this.terminal)
|
|
267
|
+
return;
|
|
268
|
+
const focused = this.shadowRoot?.activeElement;
|
|
269
|
+
const sourceFocused = !!focused && (this.$('source-options').contains(focused) || this.$('open-menu') === focused || this.$('open') === focused || (!this.$('settings').hidden && this.menuTrigger === 'open-menu' && this.$('settings').contains(focused)));
|
|
270
|
+
const diagnosticsFocused = focused === this.$('diagnostics-toggle') || focused === this.$('diagnostics-overlay');
|
|
271
|
+
if (!this.showSourceControls && this.menuTrigger === 'open-menu')
|
|
272
|
+
this.settings(false, false);
|
|
273
|
+
this.$('open-menu').hidden = !this.showSourceControls;
|
|
274
|
+
this.$('empty').hidden = !this.showSourceControls || !!this.core?.state.sourceId;
|
|
275
|
+
for (const id of ['open-menu', 'open', 'choose-file', 'file', 'subtitleFile', 'url', 'format', 'live', 'url-submit'])
|
|
276
|
+
this.$(id).disabled = !this.showSourceControls;
|
|
277
|
+
this.$('source-options').inert = !this.showSourceControls;
|
|
278
|
+
if (!this.showSourceControls)
|
|
279
|
+
this.$('source-options').hidden = true;
|
|
280
|
+
this.$('diagnostics-toggle').hidden = !this.showDiagnostics;
|
|
281
|
+
this.$('diagnostics-toggle').disabled = !this.showDiagnostics;
|
|
282
|
+
if (!this.showDiagnostics)
|
|
283
|
+
this.setDiagnostics(false);
|
|
284
|
+
this.renderQueue();
|
|
285
|
+
if ((!this.showSourceControls && sourceFocused) || (!this.showDiagnostics && diagnosticsFocused))
|
|
286
|
+
this.$('stage').focus({ preventScroll: true });
|
|
287
|
+
}
|
|
288
|
+
terminal = false;
|
|
289
|
+
cleanup = Promise.resolve();
|
|
290
|
+
connecting;
|
|
291
|
+
connection = 0;
|
|
292
|
+
unsubscribe;
|
|
293
|
+
sourceAbort;
|
|
294
|
+
sourceVersion = 0;
|
|
295
|
+
lastSource;
|
|
296
|
+
lastOptions;
|
|
297
|
+
resolveReady;
|
|
298
|
+
rejectReady;
|
|
299
|
+
readiness;
|
|
300
|
+
overrides = {};
|
|
301
|
+
menuTrigger = 'settings-toggle';
|
|
302
|
+
seekPreviewTimer;
|
|
303
|
+
hideTimer;
|
|
304
|
+
revealControls = () => { this.$('shell').classList.remove('idle', 'seek-preview'); clearTimeout(this.seekPreviewTimer); clearTimeout(this.hideTimer); if (this.core?.state.status === 'playing' && this.controlsAutoHideDelay > 0)
|
|
305
|
+
this.hideTimer = setTimeout(() => { if (this.core?.state.status === 'playing' && !this.core.state.pendingOperation && this.$('settings').hidden && !this.dragging && !this.shadowRoot?.activeElement?.matches(':focus-visible') && this.isConnected)
|
|
306
|
+
this.hideControls(); }, this.controlsAutoHideDelay); };
|
|
307
|
+
async playFromControls() { const core = this.core; await this.play(); if (core === this.core && core?.state.playbackIntent === 'play' && this.$('settings').hidden)
|
|
308
|
+
this.hideControls(true); }
|
|
309
|
+
hideControls(focusStage = false) { if (focusStage || this.shadowRoot?.activeElement)
|
|
310
|
+
this.$('stage').focus({ preventScroll: true }); clearTimeout(this.hideTimer); clearTimeout(this.seekPreviewTimer); this.$('shell').classList.remove('seek-preview'); this.$('shell').classList.add('idle'); }
|
|
311
|
+
dismissMenu = (event) => { const path = event.composedPath(); if (!this.$('settings').hidden && !['settings', 'settings-toggle', 'open-menu'].some(id => path.includes(this.$(id))))
|
|
312
|
+
this.settings(false, false); };
|
|
313
|
+
stageWasIdle = false;
|
|
314
|
+
isScreenPress(event) { return !event.composedPath().some(node => node instanceof Element && node.matches('button,input,select,textarea,a,summary,[contenteditable],[role="button"],#settings,#error,#diagnostics-overlay')); }
|
|
315
|
+
wasSeeking = false;
|
|
316
|
+
diagnosticsUpdated = 0;
|
|
317
|
+
dragging = false;
|
|
318
|
+
dimensions = '';
|
|
319
|
+
trackSignature = '';
|
|
320
|
+
reflected = false;
|
|
321
|
+
attributeScheduled = false;
|
|
322
|
+
configuredAsset = null;
|
|
323
|
+
resizeObserver;
|
|
324
|
+
lastAnnouncement = '';
|
|
325
|
+
lastFailure;
|
|
326
|
+
fullscreenChanged = () => { const active = document.fullscreenElement === this; this.$('fullscreen').setAttribute('aria-pressed', String(active)); this.iconButton('fullscreen', active ? 'collapse' : 'expand', active ? this.labels.exitFullscreen : this.labels.fullscreen); };
|
|
327
|
+
constructor() { super(); this.newReady(); this.attachShadow({ mode: 'open' }); this.renderShell(); }
|
|
328
|
+
newReady() { this.readiness = new Promise((resolve, reject) => { this.resolveReady = resolve; this.rejectReady = reject; }); void this.readiness.catch(() => { }); }
|
|
329
|
+
get ready() { return this.readiness; }
|
|
330
|
+
get player() { return this.core; }
|
|
331
|
+
get src() { return this.getAttribute('src') ?? ''; }
|
|
332
|
+
set src(value) { if (value)
|
|
333
|
+
this.setAttribute('src', String(value));
|
|
334
|
+
else
|
|
335
|
+
this.removeAttribute('src'); }
|
|
336
|
+
get controls() { return this.hasAttribute('controls'); }
|
|
337
|
+
set controls(value) { this.toggleAttribute('controls', !!value); }
|
|
338
|
+
get autoplay() { return this.hasAttribute('autoplay'); }
|
|
339
|
+
set autoplay(value) { this.toggleAttribute('autoplay', !!value); }
|
|
340
|
+
get muted() { return this.hasAttribute('muted'); }
|
|
341
|
+
set muted(value) { this.toggleAttribute('muted', !!value); }
|
|
342
|
+
get poster() { return this.getAttribute('poster') ?? ''; }
|
|
343
|
+
set poster(value) { if (value)
|
|
344
|
+
this.setAttribute('poster', value);
|
|
345
|
+
else
|
|
346
|
+
this.removeAttribute('poster'); }
|
|
347
|
+
get assetBase() { return this.getAttribute('asset-base') ?? undefined; }
|
|
348
|
+
set assetBase(value) { if (this.core)
|
|
349
|
+
throw new PlayerError('INVALID_ARGUMENT', 'assetBase is fixed after initialization'); if (value)
|
|
350
|
+
this.setAttribute('asset-base', value);
|
|
351
|
+
else
|
|
352
|
+
this.removeAttribute('asset-base'); }
|
|
353
|
+
get labels() { return { ...defaultLabels, back: `Seek backward ${this.seekStep} seconds`, forward: `Seek forward ${this.seekStep} seconds`, ...this.overrides }; }
|
|
354
|
+
set labels(value) { const next = {}; for (const [key, text] of Object.entries(value)) {
|
|
355
|
+
if (!(key in defaultLabels) || text === undefined)
|
|
356
|
+
continue;
|
|
357
|
+
if (typeof text !== 'string' || text.length > 1024)
|
|
358
|
+
throw new PlayerError('INVALID_ARGUMENT', 'Labels must be strings up to 1024 characters');
|
|
359
|
+
next[key] = text;
|
|
360
|
+
} this.overrides = next; this.labelControls(); if (this.core)
|
|
361
|
+
this.update(this.core.state); }
|
|
362
|
+
$(id) { return this.shadowRoot.getElementById(id); }
|
|
363
|
+
input(id) { return this.$(id); }
|
|
364
|
+
connectedCallback() {
|
|
365
|
+
const token = ++this.connection;
|
|
366
|
+
if (this.terminal)
|
|
367
|
+
return;
|
|
368
|
+
for (const name of ['assetBase', 'labels', 'controls', 'poster', 'autoplay', 'muted', 'title', 'titleMode', 'showSourceControls', 'showDiagnostics', 'allowFileDrop', 'seekStep', 'controlsAutoHideDelay', 'src'])
|
|
369
|
+
if (Object.prototype.hasOwnProperty.call(this, name)) {
|
|
370
|
+
const value = this[name];
|
|
371
|
+
delete this[name];
|
|
372
|
+
this[name] = value;
|
|
373
|
+
}
|
|
374
|
+
if (this.core)
|
|
375
|
+
return;
|
|
376
|
+
this.connecting = (async () => {
|
|
377
|
+
await this.cleanup;
|
|
378
|
+
if (!this.isConnected || token !== this.connection || this.terminal)
|
|
379
|
+
return;
|
|
380
|
+
try {
|
|
381
|
+
this.configuredAsset = this.getAttribute('asset-base');
|
|
382
|
+
const core = this.core = new Player(this.$('surface'), { assetBase: this.assetBase });
|
|
383
|
+
this.dimensions = '';
|
|
384
|
+
this.trackSignature = '';
|
|
385
|
+
for (const type of [...PLAYER_EVENTS, 'modechange', 'selectionchange', 'mpv', 'log', 'source', 'output'])
|
|
386
|
+
core.addEventListener(type, event => {
|
|
387
|
+
if (this.core !== core || this.terminal)
|
|
388
|
+
return;
|
|
389
|
+
const detail = event.detail;
|
|
390
|
+
if (type === 'error')
|
|
391
|
+
this.showError(detail);
|
|
392
|
+
if (type === 'ended')
|
|
393
|
+
queueMicrotask(() => { if (this.core === core)
|
|
394
|
+
this.advanceQueue(); });
|
|
395
|
+
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
396
|
+
});
|
|
397
|
+
const initiallyMuted = this.muted;
|
|
398
|
+
this.unsubscribe = core.subscribe(state => this.update(state));
|
|
399
|
+
if (initiallyMuted)
|
|
400
|
+
await core.setMuted(true);
|
|
401
|
+
this.resizeObserver = new ResizeObserver(() => { if (this.core)
|
|
402
|
+
this.geometry(this.core.state); });
|
|
403
|
+
this.resizeObserver.observe(this.$('stage'));
|
|
404
|
+
document.addEventListener('fullscreenchange', this.fullscreenChanged);
|
|
405
|
+
document.addEventListener('pointerdown', this.dismissMenu, true);
|
|
406
|
+
this.resolveReady(core);
|
|
407
|
+
if (this.src)
|
|
408
|
+
this.scheduleSource();
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
this.rejectReady(playerError(error));
|
|
412
|
+
this.componentError(error);
|
|
413
|
+
}
|
|
414
|
+
})();
|
|
415
|
+
}
|
|
416
|
+
disconnectedCallback() {
|
|
417
|
+
const token = ++this.connection;
|
|
418
|
+
queueMicrotask(() => {
|
|
419
|
+
if (this.isConnected || token !== this.connection || this.terminal)
|
|
420
|
+
return;
|
|
421
|
+
clearTimeout(this.hideTimer);
|
|
422
|
+
clearTimeout(this.seekPreviewTimer);
|
|
423
|
+
this.sourceVersion++;
|
|
424
|
+
this.sourceAbort?.abort();
|
|
425
|
+
this.resetQueue();
|
|
426
|
+
this.lastSource = undefined;
|
|
427
|
+
this.lastOptions = undefined;
|
|
428
|
+
this.sourceName = '';
|
|
429
|
+
this.sourceNameId = null;
|
|
430
|
+
this.updateTitle();
|
|
431
|
+
this.unsubscribe?.();
|
|
432
|
+
this.resizeObserver?.disconnect();
|
|
433
|
+
document.removeEventListener('fullscreenchange', this.fullscreenChanged);
|
|
434
|
+
document.removeEventListener('pointerdown', this.dismissMenu, true);
|
|
435
|
+
const old = this.core;
|
|
436
|
+
this.core = undefined;
|
|
437
|
+
this.rejectReady(new PlayerError('ABORTED', 'Player element disconnected'));
|
|
438
|
+
this.newReady();
|
|
439
|
+
this.cleanup = Promise.all([this.connecting, old?.destroy()]).then(() => { });
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
attributeChangedCallback(name, old, value) {
|
|
443
|
+
if (old === value || this.reflected || this.terminal)
|
|
444
|
+
return;
|
|
445
|
+
if (name === 'title' || name === 'title-mode')
|
|
446
|
+
this.updateTitle();
|
|
447
|
+
if (name === 'asset-base' && this.core) {
|
|
448
|
+
this.reflected = true;
|
|
449
|
+
if (this.configuredAsset === null)
|
|
450
|
+
this.removeAttribute(name);
|
|
451
|
+
else
|
|
452
|
+
this.setAttribute(name, this.configuredAsset);
|
|
453
|
+
this.reflected = false;
|
|
454
|
+
this.componentError(new PlayerError('INVALID_ARGUMENT', 'asset-base is fixed after initialization'));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (name === 'controls' && !this.controls) {
|
|
458
|
+
const focused = this.shadowRoot?.activeElement;
|
|
459
|
+
const moveFocus = !!focused && ['topbar', 'settings', 'diagnostics-overlay'].some(id => this.$(id).contains(focused));
|
|
460
|
+
this.settings(false, false);
|
|
461
|
+
this.setDiagnostics(false);
|
|
462
|
+
if (moveFocus)
|
|
463
|
+
this.$('stage').focus({ preventScroll: true });
|
|
464
|
+
}
|
|
465
|
+
if (name === 'src' && this.core)
|
|
466
|
+
this.scheduleSource();
|
|
467
|
+
if (name === 'muted' && this.core)
|
|
468
|
+
this.run(this.core.setMuted(value !== null));
|
|
469
|
+
if (name === 'poster') {
|
|
470
|
+
const img = this.$('poster');
|
|
471
|
+
if (value)
|
|
472
|
+
img.src = value;
|
|
473
|
+
else
|
|
474
|
+
img.removeAttribute('src');
|
|
475
|
+
}
|
|
476
|
+
if (this.core)
|
|
477
|
+
this.update(this.core.state);
|
|
478
|
+
else {
|
|
479
|
+
this.$('controls').hidden = !this.controls;
|
|
480
|
+
this.$('topbar').hidden = !this.controls;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
openFromControls(source) { this.settings(false, false); this.$('stage').focus({ preventScroll: true }); this.run(this.open(source)); }
|
|
484
|
+
scheduleSource() { if (this.attributeScheduled)
|
|
485
|
+
return; this.attributeScheduled = true; queueMicrotask(() => { this.attributeScheduled = false; if (!this.core || this.terminal)
|
|
486
|
+
return; this.run(this.src ? this.open(this.src) : this.close()); }); }
|
|
487
|
+
waitReady(signal) {
|
|
488
|
+
if (signal.aborted)
|
|
489
|
+
return Promise.reject(new PlayerError('ABORTED', 'Open aborted'));
|
|
490
|
+
return new Promise((resolve, reject) => { const abort = () => { signal.removeEventListener('abort', abort); reject(new PlayerError('ABORTED', 'Open aborted')); }; signal.addEventListener('abort', abort, { once: true }); this.ready.then(p => { signal.removeEventListener('abort', abort); resolve(p); }, e => { signal.removeEventListener('abort', abort); reject(e); }); });
|
|
491
|
+
}
|
|
492
|
+
async open(source, options = {}) {
|
|
493
|
+
if (this.terminal)
|
|
494
|
+
throw new PlayerError('ABORTED', 'Player element is destroyed');
|
|
495
|
+
this.resetQueue();
|
|
496
|
+
this.queueRevision++;
|
|
497
|
+
this.queueItems = [this.queueItem(source, options)];
|
|
498
|
+
return this.activateQueue(0, () => this.autoplay, options);
|
|
499
|
+
}
|
|
500
|
+
async openSource(source, options = {}) {
|
|
501
|
+
if (this.terminal)
|
|
502
|
+
throw new PlayerError('ABORTED', 'Player element is destroyed');
|
|
503
|
+
const version = ++this.sourceVersion;
|
|
504
|
+
this.sourceAbort?.abort();
|
|
505
|
+
const controller = this.sourceAbort = new AbortController();
|
|
506
|
+
const abort = () => controller.abort();
|
|
507
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
508
|
+
if (options.signal?.aborted)
|
|
509
|
+
abort();
|
|
510
|
+
try {
|
|
511
|
+
const core = this.core ?? await this.waitReady(controller.signal);
|
|
512
|
+
if (this.terminal || version !== this.sourceVersion || controller.signal.aborted)
|
|
513
|
+
throw new PlayerError('ABORTED', 'Open aborted');
|
|
514
|
+
this.lastSource = source;
|
|
515
|
+
this.lastOptions = { ...options, signal: undefined };
|
|
516
|
+
this.clearError();
|
|
517
|
+
await core.open(source, { ...options, signal: controller.signal });
|
|
518
|
+
if (version === this.sourceVersion && !controller.signal.aborted && this.core === core) {
|
|
519
|
+
this.sourceName = sourceTitle(source);
|
|
520
|
+
this.sourceNameId = core.state.sourceId;
|
|
521
|
+
this.updateTitle();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
options.signal?.removeEventListener('abort', abort);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
close() { this.sourceVersion++; this.sourceAbort?.abort(); this.resetQueue(); this.lastSource = undefined; this.lastOptions = undefined; this.clearError(); return this.core ? this.core.close() : this.terminal ? Promise.reject(new PlayerError('ABORTED', 'Player element is destroyed')) : Promise.resolve(); }
|
|
529
|
+
play() { if (this.queueOperation)
|
|
530
|
+
this.queuePlayIntent = true; return this.core ? this.core.play() : this.ready.then(p => p.play()); }
|
|
531
|
+
pause() { if (this.queueOperation)
|
|
532
|
+
this.queuePlayIntent = false; return this.core ? this.core.pause() : this.ready.then(p => p.pause()); }
|
|
533
|
+
seek(seconds) { return this.ready.then(p => p.seek(seconds)); }
|
|
534
|
+
setVolume(value) { return this.ready.then(p => p.setVolume(value)); }
|
|
535
|
+
setMuted(value) { return this.ready.then(p => p.setMuted(value)); }
|
|
536
|
+
setPlaybackRate(value) { return this.ready.then(p => p.setPlaybackRate(value)); }
|
|
537
|
+
selectAudioTrack(id) { return this.ready.then(p => p.selectAudioTrack(id)); }
|
|
538
|
+
selectSubtitleTrack(id) { return this.ready.then(p => p.selectSubtitleTrack(id)); }
|
|
539
|
+
addSubtitle(file, options) { return this.ready.then(p => p.addSubtitle(file, options)); }
|
|
540
|
+
destroy() {
|
|
541
|
+
if (this.terminal)
|
|
542
|
+
return this.cleanup;
|
|
543
|
+
clearTimeout(this.hideTimer);
|
|
544
|
+
clearTimeout(this.seekPreviewTimer);
|
|
545
|
+
this.terminal = true;
|
|
546
|
+
this.connection++;
|
|
547
|
+
this.sourceVersion++;
|
|
548
|
+
this.sourceAbort?.abort();
|
|
549
|
+
this.resetQueue();
|
|
550
|
+
this.lastSource = undefined;
|
|
551
|
+
this.lastOptions = undefined;
|
|
552
|
+
this.sourceName = '';
|
|
553
|
+
this.sourceNameId = null;
|
|
554
|
+
this.updateTitle();
|
|
555
|
+
this.unsubscribe?.();
|
|
556
|
+
this.resizeObserver?.disconnect();
|
|
557
|
+
document.removeEventListener('fullscreenchange', this.fullscreenChanged);
|
|
558
|
+
document.removeEventListener('pointerdown', this.dismissMenu, true);
|
|
559
|
+
this.rejectReady(new PlayerError('ABORTED', 'Player element is destroyed'));
|
|
560
|
+
const old = this.core;
|
|
561
|
+
this.core = undefined;
|
|
562
|
+
this.cleanup = Promise.all([this.cleanup, this.connecting, old?.destroy()]).then(() => { this.$('surface').replaceChildren(); this.$('controls').hidden = true; this.$('transport').hidden = true; this.$('topbar').hidden = true; this.$('settings').hidden = true; this.$('empty').hidden = true; this.$('diagnostics-overlay').hidden = true; this.$('buffering-indicator').hidden = true; });
|
|
563
|
+
return this.cleanup;
|
|
564
|
+
}
|
|
565
|
+
run(work) { void work.catch(error => { if (!this.terminal && playerError(error).code !== 'ABORTED')
|
|
566
|
+
this.showError(playerError(error).toJSON()); }); }
|
|
567
|
+
componentError(error) { const detail = playerError(error).toJSON(); this.showError(detail); this.dispatchEvent(new CustomEvent('error', { detail })); }
|
|
568
|
+
showError(error) { if (error.code === 'ABORTED')
|
|
569
|
+
return; this.lastFailure = error; this.$('error').hidden = false; this.$('error-text').textContent = error.message; this.$('retry').hidden = !error.retryable; this.$('retry').textContent = error.code === 'AUTOPLAY_BLOCKED' ? this.labels.play : this.labels.retry; this.announce(error.message, false); }
|
|
570
|
+
clearError() { this.lastFailure = undefined; this.$('error').hidden = true; }
|
|
571
|
+
announce(text, visual = true) { this.$('status').classList.toggle('sr', !visual); if (text === this.lastAnnouncement)
|
|
572
|
+
return; this.lastAnnouncement = text; this.$('status').textContent = text; }
|
|
573
|
+
geometry(state) { const ratio = state.mediaInfo.aspectRatio; if (!ratio) {
|
|
574
|
+
this.$('stage').style.removeProperty('--media-aspect');
|
|
575
|
+
return;
|
|
576
|
+
} this.$('stage').style.setProperty('--media-aspect', String(ratio)); if (state.pendingOperation)
|
|
577
|
+
return; const { width, height } = outputDimensions(ratio), key = `${width}x${height}`; if (this.dimensions !== key) {
|
|
578
|
+
this.dimensions = key;
|
|
579
|
+
this.core?.resize(width, height);
|
|
580
|
+
} }
|
|
581
|
+
update(state) {
|
|
582
|
+
// A host using the core directly owns its source list; release ours on replacement.
|
|
583
|
+
if (!this.queueOperation && this.queueSourceId !== null && this.queueSourceId !== state.sourceId)
|
|
584
|
+
this.resetQueue();
|
|
585
|
+
this.renderQueue();
|
|
586
|
+
this.updateSourceLabel();
|
|
587
|
+
if (this.sourceNameId !== state.sourceId) {
|
|
588
|
+
this.sourceName = '';
|
|
589
|
+
this.sourceNameId = null;
|
|
590
|
+
this.updateTitle();
|
|
591
|
+
}
|
|
592
|
+
const labels = this.labels, pending = state.pendingOperation !== null;
|
|
593
|
+
this.$('topbar').hidden = !this.controls;
|
|
594
|
+
const seeking = state.pendingOperation?.kind === 'seeking';
|
|
595
|
+
if (seeking !== this.wasSeeking) {
|
|
596
|
+
this.wasSeeking = seeking;
|
|
597
|
+
clearTimeout(this.seekPreviewTimer);
|
|
598
|
+
if (this.$('shell').classList.contains('idle')) {
|
|
599
|
+
this.$('shell').classList.add('seek-preview');
|
|
600
|
+
if (!seeking)
|
|
601
|
+
this.seekPreviewTimer = setTimeout(() => this.$('shell').classList.remove('seek-preview'), 800);
|
|
602
|
+
}
|
|
603
|
+
else
|
|
604
|
+
this.revealControls();
|
|
605
|
+
}
|
|
606
|
+
const playing = state.status === 'playing';
|
|
607
|
+
if (this.$('shell').classList.contains('playing') !== playing) {
|
|
608
|
+
this.$('shell').classList.toggle('playing', playing);
|
|
609
|
+
if (!this.$('shell').classList.contains('idle') || state.playbackIntent === 'pause' || ['ended', 'error', 'idle'].includes(state.status))
|
|
610
|
+
this.revealControls();
|
|
611
|
+
}
|
|
612
|
+
this.$('controls').hidden = !this.controls || !state.sourceId;
|
|
613
|
+
this.$('transport').hidden = !this.controls || !state.sourceId;
|
|
614
|
+
this.$('empty').hidden = !this.showSourceControls || !!state.sourceId;
|
|
615
|
+
this.$('poster').hidden = !this.poster || !!state.sourceId;
|
|
616
|
+
this.iconButton('play', state.playbackIntent === 'play' ? 'pause' : 'play', state.playbackIntent === 'play' ? labels.pause : labels.play);
|
|
617
|
+
this.$('play').disabled = !state.sourceId || pending;
|
|
618
|
+
this.iconButton('mute', state.muted ? 'muted' : 'volume', state.muted ? labels.unmute : labels.mute);
|
|
619
|
+
this.$('mute').setAttribute('aria-pressed', String(state.muted));
|
|
620
|
+
this.reflected = true;
|
|
621
|
+
this.toggleAttribute('muted', state.muted);
|
|
622
|
+
this.reflected = false;
|
|
623
|
+
if (this.shadowRoot.activeElement !== this.$('volume'))
|
|
624
|
+
this.input('volume').value = String(state.volume);
|
|
625
|
+
this.$('volume').style.setProperty('--volume-progress', `${Number(this.input('volume').value) * 100}%`);
|
|
626
|
+
const window = state.seekable;
|
|
627
|
+
this.input('timeline').disabled = pending || !window?.length;
|
|
628
|
+
for (const id of ['back', 'forward'])
|
|
629
|
+
this.$(id).disabled = pending || !window?.length;
|
|
630
|
+
if (window?.length) {
|
|
631
|
+
this.input('timeline').min = String(window[0].start);
|
|
632
|
+
this.input('timeline').max = String(window.at(-1).end);
|
|
633
|
+
}
|
|
634
|
+
if (!this.dragging) {
|
|
635
|
+
this.input('timeline').value = String(state.currentTime);
|
|
636
|
+
this.input('timeline').setAttribute('aria-valuetext', formatTime(state.currentTime));
|
|
637
|
+
this.$('time').textContent = formatTime(state.currentTime);
|
|
638
|
+
this.$('duration').textContent = state.streamType === 'live' ? labels.live : state.duration === null ? labels.unknown : formatTime(state.duration);
|
|
639
|
+
this.timelineProgress();
|
|
640
|
+
}
|
|
641
|
+
const signature = JSON.stringify([state.audioTracks, state.subtitleTracks]);
|
|
642
|
+
if (signature !== this.trackSignature) {
|
|
643
|
+
this.trackSignature = signature;
|
|
644
|
+
this.trackOptions('audio', state.audioTracks);
|
|
645
|
+
this.trackOptions('subtitles', state.subtitleTracks);
|
|
646
|
+
}
|
|
647
|
+
this.$('speed').value = String(state.playbackRate);
|
|
648
|
+
const buffering = state.status === 'buffering' && state.playbackIntent === 'play' && !pending;
|
|
649
|
+
this.$('buffering-indicator').hidden = !buffering;
|
|
650
|
+
this.$('shell').classList.toggle('buffering', buffering);
|
|
651
|
+
this.bufferedProgress(state);
|
|
652
|
+
const activity = state.pendingOperation?.kind === 'opening' ? labels.loading : state.pendingOperation?.kind === 'switching' ? labels.switching : state.pendingOperation?.kind === 'seeking' ? labels.seeking : state.status === 'buffering' ? labels.buffering : '';
|
|
653
|
+
this.$('busy').hidden = !activity || seeking || buffering;
|
|
654
|
+
this.$('busy').textContent = activity;
|
|
655
|
+
if (!this.lastFailure)
|
|
656
|
+
this.announce(activity || (state.streamType === 'live' && !window?.length ? labels.noWindow : ''), !activity);
|
|
657
|
+
this.geometry(state);
|
|
658
|
+
this.updateDiagnostics();
|
|
659
|
+
}
|
|
660
|
+
setDiagnostics(show) { show = show && this.showDiagnostics && this.controls; this.$('diagnostics-overlay').hidden = !show; this.$('diagnostics-toggle').setAttribute('aria-pressed', String(show)); this.iconButton('diagnostics-toggle', show ? 'eyeOff' : 'eye', this.labels.diagnostics); if (show)
|
|
661
|
+
this.updateDiagnostics(true); }
|
|
662
|
+
updateDiagnostics(force = false) {
|
|
663
|
+
if (this.$('diagnostics-overlay').hidden || !this.core)
|
|
664
|
+
return;
|
|
665
|
+
const now = performance.now();
|
|
666
|
+
if (!force && now - this.diagnosticsUpdated < 500)
|
|
667
|
+
return;
|
|
668
|
+
this.diagnosticsUpdated = now;
|
|
669
|
+
const s = this.core.state, d = this.core.diagnostics, m = s.mediaInfo;
|
|
670
|
+
const lines = [this.labels.diagnostics, `Engine ${s.activeMode ?? '—'} · ${s.automaticSelection ? 'automatic' : 'manual'}`, `State ${s.status}${s.pendingOperation ? ' · ' + s.pendingOperation.kind : ''}`, `Time ${formatTime(s.currentTime)} / ${s.streamType === 'live' ? this.labels.live : s.duration === null ? '—' : formatTime(s.duration)} · ${s.playbackRate}×`, `Video ${m.video?.codec ?? '—'} · ${m.displayWidth ?? '—'} × ${m.displayHeight ?? '—'}`, `Audio ${m.audio?.codec ?? '—'} · ${s.muted ? 'muted' : Math.round(s.volume * 100) + '%'}`];
|
|
671
|
+
for (const [key, value] of Object.entries(d.backend ?? {}))
|
|
672
|
+
if (['string', 'number', 'boolean'].includes(typeof value))
|
|
673
|
+
lines.push(`${key} ${String(value)}`);
|
|
674
|
+
this.$('diagnostics-overlay').textContent = lines.join('\n');
|
|
675
|
+
}
|
|
676
|
+
bufferedProgress(state) {
|
|
677
|
+
const ranges = state.seekable, min = ranges?.[0]?.start ?? 0, max = ranges?.at(-1)?.end ?? 0, span = max - min;
|
|
678
|
+
const layers = span > 0 ? (state.buffered ?? []).filter(r => Number.isFinite(r.start) && Number.isFinite(r.end) && r.end > r.start && r.end > min && r.start < max).map(r => { const start = Math.max(0, (r.start - min) / span * 100), end = Math.min(100, (r.end - min) / span * 100); return `linear-gradient(to right,transparent ${start}%,color-mix(in srgb,var(--demuxe-foreground) 21%,transparent) ${start}% ${end}%,transparent ${end}%)`; }) : [];
|
|
679
|
+
this.$('timeline').style.setProperty('--buffered', layers.join(',') || 'linear-gradient(transparent,transparent)');
|
|
680
|
+
}
|
|
681
|
+
timelineProgress() { const input = this.input('timeline'), min = Number(input.min), max = Number(input.max); input.style.setProperty('--progress', `${max > min ? Math.max(0, Math.min(100, (Number(input.value) - min) / (max - min) * 100)) : 0}%`); }
|
|
682
|
+
skip(delta) { const state = this.core?.state, ranges = state?.seekable; if (!state || state.pendingOperation || !ranges?.length)
|
|
683
|
+
return; const target = state.currentTime + delta; const range = ranges.find(r => target <= r.end) ?? ranges.at(-1); this.run(this.seek(Math.max(range.start, Math.min(range.end - .05, target)))); }
|
|
684
|
+
trackOptions(id, list) { const select = this.$(id); select.replaceChildren(new Option(this.labels.automatic, 'auto'), new Option(this.labels.off, '')); for (const t of list)
|
|
685
|
+
select.add(new Option(t.label, t.id)); select.value = list.find(t => t.selected)?.id ?? (list.length ? '' : 'auto'); select.disabled = !list.length; }
|
|
686
|
+
settings(open, restoreFocus = true, trigger = 'settings-toggle') { if (open && trigger === 'open-menu' && !this.showSourceControls)
|
|
687
|
+
return; if (open)
|
|
688
|
+
this.menuTrigger = trigger; this.revealControls(); this.$('settings').hidden = !open; this.$('shell').classList.toggle('menu-open', open); if (open) {
|
|
689
|
+
const source = this.menuTrigger === 'open-menu';
|
|
690
|
+
this.$('source-options').hidden = !source;
|
|
691
|
+
this.$('playback-options').hidden = source;
|
|
692
|
+
this.$('settings-title').textContent = source ? this.labels.open : this.labels.settings;
|
|
693
|
+
this.$('settings').classList.toggle('source-menu', source);
|
|
694
|
+
this.$('settings').scrollTop = 0;
|
|
695
|
+
} this.$('open-menu').setAttribute('aria-expanded', String(open && this.menuTrigger === 'open-menu')); this.iconButton('open-menu', open && this.menuTrigger === 'open-menu' ? 'folderOpen' : 'folder', this.labels.open); this.$('settings-toggle').setAttribute('aria-expanded', String(open && this.menuTrigger === 'settings-toggle')); if (open)
|
|
696
|
+
this.$('settings-close').focus();
|
|
697
|
+
else if (restoreFocus)
|
|
698
|
+
this.$(this.menuTrigger).focus(); }
|
|
699
|
+
fullscreen() { const active = document.fullscreenElement === this; const request = active ? document.exitFullscreen() : this.requestFullscreen?.(); if (!request) {
|
|
700
|
+
this.announce(this.labels.noFullscreen);
|
|
701
|
+
return;
|
|
702
|
+
} void request.then(() => { this.fullscreenChanged(); }, () => this.announce(this.labels.noFullscreen)); }
|
|
703
|
+
iconButton(id, icon, label) { const button = this.$(id); if (button.dataset.icon !== icon) {
|
|
704
|
+
button.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${icons[icon]}</svg>`;
|
|
705
|
+
button.dataset.icon = icon;
|
|
706
|
+
} if (icon === 'back' || icon === 'forward')
|
|
707
|
+
button.querySelector('text').textContent = String(this.seekStep); button.classList.add('icon-button'); button.setAttribute('aria-label', label); button.setAttribute('title', label); }
|
|
708
|
+
labelControls() { this.renderQueue(); this.$('choose-file').textContent = this.queueItems.length ? this.labels.addFiles : this.labels.open; this.updateSourceLabel(); this.$('diagnostics-overlay').setAttribute('aria-label', this.labels.diagnostics); for (const [id, key] of Object.entries({ mute: 'mute', 'settings-toggle': 'settings', 'settings-close': 'closeSettings', fullscreen: 'fullscreen', 'open': 'open', 'open-menu': 'open', 'url-submit': 'openURL', 'retry': 'retry' }))
|
|
709
|
+
this.$(id).textContent = this.labels[key]; for (const [id, icon, key] of [['back', 'back', 'back'], ['forward', 'forward', 'forward'], ['play', 'play', 'play'], ['mute', 'volume', 'mute'], ['settings-toggle', 'settings', 'settings'], ['settings-close', 'close', 'closeSettings'], ['open-menu', 'folder', 'open'], ['diagnostics-toggle', 'eye', 'diagnostics']]) {
|
|
710
|
+
delete this.$(id).dataset.icon;
|
|
711
|
+
this.iconButton(id, id === 'diagnostics-toggle' && this.$(id).getAttribute('aria-pressed') === 'true' ? 'eyeOff' : id === 'open-menu' && this.$(id).getAttribute('aria-expanded') === 'true' ? 'folderOpen' : icon, this.labels[key]);
|
|
712
|
+
} delete this.$('fullscreen').dataset.icon; this.fullscreenChanged(); for (const [id, key] of Object.entries({ timeline: 'seek', volume: 'volume', file: 'open', subtitleFile: 'addSubtitle' }))
|
|
713
|
+
this.$(id).setAttribute('aria-label', this.labels[key]); const opener = this.$('open'); opener.innerHTML = `<svg class="open-folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${icons.folder}</svg><span></span><svg class="open-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-5-5 5 5-5 5"/></svg>`; opener.querySelector('span').textContent = this.labels.open; for (const id of ['speed', 'audio', 'subtitles'])
|
|
714
|
+
this.$(id + '-label').textContent = this.labels[id]; this.$('settings-title').textContent = this.menuTrigger === 'open-menu' ? this.labels.open : this.labels.settings; this.$('media-file-label').textContent = this.labels.mediaFile; this.$('subtitle-file-label').textContent = this.labels.subtitleFile; for (const id of ['url', 'format', 'live'])
|
|
715
|
+
this.$(id + '-label').textContent = this.labels[id === 'live' ? 'streamLive' : id]; }
|
|
716
|
+
renderShell() {
|
|
717
|
+
this.shadowRoot.innerHTML = `<style>${styles}</style><section id="shell" class="shell" part="container" aria-label="Media player"><div id="topbar" class="topbar" part="topbar"><span id="title" class="player-title" part="title" hidden></span><span class="space"></span><button id="diagnostics-toggle" aria-pressed="false" aria-controls="diagnostics-overlay"></button><button id="open-menu" aria-expanded="false" aria-controls="settings"></button><button id="settings-toggle" aria-expanded="false" aria-controls="settings"></button><button id="fullscreen"></button></div><div id="stage" class="stage" part="stage" tabindex="0"><div id="surface" class="surface"></div><img id="poster" class="poster" alt="" hidden><div id="empty" class="empty"><button id="open"></button></div><div id="busy" class="busy" aria-hidden="true" hidden></div></div><div id="buffering-indicator" class="buffering-indicator" aria-hidden="true" hidden><span></span></div><div id="transport" part="transport" class="transport" hidden><button id="back" disabled></button><button id="play" class="play" disabled></button><button id="forward" disabled></button></div><div id="controls" class="controls" part="controls"><slot name="before-controls"></slot><input id="timeline" part="timeline" class="timeline" type="range" min="0" max="1" step="0.1" value="0" disabled><div class="times"><span id="time" class="time">0:00</span><div class="row" part="volume"><button id="mute" aria-pressed="false"></button><input id="volume" class="volume" type="range" min="0" max="1" step=".01" value="1"></div><div id="queue-navigation" class="queue-navigation" hidden><button id="previous-file" type="button"></button><span id="queue-count"></span><button id="next-file" type="button"></button></div><span class="space"></span><span id="duration" class="time">—</span></div><slot name="after-controls"></slot></div><section id="settings" class="settings" part="settings" aria-labelledby="settings-title" hidden><header><strong id="settings-title"></strong><button id="settings-close"></button></header><div id="playback-options"><label class="setting-row"><span id="speed-label"></span><select id="speed">${[.5, .75, 1, 1.25, 1.5, 1.75, 2].map(n => `<option value="${n}">${n}×</option>`).join('')}</select></label><label class="setting-row"><span id="audio-label"></span><select id="audio" disabled></select></label><label class="setting-row"><span id="subtitles-label"></span><select id="subtitles" disabled></select></label></div><div id="source-options" hidden><slot id="source-actions" name="source-actions"></slot><div class="media-picker" role="group" aria-labelledby="media-file-label"><span id="media-file-label"></span><span id="current-source"></span><button id="choose-file" type="button" aria-describedby="current-source"></button><input id="file" type="file" multiple hidden></div><section id="queue-section" class="queue-section" aria-labelledby="queue-heading" hidden><div class="queue-header"><strong id="queue-heading"></strong><button id="clear-queue" type="button"></button></div><ol id="queue-list"></ol></section><label class="subtitle-picker"><span id="subtitle-file-label"></span><input id="subtitleFile" type="file" accept=".srt,.ass,.ssa,.vtt"></label><form id="remote"><label><span id="url-label"></span><input id="url" type="url" placeholder="https://…" required></label><label><span id="format-label"></span><select id="format"><option value="file">File</option><option value="hls">HLS</option><option value="dash">DASH</option></select></label><label class="check"><input id="live" type="checkbox"><span id="live-label"></span></label><button id="url-submit" type="submit"></button></form></div></section><div id="error" class="notice" part="error" hidden><span id="error-text"></span><button id="retry"></button></div><pre id="diagnostics-overlay" class="diagnostics-overlay" tabindex="0" role="region" hidden></pre><div id="status" class="status" part="status" role="status" aria-live="polite" aria-atomic="true"></div></section>`;
|
|
718
|
+
this.labelControls();
|
|
719
|
+
this.updateTitle();
|
|
720
|
+
this.updateUtilities();
|
|
721
|
+
this.$('controls').hidden = !this.controls;
|
|
722
|
+
this.$('topbar').hidden = !this.controls;
|
|
723
|
+
this.addEventListener('pointermove', event => { if (event.pointerType !== 'touch')
|
|
724
|
+
this.revealControls(); });
|
|
725
|
+
this.addEventListener('pointerdown', event => { if (this.isScreenPress(event))
|
|
726
|
+
this.stageWasIdle = this.$('shell').classList.contains('idle');
|
|
727
|
+
else
|
|
728
|
+
this.revealControls(); });
|
|
729
|
+
this.addEventListener('focusin', this.revealControls);
|
|
730
|
+
this.addEventListener('focusout', () => { if (!this.$('shell').classList.contains('idle'))
|
|
731
|
+
this.revealControls(); });
|
|
732
|
+
this.addEventListener('pointerleave', event => { if (event.pointerType !== 'mouse' || this.terminal || !this.controls || this.core?.state.status !== 'playing' || !this.core.state.sourceId || this.core.state.pendingOperation || this.dragging || !this.$('settings').hidden || this.shadowRoot?.activeElement?.matches(':focus-visible'))
|
|
733
|
+
return; this.hideControls(); });
|
|
734
|
+
this.$('source-actions').addEventListener('click', event => { if (this.showSourceControls && event.composedPath().some(node => node instanceof HTMLButtonElement)) {
|
|
735
|
+
this.settings(false, false);
|
|
736
|
+
this.$('stage').focus({ preventScroll: true });
|
|
737
|
+
} });
|
|
738
|
+
this.$('open-menu').onclick = () => this.settings(this.$('settings').hidden || this.menuTrigger !== 'open-menu', true, 'open-menu');
|
|
739
|
+
this.$('shell').onclick = event => { if (!this.isScreenPress(event) || !this.core?.state.sourceId || !this.controls)
|
|
740
|
+
return; if (this.stageWasIdle) {
|
|
741
|
+
this.$('stage').focus({ preventScroll: true });
|
|
742
|
+
this.revealControls();
|
|
743
|
+
}
|
|
744
|
+
else
|
|
745
|
+
this.hideControls(true); };
|
|
746
|
+
this.$('remote').onsubmit = event => { event.preventDefault(); if (!this.showSourceControls)
|
|
747
|
+
return; const format = this.$('format').value; this.openFromControls({ url: this.input('url').value, format, ...(format !== 'file' ? { streaming: { live: this.input('live').checked } } : {}) }); };
|
|
748
|
+
this.addEventListener('dragover', event => { if (this.allowFileDrop && event.dataTransfer?.types.includes('Files'))
|
|
749
|
+
event.preventDefault(); });
|
|
750
|
+
this.addEventListener('drop', event => { if (!this.allowFileDrop || !event.dataTransfer?.files.length)
|
|
751
|
+
return; event.preventDefault(); this.addFiles(Array.from(event.dataTransfer.files)); });
|
|
752
|
+
this.$('back').onclick = () => this.skip(-this.seekStep);
|
|
753
|
+
this.$('forward').onclick = () => this.skip(this.seekStep);
|
|
754
|
+
this.$('play').onclick = () => { if (this.core)
|
|
755
|
+
this.run(this.core.state.playbackIntent === 'play' ? this.pause() : this.playFromControls()); };
|
|
756
|
+
this.$('mute').onclick = () => { if (this.core)
|
|
757
|
+
this.run(this.setMuted(!this.core.state.muted)); };
|
|
758
|
+
this.input('volume').oninput = () => this.$('volume').style.setProperty('--volume-progress', `${Number(this.input('volume').value) * 100}%`);
|
|
759
|
+
this.input('volume').onchange = () => this.run(this.setVolume(Number(this.input('volume').value)));
|
|
760
|
+
this.input('timeline').oninput = () => { this.dragging = true; const text = formatTime(Number(this.input('timeline').value)); this.$('time').textContent = text; this.timelineProgress(); this.input('timeline').setAttribute('aria-valuetext', text); };
|
|
761
|
+
this.input('timeline').onchange = () => { const value = Number(this.input('timeline').value); this.dragging = false; this.run(this.seek(value)); };
|
|
762
|
+
this.input('timeline').onpointercancel = () => { this.dragging = false; if (this.core)
|
|
763
|
+
this.update(this.core.state); };
|
|
764
|
+
this.$('settings-toggle').onclick = () => this.settings(this.$('settings').hidden || this.menuTrigger !== 'settings-toggle', true, 'settings-toggle');
|
|
765
|
+
this.$('settings-close').onclick = () => this.settings(false);
|
|
766
|
+
this.$('speed').onchange = () => this.run(this.setPlaybackRate(Number(this.$('speed').value)));
|
|
767
|
+
this.$('audio').onchange = () => this.run(this.selectAudioTrack(this.$('audio').value || null));
|
|
768
|
+
this.$('subtitles').onchange = () => this.run(this.selectSubtitleTrack(this.$('subtitles').value || null));
|
|
769
|
+
this.$('diagnostics-toggle').onclick = () => this.setDiagnostics(this.$('diagnostics-overlay').hidden);
|
|
770
|
+
this.$('fullscreen').onclick = () => this.fullscreen();
|
|
771
|
+
this.$('stage').ondblclick = () => this.fullscreen();
|
|
772
|
+
this.$('choose-file').onclick = () => { if (this.showSourceControls)
|
|
773
|
+
this.input('file').click(); };
|
|
774
|
+
this.$('open').onclick = () => { if (this.showSourceControls)
|
|
775
|
+
this.input('file').click(); };
|
|
776
|
+
this.input('file').onchange = () => { const files = Array.from(this.input('file').files ?? []); this.input('file').value = ''; if (this.showSourceControls)
|
|
777
|
+
this.addFiles(files); };
|
|
778
|
+
this.input('subtitleFile').onchange = () => { const file = this.input('subtitleFile').files?.[0]; this.input('subtitleFile').value = ''; if (file && this.showSourceControls)
|
|
779
|
+
this.run(this.addSubtitle(file)); };
|
|
780
|
+
this.$('previous-file').onclick = () => this.selectQueue(this.queueIndex - 1);
|
|
781
|
+
this.$('next-file').onclick = () => this.selectQueue(this.queueIndex + 1);
|
|
782
|
+
this.$('clear-queue').onclick = () => { if (this.showSourceControls && !this.queueOperation && !this.core?.state.pendingOperation) {
|
|
783
|
+
this.settings(false, false);
|
|
784
|
+
this.$('stage').focus({ preventScroll: true });
|
|
785
|
+
this.run(this.close());
|
|
786
|
+
} };
|
|
787
|
+
this.$('retry').onclick = () => { const error = this.lastFailure; this.clearError(); if (error?.code === 'AUTOPLAY_BLOCKED')
|
|
788
|
+
this.run(this.play());
|
|
789
|
+
else if (this.lastSource)
|
|
790
|
+
this.run(this.queueItems[this.queueIndex]?.source === this.lastSource ? this.activateQueue(this.queueIndex, () => this.autoplay) : this.open(this.lastSource, this.lastOptions)); };
|
|
791
|
+
this.addEventListener('keydown', event => {
|
|
792
|
+
if (event.composedPath().includes(this.$('diagnostics-overlay'))) {
|
|
793
|
+
if (event.key === 'Escape') {
|
|
794
|
+
event.preventDefault();
|
|
795
|
+
this.setDiagnostics(false);
|
|
796
|
+
this.$('diagnostics-toggle').focus();
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const topbar = event.composedPath().includes(this.$('topbar')), key = shortcut(event, topbar);
|
|
801
|
+
if (topbar && key === ' ')
|
|
802
|
+
event.preventDefault();
|
|
803
|
+
if (event.repeat && this.$('settings').hidden && [' ', 'k', 'm', 'f'].includes(key ?? '')) {
|
|
804
|
+
event.preventDefault();
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (!['arrowleft', 'arrowright', 'j', 'l', 'home', 'end', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].includes(key ?? '') || !this.$('shell').classList.contains('idle'))
|
|
808
|
+
this.revealControls();
|
|
809
|
+
if (event.key === 'Escape' && !this.$('settings').hidden) {
|
|
810
|
+
event.preventDefault();
|
|
811
|
+
this.settings(false);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (!this.$('settings').hidden)
|
|
815
|
+
return;
|
|
816
|
+
const p = this.core;
|
|
817
|
+
if (!key || !p)
|
|
818
|
+
return;
|
|
819
|
+
let action;
|
|
820
|
+
const state = p.state;
|
|
821
|
+
if (key === 'f') {
|
|
822
|
+
event.preventDefault();
|
|
823
|
+
this.fullscreen();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (key === '?') {
|
|
827
|
+
event.preventDefault();
|
|
828
|
+
this.settings(true);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (key === 'm')
|
|
832
|
+
action = p.setMuted(!state.muted);
|
|
833
|
+
if (key === 'arrowup' || key === 'arrowdown')
|
|
834
|
+
action = p.setVolume(Math.max(0, Math.min(1, state.volume + (key === 'arrowup' ? .05 : -.05))));
|
|
835
|
+
if (key === '[' || key === ']')
|
|
836
|
+
action = p.setPlaybackRate(Math.max(.5, Math.min(2, state.playbackRate + (key === ']' ? .25 : -.25))));
|
|
837
|
+
if (!state.pendingOperation && state.sourceId) {
|
|
838
|
+
if (key === 'c')
|
|
839
|
+
action = p.subtitleVisible(!state.subtitlesVisible);
|
|
840
|
+
const ranges = state.seekable;
|
|
841
|
+
if (ranges?.length) {
|
|
842
|
+
const start = ranges[0].start, end = Math.max(start, ranges.at(-1).end - .1);
|
|
843
|
+
if (key === 'home')
|
|
844
|
+
action = p.seek(start);
|
|
845
|
+
if (key === 'end')
|
|
846
|
+
action = p.seek(end);
|
|
847
|
+
if (/^[0-9]$/.test(key))
|
|
848
|
+
action = p.seek(start + (end - start) * Number(key) / 10);
|
|
849
|
+
}
|
|
850
|
+
if (key === ' ' || key === 'k')
|
|
851
|
+
action = state.playbackIntent === 'play' ? this.pause() : this.playFromControls();
|
|
852
|
+
const delta = key === 'arrowleft' ? -5 : key === 'arrowright' ? 5 : key === 'j' ? -this.seekStep : key === 'l' ? this.seekStep : 0;
|
|
853
|
+
const window = state.seekable;
|
|
854
|
+
if (delta && window?.length)
|
|
855
|
+
action = p.seek(Math.max(window[0].start, Math.min(window.at(-1).end - .05, state.currentTime + delta)));
|
|
856
|
+
}
|
|
857
|
+
if (action) {
|
|
858
|
+
event.preventDefault();
|
|
859
|
+
this.run(action);
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
export function definePlayerElement(name = 'demuxe-player') {
|
|
865
|
+
if (typeof customElements === 'undefined')
|
|
866
|
+
throw new PlayerError('INVALID_ARGUMENT', 'Custom element registration requires a browser');
|
|
867
|
+
const existing = customElements.get(name);
|
|
868
|
+
if (existing && existing !== DemuxePlayerElement)
|
|
869
|
+
throw new PlayerError('INVALID_ARGUMENT', `Custom element ${name} is already registered with another implementation`);
|
|
870
|
+
if (!existing)
|
|
871
|
+
customElements.define(name, DemuxePlayerElement);
|
|
872
|
+
return DemuxePlayerElement;
|
|
873
|
+
}
|