@nomercy-entertainment/nomercy-video-player 2.2.1 → 2.2.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/CHANGELOG.md +24 -0
- package/dist/adapters/video-backend/IVideoBackend.d.ts +15 -1
- package/dist/adapters/video-backend/html5.d.ts +15 -6
- package/dist/adapters/video-backend/html5.js +44 -7
- package/dist/adapters/video-backend/source-outage.d.ts +24 -0
- package/dist/adapters/video-backend/source-outage.js +30 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +11 -2
- package/dist/nomercy-video-player.iife.js +1 -1
- package/dist/player/preload.d.ts +5 -1
- package/dist/player/preload.js +5 -1
- package/dist/plugins/cast-sender/index.js +7 -2
- package/dist/plugins/desktop-ui/data/buttons.js +17 -1
- package/dist/plugins/desktop-ui/helpers/menuControl.js +6 -1
- package/dist/plugins/desktop-ui/mixins/domMethods.js +10 -1
- package/dist/plugins/key-handler/index.d.ts +6 -0
- package/dist/plugins/key-handler/index.js +7 -1
- package/dist/plugins/touch-zones/index.js +1 -1
- package/dist/plugins/tv-key-handler/index.js +5 -1
- package/dist/types.d.ts +10 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.2.3] — 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `disableClickToPause` in `setup()` stops a click from pausing the video.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Streams added with `registerStream()` are now used for playback.
|
|
14
|
+
- Playlist thumbnails load from the configured image URL.
|
|
15
|
+
- Changing auth at runtime now applies to media requests.
|
|
16
|
+
- Georgia subtitles fall back to a serif font, and subtitle colors match the palette.
|
|
17
|
+
- One press of `?` opens the shortcuts overlay.
|
|
18
|
+
- Cast artwork shows the right image, and the media-key option works.
|
|
19
|
+
- The help key on a TV remote works.
|
|
20
|
+
|
|
21
|
+
## [2.2.2] — 2026-09-04
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- A 404 got the full retry ladder: thirty seconds of spinner on an episode that was never encoded, ending in the generic network error. The file is not late, it is absent, and no amount of waiting changes the answer; 410 is the same answer said more firmly. Both now get no retries and their own code, `core:stream/media-absent`, so a consumer can offer the next item instead of the dead-end overlay it shows for a decode failure. The code matches `CoreErrorCodes.MEDIA_ABSENT` in the Kotlin trio.
|
|
26
|
+
- Georgia's subtitle-font fallback was `sans-serif`. Georgia is a serif face, so a client without it installed — Android carries none — fell back to the one font every other choice already draws, collapsing distinct options into each other.
|
|
27
|
+
|
|
28
|
+
|
|
5
29
|
## [2.2.1] — 2026-09-04
|
|
6
30
|
|
|
7
31
|
### Changed
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type { AudioTrack, AuthHeaderProvider, BackendLoaderState, BackendState, HdrOnSdrFallback, MinimalBackendEventPayload, QualityLevel, SubtitleCueChange, SubtitleTrack } from '@nomercy-entertainment/nomercy-player-core';
|
|
1
|
+
import type { AudioTrack, AuthHeaderProvider, BackendLoaderState, BackendState, HdrOnSdrFallback, IStreamSource, MinimalBackendEventPayload, QualityLevel, SubtitleCueChange, SubtitleTrack } from '@nomercy-entertainment/nomercy-player-core';
|
|
2
2
|
import type { HtmlPreloadMode } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Asks the player's stream registry whether a consumer registered a factory for
|
|
5
|
+
* this URL. Returns a source to play it with, or `undefined` to let the backend
|
|
6
|
+
* handle the URL the way it always has.
|
|
7
|
+
*/
|
|
8
|
+
export type StreamResolver = (url: string, contentType?: string) => IStreamSource | undefined;
|
|
3
9
|
/**
|
|
4
10
|
* Per-event payload map. Each backend event has a fixed payload shape so
|
|
5
11
|
* `IVideoBackend.on(event, fn)` narrows the listener's parameter type
|
|
@@ -136,6 +142,14 @@ export interface IVideoBackend {
|
|
|
136
142
|
* stack omit it.
|
|
137
143
|
*/
|
|
138
144
|
setAuthHeaderProvider?(provider: AuthHeaderProvider): void;
|
|
145
|
+
/**
|
|
146
|
+
* Wire the lookup that asks the player's stream registry whether a consumer
|
|
147
|
+
* has registered a factory for a URL. The backend calls it before its own
|
|
148
|
+
* HLS and progressive handling, and falls through to that handling when the
|
|
149
|
+
* lookup returns nothing, so a player with no registered factory behaves
|
|
150
|
+
* exactly as it did before. Optional — backends with no registry omit it.
|
|
151
|
+
*/
|
|
152
|
+
setStreamResolver?(resolver: StreamResolver): void;
|
|
139
153
|
/**
|
|
140
154
|
* Consumer policy for an all-HDR item on a display that can't show HDR and
|
|
141
155
|
* a backend that can't tone-map it. Optional — backends that never face the
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { AudioTrack, HdrOnSdrFallback, QualityLevel, SubtitleTrack } from '@nomercy-entertainment/nomercy-player-core';
|
|
2
2
|
import type { HtmlPreloadMode } from '../../types.js';
|
|
3
|
-
import type { BackendEventPayload, BackendState, IVideoBackend } from './IVideoBackend.js';
|
|
3
|
+
import type { BackendEventPayload, BackendState, IVideoBackend, StreamResolver } from './IVideoBackend.js';
|
|
4
4
|
import { MediaElementBackend } from '@nomercy-entertainment/nomercy-player-core';
|
|
5
5
|
/**
|
|
6
6
|
* Default video backend. Wraps an `<HTMLVideoElement>` for transport.
|
|
7
7
|
*
|
|
8
|
-
* HLS support: native pass-through when `canPlayType` reports support
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* HLS support: native pass-through when `canPlayType` reports support,
|
|
9
|
+
* otherwise dynamically imports `hls.js` and attaches it. `hls.js` is a
|
|
10
|
+
* runtime dependency of nomercy-player-core, not a peer dependency, so an
|
|
11
|
+
* npm consumer installs it without asking. The IIFE build externalizes it
|
|
12
|
+
* as `window.Hls`; the ESM build resolves it from node_modules. MSE /
|
|
13
|
+
* WebCodecs backends ship later.
|
|
13
14
|
*/
|
|
14
15
|
export declare class Html5VideoBackend extends MediaElementBackend<HTMLVideoElement, BackendEventPayload> implements IVideoBackend {
|
|
15
16
|
readonly kind: "html5";
|
|
@@ -137,6 +138,14 @@ export declare class Html5VideoBackend extends MediaElementBackend<HTMLVideoElem
|
|
|
137
138
|
unload(): void;
|
|
138
139
|
dispose(): void;
|
|
139
140
|
private _teardownAudioGraph;
|
|
141
|
+
private _streamResolver?;
|
|
142
|
+
private _customSource?;
|
|
143
|
+
/**
|
|
144
|
+
* Wire the lookup that asks the player's stream registry for a consumer's
|
|
145
|
+
* factory. Without it the backend keeps its own HLS and progressive handling
|
|
146
|
+
* and a registered factory is never consulted.
|
|
147
|
+
*/
|
|
148
|
+
setStreamResolver(resolver: StreamResolver): void;
|
|
140
149
|
buffered(): number;
|
|
141
150
|
videoWidth(): number;
|
|
142
151
|
videoHeight(): number;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// SPDX-License-Identifier: Apache-2.0
|
|
7
7
|
// -----------------------------------------------------------------------------
|
|
8
8
|
import { abrCeiling, appendAuthTokenParam, createAuthorizationXhrSetup, destroyHlsInstance, detectDisplayHdr, hdrDecision, isHls, MediaElementBackend, MediaFormatError, mediaFormatError, panePixels, resetMediaElement, sizeAbrCeiling, supportsNativeHls, } from '@nomercy-entertainment/nomercy-player-core';
|
|
9
|
-
import { SOURCE_OUTAGE_BACKOFF_MS, sourceOutageRetryLimit } from './source-outage.js';
|
|
9
|
+
import { isMediaAbsentStatus, MEDIA_ABSENT, SOURCE_OUTAGE_BACKOFF_MS, sourceOutageRetryLimit } from './source-outage.js';
|
|
10
10
|
const CODEC_LABELS = [
|
|
11
11
|
[/avc1|h264/i, 'H.264'],
|
|
12
12
|
[/hvc1|hev1|h265/i, 'H.265 (HEVC)'],
|
|
@@ -34,11 +34,12 @@ function humanCodec(raw) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Default video backend. Wraps an `<HTMLVideoElement>` for transport.
|
|
36
36
|
*
|
|
37
|
-
* HLS support: native pass-through when `canPlayType` reports support
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* HLS support: native pass-through when `canPlayType` reports support,
|
|
38
|
+
* otherwise dynamically imports `hls.js` and attaches it. `hls.js` is a
|
|
39
|
+
* runtime dependency of nomercy-player-core, not a peer dependency, so an
|
|
40
|
+
* npm consumer installs it without asking. The IIFE build externalizes it
|
|
41
|
+
* as `window.Hls`; the ESM build resolves it from node_modules. MSE /
|
|
42
|
+
* WebCodecs backends ship later.
|
|
42
43
|
*/
|
|
43
44
|
export class Html5VideoBackend extends MediaElementBackend {
|
|
44
45
|
kind = 'html5';
|
|
@@ -238,6 +239,21 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
238
239
|
resetMediaElement(this.element);
|
|
239
240
|
performance.mark('nm:backend:load:start');
|
|
240
241
|
const headerValue = this._authHeaderProvider?.(url);
|
|
242
|
+
// A consumer's factory is asked first, because the branches below cover
|
|
243
|
+
// only HLS and whatever the element itself decodes. Nothing registered
|
|
244
|
+
// means nothing returned, and the built-in path runs untouched.
|
|
245
|
+
if (this._customSource) {
|
|
246
|
+
this._customSource.destroy();
|
|
247
|
+
this._customSource = undefined;
|
|
248
|
+
}
|
|
249
|
+
const customSource = this._streamResolver?.(url);
|
|
250
|
+
if (customSource) {
|
|
251
|
+
this._customSource = customSource;
|
|
252
|
+
await customSource.attach(this.element);
|
|
253
|
+
this._state = 'ready';
|
|
254
|
+
performance.mark('nm:backend:load:end');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
241
257
|
if (hlsUrl && !nativeHls) {
|
|
242
258
|
const { default: Hls } = await import('hls.js');
|
|
243
259
|
if (!Hls?.isSupported?.()) {
|
|
@@ -349,6 +365,16 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
349
365
|
// playbackRate / volume / mute / unmute / captureStream / setSinkId /
|
|
350
366
|
// getSinkId / mediaKeys / setMediaKeys / mediaElement / pauseLoader /
|
|
351
367
|
// resumeLoader / loaderState / setAuthHeaderProvider
|
|
368
|
+
_streamResolver;
|
|
369
|
+
_customSource;
|
|
370
|
+
/**
|
|
371
|
+
* Wire the lookup that asks the player's stream registry for a consumer's
|
|
372
|
+
* factory. Without it the backend keeps its own HLS and progressive handling
|
|
373
|
+
* and a registered factory is never consulted.
|
|
374
|
+
*/
|
|
375
|
+
setStreamResolver(resolver) {
|
|
376
|
+
this._streamResolver = resolver;
|
|
377
|
+
}
|
|
352
378
|
// ── Time / position (video-specific: buffered uses currentTime walk) ──
|
|
353
379
|
buffered() {
|
|
354
380
|
const ranges = this.element.buffered;
|
|
@@ -381,7 +407,9 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
381
407
|
default: hlsAudioTrack.default === true,
|
|
382
408
|
}));
|
|
383
409
|
}
|
|
384
|
-
// Native: HTMLMediaElement.audioTracks
|
|
410
|
+
// Native: HTMLMediaElement.audioTracks. Measured absent in Chrome 152,
|
|
411
|
+
// Edge 152 and bundled Chromium, so this branch is Safari in practice
|
|
412
|
+
// and yields [] elsewhere; the HLS path above is what fills the list.
|
|
385
413
|
const nativeTracks = this.element.audioTracks;
|
|
386
414
|
if (nativeTracks && nativeTracks.length > 0) {
|
|
387
415
|
const out = [];
|
|
@@ -860,6 +888,15 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
860
888
|
if (httpStatus === 0)
|
|
861
889
|
this._sawConnectionFailure = true;
|
|
862
890
|
const limit = sourceOutageRetryLimit(httpStatus, this._sawConnectionFailure);
|
|
891
|
+
// The server answered that this item has no media. Waiting cannot change
|
|
892
|
+
// that, and a consumer can act on it — offer the next item — but only if
|
|
893
|
+
// it is told which failure this was, so it gets its own code rather than
|
|
894
|
+
// the generic network one every outage ends with.
|
|
895
|
+
if (isMediaAbsentStatus(httpStatus) && !this._sawConnectionFailure) {
|
|
896
|
+
this._recoveringFromOutage = false;
|
|
897
|
+
this._escalateHlsError(MEDIA_ABSENT, `media absent (HTTP ${httpStatus}): ${data.details}`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
863
900
|
if (this._netRetryCount >= limit) {
|
|
864
901
|
// The ladder is spent, but a device that gets a route back later
|
|
865
902
|
// still deserves its session rather than a dead error overlay.
|
|
@@ -45,9 +45,26 @@ export declare function sourceOutageBudgetMs(): number;
|
|
|
45
45
|
* connection-failure-gated budget would cut short.
|
|
46
46
|
*/
|
|
47
47
|
export declare function isOriginDownStatus(httpStatus: number): boolean;
|
|
48
|
+
/** No rung at all: the answer will not change by asking again. */
|
|
49
|
+
export declare const NO_RETRIES = 0;
|
|
50
|
+
/**
|
|
51
|
+
* True for a status that means the server answered, and answered that this
|
|
52
|
+
* media does not exist.
|
|
53
|
+
*
|
|
54
|
+
* An episode that has not been encoded yet answers 404, and it will answer 404
|
|
55
|
+
* to every retry: the file is not late, it is absent. Waiting is not a strategy
|
|
56
|
+
* for it, and the viewer is the only one who can decide what to do instead.
|
|
57
|
+
* 410 is the same answer said more firmly.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isMediaAbsentStatus(httpStatus: number): boolean;
|
|
48
60
|
/**
|
|
49
61
|
* How many rungs this failure is allowed.
|
|
50
62
|
*
|
|
63
|
+
* A 404 gets NONE. It used to get five, which is thirty seconds of spinner on
|
|
64
|
+
* an episode that was never encoded, and at the end of it the viewer was told
|
|
65
|
+
* nothing they could act on. A definite answer is worth surfacing the moment it
|
|
66
|
+
* arrives, so the consumer can offer to move to the next item.
|
|
67
|
+
*
|
|
51
68
|
* @param httpStatus - The status the load gave up on, or 0 when the failure
|
|
52
69
|
* carried no response at all, which is what a refused connection looks like
|
|
53
70
|
* from a browser.
|
|
@@ -56,3 +73,10 @@ export declare function isOriginDownStatus(httpStatus: number): boolean;
|
|
|
56
73
|
* from a video that is genuinely gone.
|
|
57
74
|
*/
|
|
58
75
|
export declare function sourceOutageRetryLimit(httpStatus: number, sawConnectionFailure: boolean): number;
|
|
76
|
+
/**
|
|
77
|
+
* The code a consumer reads to offer "skip to the next item" rather than a dead
|
|
78
|
+
* end. Its Kotlin peer is `CoreErrorCodes.MEDIA_ABSENT`; the two trios must
|
|
79
|
+
* name this the same failure or a client written against one is wrong on the
|
|
80
|
+
* other.
|
|
81
|
+
*/
|
|
82
|
+
export declare const MEDIA_ABSENT = "core:stream/media-absent";
|
|
@@ -63,9 +63,28 @@ export function isOriginDownStatus(httpStatus) {
|
|
|
63
63
|
|| httpStatus === 503
|
|
64
64
|
|| httpStatus === 504;
|
|
65
65
|
}
|
|
66
|
+
/** No rung at all: the answer will not change by asking again. */
|
|
67
|
+
export const NO_RETRIES = 0;
|
|
68
|
+
/**
|
|
69
|
+
* True for a status that means the server answered, and answered that this
|
|
70
|
+
* media does not exist.
|
|
71
|
+
*
|
|
72
|
+
* An episode that has not been encoded yet answers 404, and it will answer 404
|
|
73
|
+
* to every retry: the file is not late, it is absent. Waiting is not a strategy
|
|
74
|
+
* for it, and the viewer is the only one who can decide what to do instead.
|
|
75
|
+
* 410 is the same answer said more firmly.
|
|
76
|
+
*/
|
|
77
|
+
export function isMediaAbsentStatus(httpStatus) {
|
|
78
|
+
return httpStatus === 404 || httpStatus === 410;
|
|
79
|
+
}
|
|
66
80
|
/**
|
|
67
81
|
* How many rungs this failure is allowed.
|
|
68
82
|
*
|
|
83
|
+
* A 404 gets NONE. It used to get five, which is thirty seconds of spinner on
|
|
84
|
+
* an episode that was never encoded, and at the end of it the viewer was told
|
|
85
|
+
* nothing they could act on. A definite answer is worth surfacing the moment it
|
|
86
|
+
* arrives, so the consumer can offer to move to the next item.
|
|
87
|
+
*
|
|
69
88
|
* @param httpStatus - The status the load gave up on, or 0 when the failure
|
|
70
89
|
* carried no response at all, which is what a refused connection looks like
|
|
71
90
|
* from a browser.
|
|
@@ -74,11 +93,22 @@ export function isOriginDownStatus(httpStatus) {
|
|
|
74
93
|
* from a video that is genuinely gone.
|
|
75
94
|
*/
|
|
76
95
|
export function sourceOutageRetryLimit(httpStatus, sawConnectionFailure) {
|
|
96
|
+
// A connection failure first means the host went away; the 404 that follows
|
|
97
|
+
// is a route table still warming up, not a missing file.
|
|
77
98
|
if (sawConnectionFailure)
|
|
78
99
|
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
79
100
|
if (isOriginDownStatus(httpStatus))
|
|
80
101
|
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
102
|
+
if (isMediaAbsentStatus(httpStatus))
|
|
103
|
+
return NO_RETRIES;
|
|
81
104
|
if (httpStatus > 0)
|
|
82
105
|
return HTTP_STATUS_RETRY_LIMIT;
|
|
83
106
|
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
84
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The code a consumer reads to offer "skip to the next item" rather than a dead
|
|
110
|
+
* end. Its Kotlin peer is `CoreErrorCodes.MEDIA_ABSENT`; the two trios must
|
|
111
|
+
* name this the same failure or a client written against one is wrong on the
|
|
112
|
+
* other.
|
|
113
|
+
*/
|
|
114
|
+
export const MEDIA_ABSENT = 'core:stream/media-absent';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionOptions, AriaLiveLevel, AudioTrack, AuthConfig, BasePlaylistItem, BufferState, CanPlayResult, CastState, CastTarget, Chapter, CurrentAudioTrackSelection, CurrentQualitySelection, CurrentSubtitleSelection, DeviceCapabilities, ICueParser, IPlatform, IPlayer, IPreloadStrategy, IStreamFactory, ITransitionStrategy, IUrlResolver, TimeState as KitTimeState, LoadOptions, NetworkState, PlaybackMetrics, PlayerExperimental, PlayerPhase, Plugin, PluginCtorWithId, QualityLevel, ResolvedUrl, SetupState, SidecarSubtitleInput, SubtitleStyle, SubtitleTrack, Translations, UrlCategory, VisibilityState } from '@nomercy-entertainment/nomercy-player-core';
|
|
1
|
+
import type { ActionOptions, AriaLiveLevel, AudioTrack, AuthConfig, BasePlaylistItem, BufferState, CanPlayResult, CastState, CastTarget, Chapter, CurrentAudioTrackSelection, CurrentQualitySelection, CurrentSubtitleSelection, DeviceCapabilities, ICueParser, IPlatform, IPlayer, IPreloadStrategy, IStreamFactory, IStreamSource, ITransitionStrategy, IUrlResolver, TimeState as KitTimeState, LoadOptions, NetworkState, PlaybackMetrics, PlayerExperimental, PlayerPhase, Plugin, PluginCtorWithId, QualityLevel, ResolvedUrl, SetupState, SidecarSubtitleInput, SubtitleStyle, SubtitleTrack, Translations, UrlCategory, VisibilityState } from '@nomercy-entertainment/nomercy-player-core';
|
|
2
2
|
import type { IVideoBackend, VideoBackendKind } from './adapters/video-backend/IVideoBackend.js';
|
|
3
3
|
import type { AudioTrackState, IVideoPlayer, PlayState, QualityState, RepeatState, SegmentOptions, ShuffleState, Stretching, VideoEventMap, VideoPlayerConfig, VideoPlaylistItem, VideoRect, VolumeState } from './types.js';
|
|
4
4
|
import { EventEmitter } from '@nomercy-entertainment/nomercy-player-core';
|
|
@@ -200,12 +200,15 @@ export declare class NMVideoPlayer<T extends VideoPlaylistItem = VideoPlaylistIt
|
|
|
200
200
|
* `mediaReady` fires so the backend's track lists are populated.
|
|
201
201
|
*
|
|
202
202
|
* Language matching: exact tag first, then prefix (e.g. `'en'` matches
|
|
203
|
-
* `'en-US'`). No match
|
|
203
|
+
* `'en-US'`). No match leaves an UNSELECTED slot alone, but where a track is
|
|
204
|
+
* already selected the slot is handed to the stream's own default rendition
|
|
205
|
+
* instead. No warning either way.
|
|
204
206
|
*/
|
|
205
207
|
private _applyDefaultTracks;
|
|
206
208
|
/** Test-only: clear the registry. Not part of the public API. */
|
|
207
209
|
static _resetRegistry(): void;
|
|
208
210
|
registerStream: (factory: IStreamFactory, prepend?: boolean) => this;
|
|
211
|
+
resolveCustomStream: (url: string, contentType?: string) => IStreamSource | undefined;
|
|
209
212
|
unregisterStream: (id: string) => this;
|
|
210
213
|
streams: () => ReadonlyArray<string>;
|
|
211
214
|
getStreamFactory: (id: string) => IStreamFactory | undefined;
|
package/dist/index.js
CHANGED
|
@@ -212,7 +212,9 @@ export class NMVideoPlayer extends EventEmitter {
|
|
|
212
212
|
* `mediaReady` fires so the backend's track lists are populated.
|
|
213
213
|
*
|
|
214
214
|
* Language matching: exact tag first, then prefix (e.g. `'en'` matches
|
|
215
|
-
* `'en-US'`). No match
|
|
215
|
+
* `'en-US'`). No match leaves an UNSELECTED slot alone, but where a track is
|
|
216
|
+
* already selected the slot is handed to the stream's own default rendition
|
|
217
|
+
* instead. No warning either way.
|
|
216
218
|
*/
|
|
217
219
|
_applyDefaultTracks() {
|
|
218
220
|
const remembered = this.languageMemory.subtitleChoice();
|
|
@@ -308,7 +310,14 @@ export class NMVideoPlayer extends EventEmitter {
|
|
|
308
310
|
// lazily so getter-style tokens (Vue refs, stores) stay live.
|
|
309
311
|
// Read per request rather than captured, so a consumer whose token or
|
|
310
312
|
// rule changes mid-session is not answered from a stale closure.
|
|
311
|
-
|
|
313
|
+
// Live config, not `options.auth`: setup seeds it and `player.auth()`
|
|
314
|
+
// updates it, so a rule set after setup reaches media requests too.
|
|
315
|
+
instance.setAuthHeaderProvider?.((url) => this.auth()?.mediaAuthorization?.(url));
|
|
316
|
+
// Without this the stream registry is seeded and never asked, so
|
|
317
|
+
// `registerStream` records a factory that no load path consults. Only
|
|
318
|
+
// consumer-registered factories are offered: HLS and progressive files
|
|
319
|
+
// stay on the backend's own path exactly as before.
|
|
320
|
+
instance.setStreamResolver?.((url, contentType) => this.resolveCustomStream?.(url, contentType));
|
|
312
321
|
// Consumer's HDR-on-SDR policy, read once here rather than normalized
|
|
313
322
|
// into `options` — `hdrDecision`'s own default ('play') applies at the
|
|
314
323
|
// point of use when this was never pushed at all.
|