@f8team/reel-core 1.0.4 → 1.0.5
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/CHANGELOG.md +10 -0
- package/dist/index.cjs +197 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -13
- package/dist/index.d.ts +40 -13
- package/dist/index.js +196 -68
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.d.cts
CHANGED
|
@@ -132,12 +132,12 @@ interface ThumbnailsDescriptor {
|
|
|
132
132
|
/**
|
|
133
133
|
* The kind of source. `auto` lets the registry pick.
|
|
134
134
|
*
|
|
135
|
-
* - `hls`
|
|
136
|
-
* - `mp4`
|
|
137
|
-
* - `youtube`
|
|
138
|
-
* - `dash`
|
|
139
|
-
* - `native`
|
|
140
|
-
* - `auto`
|
|
135
|
+
* - `hls` - HLS m3u8 manifest, dispatched to `hls.js` (or native on Safari).
|
|
136
|
+
* - `mp4` - Progressive MP4/WebM, dispatched to the native engine.
|
|
137
|
+
* - `youtube` - YouTube IFrame API.
|
|
138
|
+
* - `dash` - MPEG-DASH (reserved for a future dash provider).
|
|
139
|
+
* - `native` - Force the native engine even if the URL looks like HLS (G4 Safari escape).
|
|
140
|
+
* - `auto` - Detect from the URL via `looksLikeHls` / `detectSourceType`.
|
|
141
141
|
*/
|
|
142
142
|
type SourceType = "auto" | "hls" | "mp4" | "youtube" | "dash" | "native";
|
|
143
143
|
/**
|
|
@@ -154,8 +154,8 @@ interface SourceDescriptor {
|
|
|
154
154
|
/**
|
|
155
155
|
* Cookie / credentials policy for HLS XHRs.
|
|
156
156
|
*
|
|
157
|
-
* - `boolean`
|
|
158
|
-
* - `(url) => boolean`
|
|
157
|
+
* - `boolean` - sent for every request.
|
|
158
|
+
* - `(url) => boolean` - predicate scoped to a trusted API origin/path (see G7, G13).
|
|
159
159
|
*/
|
|
160
160
|
withCredentials?: boolean | ((url: string) => boolean);
|
|
161
161
|
/** Subtitle tracks to attach (G1, G15). */
|
|
@@ -222,9 +222,9 @@ interface SourceProvider {
|
|
|
222
222
|
/**
|
|
223
223
|
* Decide whether this provider can handle the given source.
|
|
224
224
|
*
|
|
225
|
-
* - `true`
|
|
226
|
-
* - `"maybe"`
|
|
227
|
-
* - `false`
|
|
225
|
+
* - `true` - definite match.
|
|
226
|
+
* - `"maybe"` - handle if no `true` match exists (used by the native fallback).
|
|
227
|
+
* - `false` - skip.
|
|
228
228
|
*/
|
|
229
229
|
canHandle(source: SourceDescriptor): boolean | "maybe";
|
|
230
230
|
/** Construct a fresh loader. Called once per source attach. */
|
|
@@ -469,6 +469,13 @@ interface Player {
|
|
|
469
469
|
dispose(): void;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
/** Synchronous source normalization hook applied before provider resolution. */
|
|
473
|
+
type SourceTransform = (source: SourceDescriptor) => SourceDescriptor;
|
|
474
|
+
/** Media element and container owned by one player attachment. */
|
|
475
|
+
interface PluginMediaAttachment {
|
|
476
|
+
video: HTMLVideoElement;
|
|
477
|
+
container: Element;
|
|
478
|
+
}
|
|
472
479
|
/**
|
|
473
480
|
* Command registry shared between the player and plugins. Commands are the
|
|
474
481
|
* extension point for keyboard handlers, plugin contributions, and UI buttons.
|
|
@@ -505,6 +512,22 @@ interface PluginHost {
|
|
|
505
512
|
};
|
|
506
513
|
/** Shared command registry. Plugins MUST namespace their commands. */
|
|
507
514
|
commands: CommandRegistry;
|
|
515
|
+
/**
|
|
516
|
+
* Register a synchronous source transform. Core applies transforms before
|
|
517
|
+
* the source is stored or passed to a provider, including the initial
|
|
518
|
+
* source supplied in `PlayerOptions`.
|
|
519
|
+
*
|
|
520
|
+
* Optional for compatibility with custom test hosts built against earlier
|
|
521
|
+
* versions of the contract.
|
|
522
|
+
*/
|
|
523
|
+
sources?: {
|
|
524
|
+
transform(transform: SourceTransform): () => void;
|
|
525
|
+
};
|
|
526
|
+
/** Player-scoped media ownership and attach/detach lifecycle. */
|
|
527
|
+
media?: {
|
|
528
|
+
get(): PluginMediaAttachment | null;
|
|
529
|
+
subscribe(listener: (attachment: PluginMediaAttachment | null) => void): () => void;
|
|
530
|
+
};
|
|
508
531
|
/** Read-only state. */
|
|
509
532
|
store: ReadableStore<PlayerState>;
|
|
510
533
|
/** Emit a custom plugin event. Use the `${plugin}:${verb}` namespace. */
|
|
@@ -810,9 +833,13 @@ declare function toSeconds(input: number | string): number;
|
|
|
810
833
|
|
|
811
834
|
/**
|
|
812
835
|
* URL helpers used by the source registry to dispatch a source descriptor to
|
|
813
|
-
* the right provider. Pure string predicates
|
|
836
|
+
* the right provider. Pure string predicates - no DOM, no I/O.
|
|
814
837
|
*/
|
|
815
838
|
|
|
839
|
+
/** Compare the descriptor fields that affect provider loading or media UI. */
|
|
840
|
+
declare function areSourceDescriptorsEquivalent(previous: SourceDescriptor | null, next: SourceDescriptor | null): boolean;
|
|
841
|
+
/** Whether a media/resource URL is safe to hand to browser media APIs. */
|
|
842
|
+
declare function isSafeMediaUrl(input: string | undefined | null): boolean;
|
|
816
843
|
/**
|
|
817
844
|
* Heuristic for HLS m3u8 URLs.
|
|
818
845
|
*
|
|
@@ -843,4 +870,4 @@ declare function looksLikeProgressive(input: string | undefined | null): boolean
|
|
|
843
870
|
*/
|
|
844
871
|
declare function detectSourceType(source: SourceDescriptor): SourceType;
|
|
845
872
|
|
|
846
|
-
export { type AutoplayMode, type BufferedRange, type CommandRegistry, DEFAULT_TOKENS, type Disposer, type KeyboardScope, type MutableStore, type Player, type PlayerError, type PlayerErrorCode, type PlayerEvents, type PlayerHooks, type PlayerOptions, type PlayerState, type PlayerStatus, type PluginHost, type PluginInstance, type QualityLevel, type ReadableStore, type SourceDescriptor, type SourceLoader, type SourceProvider, type SourceType, type SubtitleTrack, THEME_PRESETS, TOKEN_TO_CSS_VAR, type ThemeName, type ThemeTokens, type ThumbnailsDescriptor, type UnauthorizedEvent, announce, applyTheme, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
|
873
|
+
export { type AutoplayMode, type BufferedRange, type CommandRegistry, DEFAULT_TOKENS, type Disposer, type KeyboardScope, type MutableStore, type Player, type PlayerError, type PlayerErrorCode, type PlayerEvents, type PlayerHooks, type PlayerOptions, type PlayerState, type PlayerStatus, type PluginHost, type PluginInstance, type PluginMediaAttachment, type QualityLevel, type ReadableStore, type SourceDescriptor, type SourceLoader, type SourceProvider, type SourceTransform, type SourceType, type SubtitleTrack, THEME_PRESETS, TOKEN_TO_CSS_VAR, type ThemeName, type ThemeTokens, type ThumbnailsDescriptor, type UnauthorizedEvent, announce, applyTheme, areSourceDescriptorsEquivalent, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, isSafeMediaUrl, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
package/dist/index.d.ts
CHANGED
|
@@ -132,12 +132,12 @@ interface ThumbnailsDescriptor {
|
|
|
132
132
|
/**
|
|
133
133
|
* The kind of source. `auto` lets the registry pick.
|
|
134
134
|
*
|
|
135
|
-
* - `hls`
|
|
136
|
-
* - `mp4`
|
|
137
|
-
* - `youtube`
|
|
138
|
-
* - `dash`
|
|
139
|
-
* - `native`
|
|
140
|
-
* - `auto`
|
|
135
|
+
* - `hls` - HLS m3u8 manifest, dispatched to `hls.js` (or native on Safari).
|
|
136
|
+
* - `mp4` - Progressive MP4/WebM, dispatched to the native engine.
|
|
137
|
+
* - `youtube` - YouTube IFrame API.
|
|
138
|
+
* - `dash` - MPEG-DASH (reserved for a future dash provider).
|
|
139
|
+
* - `native` - Force the native engine even if the URL looks like HLS (G4 Safari escape).
|
|
140
|
+
* - `auto` - Detect from the URL via `looksLikeHls` / `detectSourceType`.
|
|
141
141
|
*/
|
|
142
142
|
type SourceType = "auto" | "hls" | "mp4" | "youtube" | "dash" | "native";
|
|
143
143
|
/**
|
|
@@ -154,8 +154,8 @@ interface SourceDescriptor {
|
|
|
154
154
|
/**
|
|
155
155
|
* Cookie / credentials policy for HLS XHRs.
|
|
156
156
|
*
|
|
157
|
-
* - `boolean`
|
|
158
|
-
* - `(url) => boolean`
|
|
157
|
+
* - `boolean` - sent for every request.
|
|
158
|
+
* - `(url) => boolean` - predicate scoped to a trusted API origin/path (see G7, G13).
|
|
159
159
|
*/
|
|
160
160
|
withCredentials?: boolean | ((url: string) => boolean);
|
|
161
161
|
/** Subtitle tracks to attach (G1, G15). */
|
|
@@ -222,9 +222,9 @@ interface SourceProvider {
|
|
|
222
222
|
/**
|
|
223
223
|
* Decide whether this provider can handle the given source.
|
|
224
224
|
*
|
|
225
|
-
* - `true`
|
|
226
|
-
* - `"maybe"`
|
|
227
|
-
* - `false`
|
|
225
|
+
* - `true` - definite match.
|
|
226
|
+
* - `"maybe"` - handle if no `true` match exists (used by the native fallback).
|
|
227
|
+
* - `false` - skip.
|
|
228
228
|
*/
|
|
229
229
|
canHandle(source: SourceDescriptor): boolean | "maybe";
|
|
230
230
|
/** Construct a fresh loader. Called once per source attach. */
|
|
@@ -469,6 +469,13 @@ interface Player {
|
|
|
469
469
|
dispose(): void;
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
/** Synchronous source normalization hook applied before provider resolution. */
|
|
473
|
+
type SourceTransform = (source: SourceDescriptor) => SourceDescriptor;
|
|
474
|
+
/** Media element and container owned by one player attachment. */
|
|
475
|
+
interface PluginMediaAttachment {
|
|
476
|
+
video: HTMLVideoElement;
|
|
477
|
+
container: Element;
|
|
478
|
+
}
|
|
472
479
|
/**
|
|
473
480
|
* Command registry shared between the player and plugins. Commands are the
|
|
474
481
|
* extension point for keyboard handlers, plugin contributions, and UI buttons.
|
|
@@ -505,6 +512,22 @@ interface PluginHost {
|
|
|
505
512
|
};
|
|
506
513
|
/** Shared command registry. Plugins MUST namespace their commands. */
|
|
507
514
|
commands: CommandRegistry;
|
|
515
|
+
/**
|
|
516
|
+
* Register a synchronous source transform. Core applies transforms before
|
|
517
|
+
* the source is stored or passed to a provider, including the initial
|
|
518
|
+
* source supplied in `PlayerOptions`.
|
|
519
|
+
*
|
|
520
|
+
* Optional for compatibility with custom test hosts built against earlier
|
|
521
|
+
* versions of the contract.
|
|
522
|
+
*/
|
|
523
|
+
sources?: {
|
|
524
|
+
transform(transform: SourceTransform): () => void;
|
|
525
|
+
};
|
|
526
|
+
/** Player-scoped media ownership and attach/detach lifecycle. */
|
|
527
|
+
media?: {
|
|
528
|
+
get(): PluginMediaAttachment | null;
|
|
529
|
+
subscribe(listener: (attachment: PluginMediaAttachment | null) => void): () => void;
|
|
530
|
+
};
|
|
508
531
|
/** Read-only state. */
|
|
509
532
|
store: ReadableStore<PlayerState>;
|
|
510
533
|
/** Emit a custom plugin event. Use the `${plugin}:${verb}` namespace. */
|
|
@@ -810,9 +833,13 @@ declare function toSeconds(input: number | string): number;
|
|
|
810
833
|
|
|
811
834
|
/**
|
|
812
835
|
* URL helpers used by the source registry to dispatch a source descriptor to
|
|
813
|
-
* the right provider. Pure string predicates
|
|
836
|
+
* the right provider. Pure string predicates - no DOM, no I/O.
|
|
814
837
|
*/
|
|
815
838
|
|
|
839
|
+
/** Compare the descriptor fields that affect provider loading or media UI. */
|
|
840
|
+
declare function areSourceDescriptorsEquivalent(previous: SourceDescriptor | null, next: SourceDescriptor | null): boolean;
|
|
841
|
+
/** Whether a media/resource URL is safe to hand to browser media APIs. */
|
|
842
|
+
declare function isSafeMediaUrl(input: string | undefined | null): boolean;
|
|
816
843
|
/**
|
|
817
844
|
* Heuristic for HLS m3u8 URLs.
|
|
818
845
|
*
|
|
@@ -843,4 +870,4 @@ declare function looksLikeProgressive(input: string | undefined | null): boolean
|
|
|
843
870
|
*/
|
|
844
871
|
declare function detectSourceType(source: SourceDescriptor): SourceType;
|
|
845
872
|
|
|
846
|
-
export { type AutoplayMode, type BufferedRange, type CommandRegistry, DEFAULT_TOKENS, type Disposer, type KeyboardScope, type MutableStore, type Player, type PlayerError, type PlayerErrorCode, type PlayerEvents, type PlayerHooks, type PlayerOptions, type PlayerState, type PlayerStatus, type PluginHost, type PluginInstance, type QualityLevel, type ReadableStore, type SourceDescriptor, type SourceLoader, type SourceProvider, type SourceType, type SubtitleTrack, THEME_PRESETS, TOKEN_TO_CSS_VAR, type ThemeName, type ThemeTokens, type ThumbnailsDescriptor, type UnauthorizedEvent, announce, applyTheme, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
|
873
|
+
export { type AutoplayMode, type BufferedRange, type CommandRegistry, DEFAULT_TOKENS, type Disposer, type KeyboardScope, type MutableStore, type Player, type PlayerError, type PlayerErrorCode, type PlayerEvents, type PlayerHooks, type PlayerOptions, type PlayerState, type PlayerStatus, type PluginHost, type PluginInstance, type PluginMediaAttachment, type QualityLevel, type ReadableStore, type SourceDescriptor, type SourceLoader, type SourceProvider, type SourceTransform, type SourceType, type SubtitleTrack, THEME_PRESETS, TOKEN_TO_CSS_VAR, type ThemeName, type ThemeTokens, type ThumbnailsDescriptor, type UnauthorizedEvent, announce, applyTheme, areSourceDescriptorsEquivalent, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, isSafeMediaUrl, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
package/dist/index.js
CHANGED
|
@@ -212,12 +212,58 @@ function createControlsRegistry() {
|
|
|
212
212
|
}
|
|
213
213
|
return { contribute, snapshot, clear };
|
|
214
214
|
}
|
|
215
|
-
function
|
|
215
|
+
function createSourceTransformRegistry() {
|
|
216
|
+
const transforms = /* @__PURE__ */ new Set();
|
|
217
|
+
function register(transform) {
|
|
218
|
+
transforms.add(transform);
|
|
219
|
+
return () => transforms.delete(transform);
|
|
220
|
+
}
|
|
221
|
+
function apply(source) {
|
|
222
|
+
let current = source;
|
|
223
|
+
for (const transform of transforms) {
|
|
224
|
+
current = transform(current);
|
|
225
|
+
}
|
|
226
|
+
return current;
|
|
227
|
+
}
|
|
228
|
+
return { register, apply };
|
|
229
|
+
}
|
|
230
|
+
function createMediaAttachmentRegistry() {
|
|
231
|
+
let current = null;
|
|
232
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
233
|
+
function get() {
|
|
234
|
+
return current;
|
|
235
|
+
}
|
|
236
|
+
function set(attachment) {
|
|
237
|
+
current = attachment;
|
|
238
|
+
for (const listener of listeners) listener(current);
|
|
239
|
+
}
|
|
240
|
+
function subscribe(listener) {
|
|
241
|
+
listeners.add(listener);
|
|
242
|
+
if (current) listener(current);
|
|
243
|
+
return () => listeners.delete(listener);
|
|
244
|
+
}
|
|
245
|
+
return { get, set, subscribe };
|
|
246
|
+
}
|
|
247
|
+
function createPluginHost({
|
|
248
|
+
commands,
|
|
249
|
+
controls,
|
|
250
|
+
store,
|
|
251
|
+
bus,
|
|
252
|
+
sourceTransforms = createSourceTransformRegistry(),
|
|
253
|
+
mediaAttachments = createMediaAttachmentRegistry()
|
|
254
|
+
}) {
|
|
216
255
|
return {
|
|
217
256
|
controls: {
|
|
218
|
-
contribute:
|
|
257
|
+
contribute: controls.contribute
|
|
219
258
|
},
|
|
220
259
|
commands,
|
|
260
|
+
sources: {
|
|
261
|
+
transform: sourceTransforms.register
|
|
262
|
+
},
|
|
263
|
+
media: {
|
|
264
|
+
get: mediaAttachments.get,
|
|
265
|
+
subscribe: mediaAttachments.subscribe
|
|
266
|
+
},
|
|
221
267
|
store,
|
|
222
268
|
emit: (event, payload) => {
|
|
223
269
|
bus.emit(event, payload);
|
|
@@ -226,6 +272,21 @@ function createPluginHost({ commands, controls, store, bus }) {
|
|
|
226
272
|
}
|
|
227
273
|
|
|
228
274
|
// src/util/url.ts
|
|
275
|
+
function areSourceDescriptorsEquivalent(previous, next) {
|
|
276
|
+
if (previous === next) return true;
|
|
277
|
+
if (!previous || !next) return false;
|
|
278
|
+
const previousThumbnails = previous.thumbnails;
|
|
279
|
+
const nextThumbnails = next.thumbnails;
|
|
280
|
+
return previous.src === next.src && (previous.type || "auto") === (next.type || "auto") && previous.withCredentials === next.withCredentials && previous.tracks === next.tracks && (previousThumbnails === nextThumbnails || previousThumbnails?.src === nextThumbnails?.src && previousThumbnails?.withCredentials === nextThumbnails?.withCredentials);
|
|
281
|
+
}
|
|
282
|
+
function isSafeMediaUrl(input) {
|
|
283
|
+
if (!input?.trim()) return false;
|
|
284
|
+
try {
|
|
285
|
+
return /^(https?|blob|data):$/.test(new URL(input, "https://reel.invalid").protocol);
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
229
290
|
function looksLikeHls(input) {
|
|
230
291
|
if (!input) return false;
|
|
231
292
|
const lower = input.toLowerCase();
|
|
@@ -854,6 +915,7 @@ var NativeLoader = class {
|
|
|
854
915
|
constructor() {
|
|
855
916
|
this.video = null;
|
|
856
917
|
this.cleanups = [];
|
|
918
|
+
this.rejectPending = null;
|
|
857
919
|
}
|
|
858
920
|
async attach(video, source) {
|
|
859
921
|
this.detach();
|
|
@@ -861,12 +923,14 @@ var NativeLoader = class {
|
|
|
861
923
|
return new Promise((resolve, reject) => {
|
|
862
924
|
const onMeta = () => {
|
|
863
925
|
cleanupListeners();
|
|
926
|
+
this.rejectPending = null;
|
|
864
927
|
resolve();
|
|
865
928
|
};
|
|
866
929
|
const onError = () => {
|
|
867
930
|
cleanupListeners();
|
|
868
931
|
const err = video.error;
|
|
869
932
|
const message = err ? `MediaError code=${err.code} message=${err.message}` : "Native loader failed";
|
|
933
|
+
this.rejectPending = null;
|
|
870
934
|
reject(new Error(message));
|
|
871
935
|
};
|
|
872
936
|
const cleanupListeners = () => {
|
|
@@ -876,17 +940,28 @@ var NativeLoader = class {
|
|
|
876
940
|
video.addEventListener("loadedmetadata", onMeta);
|
|
877
941
|
video.addEventListener("error", onError);
|
|
878
942
|
this.cleanups.push(cleanupListeners);
|
|
943
|
+
this.rejectPending = reject;
|
|
879
944
|
video.src = source.src;
|
|
880
945
|
video.load();
|
|
881
946
|
});
|
|
882
947
|
}
|
|
883
|
-
|
|
948
|
+
abort() {
|
|
949
|
+
const reject = this.rejectPending;
|
|
950
|
+
this.rejectPending = null;
|
|
884
951
|
while (this.cleanups.length) {
|
|
885
952
|
try {
|
|
886
953
|
this.cleanups.pop()?.();
|
|
887
954
|
} catch {
|
|
888
955
|
}
|
|
889
956
|
}
|
|
957
|
+
if (reject) {
|
|
958
|
+
const error = new Error("Native source load aborted");
|
|
959
|
+
error.name = "AbortError";
|
|
960
|
+
reject(error);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
detach() {
|
|
964
|
+
this.abort();
|
|
890
965
|
if (this.video) {
|
|
891
966
|
try {
|
|
892
967
|
this.video.removeAttribute("src");
|
|
@@ -1020,7 +1095,9 @@ var YouTubeLoader = class {
|
|
|
1020
1095
|
}
|
|
1021
1096
|
const host = document.createElement("div");
|
|
1022
1097
|
host.setAttribute("data-reel-yt-host", "");
|
|
1023
|
-
host.style.cssText = "position:absolute;inset:0;
|
|
1098
|
+
host.style.cssText = "position:absolute;inset:0;overflow:hidden;background:#000";
|
|
1099
|
+
const mount = document.createElement("div");
|
|
1100
|
+
host.appendChild(mount);
|
|
1024
1101
|
parent.appendChild(host);
|
|
1025
1102
|
this.host = host;
|
|
1026
1103
|
video.setAttribute(YT_HIDDEN_ATTR, "");
|
|
@@ -1033,7 +1110,7 @@ var YouTubeLoader = class {
|
|
|
1033
1110
|
run();
|
|
1034
1111
|
};
|
|
1035
1112
|
try {
|
|
1036
|
-
this.yt = new YT.Player(
|
|
1113
|
+
this.yt = new YT.Player(mount, {
|
|
1037
1114
|
videoId: id,
|
|
1038
1115
|
// 100% so the iframe fills the host div rather than using default px dimensions.
|
|
1039
1116
|
width: "100%",
|
|
@@ -1049,7 +1126,7 @@ var YouTubeLoader = class {
|
|
|
1049
1126
|
onReady: () => {
|
|
1050
1127
|
const iframe = host.querySelector("iframe");
|
|
1051
1128
|
if (iframe) {
|
|
1052
|
-
iframe.style.cssText = "width:100%;height:100%;border:none;display:block
|
|
1129
|
+
iframe.style.cssText = "width:100%;height:100%;border:none;display:block";
|
|
1053
1130
|
}
|
|
1054
1131
|
try {
|
|
1055
1132
|
this.duration = this.yt?.getDuration() ?? 0;
|
|
@@ -1381,21 +1458,6 @@ function createStore(initial, options = {}) {
|
|
|
1381
1458
|
}
|
|
1382
1459
|
|
|
1383
1460
|
// src/createPlayer.ts
|
|
1384
|
-
var NATIVE_EVENTS = [
|
|
1385
|
-
"loadedmetadata",
|
|
1386
|
-
"durationchange",
|
|
1387
|
-
"timeupdate",
|
|
1388
|
-
"play",
|
|
1389
|
-
"pause",
|
|
1390
|
-
"ended",
|
|
1391
|
-
"error",
|
|
1392
|
-
"volumechange",
|
|
1393
|
-
"ratechange",
|
|
1394
|
-
"seeking",
|
|
1395
|
-
"seeked",
|
|
1396
|
-
"waiting",
|
|
1397
|
-
"playing"
|
|
1398
|
-
];
|
|
1399
1461
|
function makeInitialState(options) {
|
|
1400
1462
|
return {
|
|
1401
1463
|
status: "idle",
|
|
@@ -1428,13 +1490,42 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1428
1490
|
const bus = createEventBus();
|
|
1429
1491
|
const commands = createCommandRegistry();
|
|
1430
1492
|
const controls = createControlsRegistry();
|
|
1493
|
+
const sourceTransforms = createSourceTransformRegistry();
|
|
1494
|
+
const mediaAttachments = createMediaAttachmentRegistry();
|
|
1431
1495
|
let status = "idle";
|
|
1432
1496
|
let video = null;
|
|
1433
1497
|
let activeLoader = null;
|
|
1434
1498
|
let pendingAttachAbort = null;
|
|
1499
|
+
let pendingSourceLoad = null;
|
|
1500
|
+
let latestSourceLoadPromise = null;
|
|
1435
1501
|
let disposed = false;
|
|
1436
|
-
let initialOptionsApplied = false;
|
|
1437
1502
|
let pendingPlay = false;
|
|
1503
|
+
function createPendingSourceLoad() {
|
|
1504
|
+
let resolve;
|
|
1505
|
+
let reject;
|
|
1506
|
+
const promise = new Promise((onResolve, onReject) => {
|
|
1507
|
+
resolve = onResolve;
|
|
1508
|
+
reject = onReject;
|
|
1509
|
+
});
|
|
1510
|
+
void promise.catch(() => void 0);
|
|
1511
|
+
latestSourceLoadPromise = promise;
|
|
1512
|
+
return { promise, resolve, reject };
|
|
1513
|
+
}
|
|
1514
|
+
function resolvePendingSourceLoad() {
|
|
1515
|
+
const load = pendingSourceLoad;
|
|
1516
|
+
if (!load) return;
|
|
1517
|
+
pendingSourceLoad = null;
|
|
1518
|
+
load.resolve();
|
|
1519
|
+
}
|
|
1520
|
+
function rejectPendingSourceLoad(reason) {
|
|
1521
|
+
const load = pendingSourceLoad;
|
|
1522
|
+
if (!load) return;
|
|
1523
|
+
pendingSourceLoad = null;
|
|
1524
|
+
load.reject(reason);
|
|
1525
|
+
}
|
|
1526
|
+
function createAbortError() {
|
|
1527
|
+
return Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
1528
|
+
}
|
|
1438
1529
|
const YT_PLAYER_STATE_PLAYING = 1;
|
|
1439
1530
|
function getYouTubePlayer() {
|
|
1440
1531
|
return activeLoader?._instance?.() ?? null;
|
|
@@ -1457,11 +1548,12 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1457
1548
|
bus.emit("qualitychange", { quality, auto });
|
|
1458
1549
|
}
|
|
1459
1550
|
commands.add("hls:setQuality", (level) => {
|
|
1460
|
-
if (level
|
|
1461
|
-
|
|
1462
|
-
} else if (level !== null && typeof level === "object" && typeof level.height === "number") {
|
|
1463
|
-
bus.emit("qualitypreference", { qualityHeight: level.height });
|
|
1551
|
+
if (level !== null && (typeof level !== "object" || typeof level.height !== "number")) {
|
|
1552
|
+
return;
|
|
1464
1553
|
}
|
|
1554
|
+
bus.emit("qualitypreference", {
|
|
1555
|
+
qualityHeight: level === null ? null : level.height
|
|
1556
|
+
});
|
|
1465
1557
|
const loader = activeLoader;
|
|
1466
1558
|
if (typeof loader?.setQuality === "function") {
|
|
1467
1559
|
loader.setQuality(level === null ? "auto" : level);
|
|
@@ -1522,7 +1614,9 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1522
1614
|
commands,
|
|
1523
1615
|
controls,
|
|
1524
1616
|
store: { getState: store.getState, subscribe: store.subscribe },
|
|
1525
|
-
bus
|
|
1617
|
+
bus,
|
|
1618
|
+
sourceTransforms,
|
|
1619
|
+
mediaAttachments
|
|
1526
1620
|
});
|
|
1527
1621
|
const plugins = createPluginBus({ player: playerRef, host });
|
|
1528
1622
|
function dispatch(event) {
|
|
@@ -1534,6 +1628,10 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1534
1628
|
for (const effect of result.effects) applyEffect(effect);
|
|
1535
1629
|
if (prev !== "ready" && status === "ready") {
|
|
1536
1630
|
bus.emit("ready", { duration: store.getState().duration });
|
|
1631
|
+
resolvePendingSourceLoad();
|
|
1632
|
+
}
|
|
1633
|
+
if (event.type === "loadFailed") {
|
|
1634
|
+
rejectPendingSourceLoad(event.error);
|
|
1537
1635
|
}
|
|
1538
1636
|
}
|
|
1539
1637
|
function applyEffect(effect) {
|
|
@@ -1577,15 +1675,29 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1577
1675
|
activeQuality: null
|
|
1578
1676
|
});
|
|
1579
1677
|
bus.emit("qualitychange", { quality: null, auto: true });
|
|
1678
|
+
if (!isSafeMediaUrl(source.src) || source.tracks?.some((track) => !isSafeMediaUrl(track.src)) || source.thumbnails && !isSafeMediaUrl(source.thumbnails.src)) {
|
|
1679
|
+
if (video) {
|
|
1680
|
+
pendingSourceLoad = createPendingSourceLoad();
|
|
1681
|
+
}
|
|
1682
|
+
const error = createPlayerError({
|
|
1683
|
+
code: "unsupported",
|
|
1684
|
+
message: "Invalid URL"
|
|
1685
|
+
});
|
|
1686
|
+
dispatch({ type: "loadFailed", error });
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1580
1689
|
if (!video) return;
|
|
1690
|
+
const sourceLoad = createPendingSourceLoad();
|
|
1691
|
+
pendingSourceLoad = sourceLoad;
|
|
1581
1692
|
const provider = registry.resolve(source);
|
|
1582
1693
|
if (!provider) {
|
|
1694
|
+
const error = createPlayerError({
|
|
1695
|
+
code: "unsupported",
|
|
1696
|
+
message: `No source provider for ${source.src}`
|
|
1697
|
+
});
|
|
1583
1698
|
dispatch({
|
|
1584
1699
|
type: "loadFailed",
|
|
1585
|
-
error
|
|
1586
|
-
code: "unsupported",
|
|
1587
|
-
message: `No source provider for ${source.src}`
|
|
1588
|
-
})
|
|
1700
|
+
error
|
|
1589
1701
|
});
|
|
1590
1702
|
return;
|
|
1591
1703
|
}
|
|
@@ -1596,8 +1708,13 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1596
1708
|
aborted = true;
|
|
1597
1709
|
try {
|
|
1598
1710
|
loader.abort?.();
|
|
1599
|
-
} catch (
|
|
1600
|
-
console.error(
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
console.error(error);
|
|
1713
|
+
}
|
|
1714
|
+
if (pendingSourceLoad === sourceLoad) {
|
|
1715
|
+
rejectPendingSourceLoad(createAbortError());
|
|
1716
|
+
} else {
|
|
1717
|
+
sourceLoad.reject(createAbortError());
|
|
1601
1718
|
}
|
|
1602
1719
|
};
|
|
1603
1720
|
loader.attach(video, source).then(() => {
|
|
@@ -1699,8 +1816,6 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1699
1816
|
target.addEventListener(name, handler);
|
|
1700
1817
|
nativeListeners.push(() => target.removeEventListener(name, handler));
|
|
1701
1818
|
}
|
|
1702
|
-
for (const _ of NATIVE_EVENTS) {
|
|
1703
|
-
}
|
|
1704
1819
|
on2("loadedmetadata", () => {
|
|
1705
1820
|
store.setState({
|
|
1706
1821
|
duration: Number.isFinite(target.duration) ? target.duration : 0,
|
|
@@ -1771,17 +1886,24 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1771
1886
|
on2("playing", () => {
|
|
1772
1887
|
bus.emit("buffering", { isBuffering: false });
|
|
1773
1888
|
});
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
};
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1889
|
+
on2("enterpictureinpicture", () => {
|
|
1890
|
+
store.setState({ pip: true });
|
|
1891
|
+
bus.emit("pipchange", { pip: true });
|
|
1892
|
+
});
|
|
1893
|
+
on2("leavepictureinpicture", () => {
|
|
1894
|
+
store.setState({ pip: false });
|
|
1895
|
+
bus.emit("pipchange", { pip: false });
|
|
1896
|
+
});
|
|
1897
|
+
const ownerDocument = target.ownerDocument;
|
|
1898
|
+
const onFullscreenChange = () => {
|
|
1899
|
+
const isFullscreen = !!ownerDocument.fullscreenElement?.contains(target);
|
|
1900
|
+
store.setState({ fullscreen: isFullscreen });
|
|
1901
|
+
bus.emit("fullscreenchange", { fullscreen: isFullscreen });
|
|
1902
|
+
};
|
|
1903
|
+
ownerDocument.addEventListener("fullscreenchange", onFullscreenChange);
|
|
1904
|
+
nativeListeners.push(
|
|
1905
|
+
() => ownerDocument.removeEventListener("fullscreenchange", onFullscreenChange)
|
|
1906
|
+
);
|
|
1785
1907
|
}
|
|
1786
1908
|
function unbindNativeEvents() {
|
|
1787
1909
|
while (nativeListeners.length) {
|
|
@@ -1791,24 +1913,18 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1791
1913
|
}
|
|
1792
1914
|
}
|
|
1793
1915
|
}
|
|
1794
|
-
function
|
|
1795
|
-
if (
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1916
|
+
function applyAttachOptions() {
|
|
1917
|
+
if (!video) return;
|
|
1918
|
+
const state = store.getState();
|
|
1919
|
+
video.volume = state.volume;
|
|
1920
|
+
video.muted = state.muted;
|
|
1921
|
+
video.playbackRate = state.playbackRate;
|
|
1799
1922
|
if (options.poster) video.poster = options.poster;
|
|
1800
1923
|
if (options.preload) video.preload = options.preload;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
if ("crossOrigin" in options) {
|
|
1806
|
-
if (options.crossOrigin) video.crossOrigin = options.crossOrigin;
|
|
1807
|
-
} else {
|
|
1808
|
-
video.crossOrigin = "anonymous";
|
|
1809
|
-
}
|
|
1810
|
-
if (options.loop) video.loop = true;
|
|
1811
|
-
initialOptionsApplied = true;
|
|
1924
|
+
video.playsInline = options.playsInline ?? true;
|
|
1925
|
+
video.toggleAttribute("webkit-playsinline", video.playsInline);
|
|
1926
|
+
video.crossOrigin = "crossOrigin" in options ? options.crossOrigin ?? null : "anonymous";
|
|
1927
|
+
video.loop = !!options.loop;
|
|
1812
1928
|
}
|
|
1813
1929
|
const attachDisposers = [];
|
|
1814
1930
|
function flushAttachDisposers() {
|
|
@@ -1832,12 +1948,21 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1832
1948
|
}
|
|
1833
1949
|
async function attach(el) {
|
|
1834
1950
|
if (disposed) throw new Error("[@f8team/reel-core] cannot attach: player disposed");
|
|
1835
|
-
if (video === el)
|
|
1951
|
+
if (video === el) {
|
|
1952
|
+
await latestSourceLoadPromise;
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1836
1955
|
if (video) detach();
|
|
1837
1956
|
video = el;
|
|
1957
|
+
mediaAttachments.set({
|
|
1958
|
+
video: el,
|
|
1959
|
+
container: el.closest("[data-reel]") ?? el.parentElement ?? el
|
|
1960
|
+
});
|
|
1838
1961
|
bindNativeEvents(el);
|
|
1839
|
-
|
|
1840
|
-
|
|
1962
|
+
applyAttachOptions();
|
|
1963
|
+
const currentSource = store.getState().source;
|
|
1964
|
+
if (currentSource) setSource(currentSource);
|
|
1965
|
+
const sourceLoadPromise = latestSourceLoadPromise;
|
|
1841
1966
|
const wantStartTime = typeof options.startTime === "number" && options.startTime > 0;
|
|
1842
1967
|
const wantAutoplay = !!options.autoplay && options.autoplay !== "off" && !!options.source;
|
|
1843
1968
|
if (status === "ready") {
|
|
@@ -1859,6 +1984,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1859
1984
|
attachDisposers.push(off2);
|
|
1860
1985
|
}
|
|
1861
1986
|
}
|
|
1987
|
+
await sourceLoadPromise;
|
|
1862
1988
|
}
|
|
1863
1989
|
function detach() {
|
|
1864
1990
|
pendingAttachAbort?.();
|
|
@@ -1872,6 +1998,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1872
1998
|
}
|
|
1873
1999
|
activeLoader = null;
|
|
1874
2000
|
}
|
|
2001
|
+
mediaAttachments.set(null);
|
|
1875
2002
|
video = null;
|
|
1876
2003
|
}
|
|
1877
2004
|
function dispose() {
|
|
@@ -1886,7 +2013,8 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1886
2013
|
}
|
|
1887
2014
|
function setSource(source) {
|
|
1888
2015
|
pendingPlay = false;
|
|
1889
|
-
|
|
2016
|
+
latestSourceLoadPromise = null;
|
|
2017
|
+
dispatch({ type: "setSource", source: source ? sourceTransforms.apply(source) : null });
|
|
1890
2018
|
}
|
|
1891
2019
|
function getSource() {
|
|
1892
2020
|
return store.getState().source;
|
|
@@ -1894,7 +2022,7 @@ function createPlayer(options = {}, internal = {}) {
|
|
|
1894
2022
|
function retry() {
|
|
1895
2023
|
const current = store.getState().source;
|
|
1896
2024
|
if (!current) return false;
|
|
1897
|
-
|
|
2025
|
+
setSource(current);
|
|
1898
2026
|
return true;
|
|
1899
2027
|
}
|
|
1900
2028
|
async function play() {
|
|
@@ -2297,6 +2425,6 @@ function toSeconds(input) {
|
|
|
2297
2425
|
return parseTime(input);
|
|
2298
2426
|
}
|
|
2299
2427
|
|
|
2300
|
-
export { DEFAULT_TOKENS, THEME_PRESETS, TOKEN_TO_CSS_VAR, announce, applyTheme, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
|
2428
|
+
export { DEFAULT_TOKENS, THEME_PRESETS, TOKEN_TO_CSS_VAR, announce, applyTheme, areSourceDescriptorsEquivalent, createFocusTrap, createPlayer, definePlugin, detectSourceType, formatTime, isSafeMediaUrl, looksLikeHls, looksLikeProgressive, looksLikeYouTube, parseTime, resolveTokens, toSeconds };
|
|
2301
2429
|
//# sourceMappingURL=index.js.map
|
|
2302
2430
|
//# sourceMappingURL=index.js.map
|