@nomercy-entertainment/nomercy-video-player 2.0.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/adapters/video-backend/html5.d.ts +43 -2
- package/dist/adapters/video-backend/html5.js +107 -21
- package/dist/adapters/video-backend/source-outage.d.ts +58 -0
- package/dist/adapters/video-backend/source-outage.js +84 -0
- package/dist/index.d.ts +2 -7
- package/dist/index.js +2 -19
- package/dist/nomercy-video-player.iife.js +1 -1
- package/dist/plugins/desktop-ui/helpers/menuControl.js +10 -0
- package/dist/plugins/desktop-ui/helpers/responsive.js +10 -3
- package/dist/plugins/desktop-ui/index.js +1 -1
- package/dist/plugins/desktop-ui/styles.css +63 -6
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +1 -1
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [2.1.0] — 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Re-exports `volumeMemoryPlugin` / `VolumeMemoryPlugin` from `@nomercy-entertainment/nomercy-player-core@2.1.0`, alongside the rest of the kit's audio-graph plugins.
|
|
10
|
+
- New `./adapters` subpath export.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- A server that went away is now ridden out as an outage rather than treated as a dead file.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Built against `@nomercy-entertainment/nomercy-player-core@2.1.0`.
|
|
19
|
+
|
|
5
20
|
## [2.0.3] — 2026-07-31
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -40,6 +40,23 @@ export declare class Html5VideoBackend extends MediaElementBackend<HTMLVideoElem
|
|
|
40
40
|
private _mediaRecoveryStartMs;
|
|
41
41
|
/** Timer handle for exponential back-off retries. Cleared on unload/dispose. */
|
|
42
42
|
private _retryTimer;
|
|
43
|
+
/**
|
|
44
|
+
* Whether this outage already failed below the HTTP layer, which is what
|
|
45
|
+
* tells a restarting server's 404 apart from a video that is genuinely gone.
|
|
46
|
+
*/
|
|
47
|
+
private _sawConnectionFailure;
|
|
48
|
+
/**
|
|
49
|
+
* True while the ladder is riding out a source the server stopped serving.
|
|
50
|
+
*
|
|
51
|
+
* The backend is the first thing a consumer has that learns the origin is
|
|
52
|
+
* gone: it asks for bytes constantly, while a realtime socket through the
|
|
53
|
+
* same tunnel can stay up long after the server behind it died. An app
|
|
54
|
+
* watches this to raise its own server-offline screen on the player's
|
|
55
|
+
* evidence rather than waiting for a socket that may never drop.
|
|
56
|
+
*/
|
|
57
|
+
private _recoveringFromOutage;
|
|
58
|
+
/** Retries the moment the device has a route again, rather than waiting out a rung. */
|
|
59
|
+
private _onlineHandler;
|
|
43
60
|
private _sourceNode?;
|
|
44
61
|
private _sourceCtx?;
|
|
45
62
|
private _analyserNode?;
|
|
@@ -213,6 +230,30 @@ export declare class Html5VideoBackend extends MediaElementBackend<HTMLVideoElem
|
|
|
213
230
|
private wireElementEvents;
|
|
214
231
|
/** Reset all recovery counters. Call on successful playback resume or unload. */
|
|
215
232
|
private _resetRecoveryState;
|
|
233
|
+
/**
|
|
234
|
+
* True while the ladder is riding out a source the server stopped serving —
|
|
235
|
+
* see `_recoveringFromOutage`. A consumer probes server reachability on this
|
|
236
|
+
* so its own offline screen rises from the evidence that arrives first.
|
|
237
|
+
*/
|
|
238
|
+
get isRecoveringFromOutage(): boolean;
|
|
239
|
+
/**
|
|
240
|
+
* A server that went away is an outage to ride out, not a dead file.
|
|
241
|
+
*
|
|
242
|
+
* Escalating after three tries over seven seconds is what made a host
|
|
243
|
+
* restart read to a viewer as a broken film: the ladder was spent before
|
|
244
|
+
* the server had finished booting. The budget now matches how long a real
|
|
245
|
+
* restart takes, and how many rungs a failure earns depends on what it was
|
|
246
|
+
* — an origin-down status or a refused connection gets all of them, a cold
|
|
247
|
+
* 4xx gets five, because a URL that is genuinely gone answers the same way
|
|
248
|
+
* every time and a viewer should be told so rather than shown a spinner.
|
|
249
|
+
*/
|
|
250
|
+
private _rideOutNetworkError;
|
|
251
|
+
/**
|
|
252
|
+
* Retries the moment the device has a route again, so an outage longer than
|
|
253
|
+
* the ladder still ends in playback. Without it a Wi-Fi drop that outlasts
|
|
254
|
+
* the budget leaves the session unrecoverable until the viewer reloads.
|
|
255
|
+
*/
|
|
256
|
+
private _watchForConnectivityReturn;
|
|
216
257
|
/**
|
|
217
258
|
* Escalate a fatal HLS error to a player-level error event.
|
|
218
259
|
*
|
|
@@ -227,8 +268,8 @@ export declare class Html5VideoBackend extends MediaElementBackend<HTMLVideoElem
|
|
|
227
268
|
*
|
|
228
269
|
* Decision tree per hls.js error semantics:
|
|
229
270
|
* - Non-fatal → emit `stream:error` with `fatal: false`, no escalation.
|
|
230
|
-
* - Fatal NETWORK →
|
|
231
|
-
*
|
|
271
|
+
* - Fatal NETWORK → ride the outage out on `SOURCE_OUTAGE_BACKOFF_MS`,
|
|
272
|
+
* for as many rungs as the failure earns. If exhausted, escalate.
|
|
232
273
|
* - Fatal MEDIA → call `hls.recoverMediaError()`. If a second media
|
|
233
274
|
* error fires within 5 s, escalate.
|
|
234
275
|
* - Fatal MUX / other → destroy + reload via `load()`. If that fails, escalate.
|
|
@@ -6,6 +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
10
|
const CODEC_LABELS = [
|
|
10
11
|
[/avc1|h264/i, 'H.264'],
|
|
11
12
|
[/hvc1|hev1|h265/i, 'H.265 (HEVC)'],
|
|
@@ -69,6 +70,23 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
69
70
|
_mediaRecoveryStartMs = 0;
|
|
70
71
|
/** Timer handle for exponential back-off retries. Cleared on unload/dispose. */
|
|
71
72
|
_retryTimer;
|
|
73
|
+
/**
|
|
74
|
+
* Whether this outage already failed below the HTTP layer, which is what
|
|
75
|
+
* tells a restarting server's 404 apart from a video that is genuinely gone.
|
|
76
|
+
*/
|
|
77
|
+
_sawConnectionFailure = false;
|
|
78
|
+
/**
|
|
79
|
+
* True while the ladder is riding out a source the server stopped serving.
|
|
80
|
+
*
|
|
81
|
+
* The backend is the first thing a consumer has that learns the origin is
|
|
82
|
+
* gone: it asks for bytes constantly, while a realtime socket through the
|
|
83
|
+
* same tunnel can stay up long after the server behind it died. An app
|
|
84
|
+
* watches this to raise its own server-offline screen on the player's
|
|
85
|
+
* evidence rather than waiting for a socket that may never drop.
|
|
86
|
+
*/
|
|
87
|
+
_recoveringFromOutage = false;
|
|
88
|
+
/** Retries the moment the device has a route again, rather than waiting out a rung. */
|
|
89
|
+
_onlineHandler;
|
|
72
90
|
// ── Web Audio graph ──────────────────────────────────────────────────────
|
|
73
91
|
_sourceNode;
|
|
74
92
|
_sourceCtx;
|
|
@@ -811,6 +829,92 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
811
829
|
}
|
|
812
830
|
this._netRetryCount = 0;
|
|
813
831
|
this._mediaRecoveryStartMs = 0;
|
|
832
|
+
this._sawConnectionFailure = false;
|
|
833
|
+
this._recoveringFromOutage = false;
|
|
834
|
+
if (this._onlineHandler !== undefined) {
|
|
835
|
+
globalThis.removeEventListener?.('online', this._onlineHandler);
|
|
836
|
+
this._onlineHandler = undefined;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* True while the ladder is riding out a source the server stopped serving —
|
|
841
|
+
* see `_recoveringFromOutage`. A consumer probes server reachability on this
|
|
842
|
+
* so its own offline screen rises from the evidence that arrives first.
|
|
843
|
+
*/
|
|
844
|
+
get isRecoveringFromOutage() {
|
|
845
|
+
return this._recoveringFromOutage;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* A server that went away is an outage to ride out, not a dead file.
|
|
849
|
+
*
|
|
850
|
+
* Escalating after three tries over seven seconds is what made a host
|
|
851
|
+
* restart read to a viewer as a broken film: the ladder was spent before
|
|
852
|
+
* the server had finished booting. The budget now matches how long a real
|
|
853
|
+
* restart takes, and how many rungs a failure earns depends on what it was
|
|
854
|
+
* — an origin-down status or a refused connection gets all of them, a cold
|
|
855
|
+
* 4xx gets five, because a URL that is genuinely gone answers the same way
|
|
856
|
+
* every time and a viewer should be told so rather than shown a spinner.
|
|
857
|
+
*/
|
|
858
|
+
_rideOutNetworkError(data) {
|
|
859
|
+
const httpStatus = data.response?.code ?? 0;
|
|
860
|
+
if (httpStatus === 0)
|
|
861
|
+
this._sawConnectionFailure = true;
|
|
862
|
+
const limit = sourceOutageRetryLimit(httpStatus, this._sawConnectionFailure);
|
|
863
|
+
if (this._netRetryCount >= limit) {
|
|
864
|
+
// The ladder is spent, but a device that gets a route back later
|
|
865
|
+
// still deserves its session rather than a dead error overlay.
|
|
866
|
+
this._watchForConnectivityReturn();
|
|
867
|
+
this._recoveringFromOutage = false;
|
|
868
|
+
this._escalateHlsError(data.details, `HLS network error after ${limit} retries: ${data.details}`);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
// An outage on THIS device's side is not evidence that the server is
|
|
872
|
+
// gone, so it must not spend the budget — a two-minute Wi-Fi drop would
|
|
873
|
+
// burn the whole ladder without a single attempt ever having a route to
|
|
874
|
+
// try. Hold at the current rung and let the `online` event take over.
|
|
875
|
+
const deviceOffline = globalThis.navigator?.onLine === false;
|
|
876
|
+
const attempt = this._netRetryCount;
|
|
877
|
+
if (!deviceOffline)
|
|
878
|
+
this._netRetryCount = attempt + 1;
|
|
879
|
+
this._watchForConnectivityReturn();
|
|
880
|
+
this._recoveringFromOutage = true;
|
|
881
|
+
this.emit('stream:recovering', { details: data.details, attempt: attempt + 1, maxAttempts: limit });
|
|
882
|
+
this._retryTimer = setTimeout(() => {
|
|
883
|
+
this._retryTimer = undefined;
|
|
884
|
+
if (!this.hls)
|
|
885
|
+
return;
|
|
886
|
+
try {
|
|
887
|
+
this.hls.startLoad();
|
|
888
|
+
}
|
|
889
|
+
catch {
|
|
890
|
+
this._escalateHlsError(data.details, `HLS startLoad failed: ${data.details}`);
|
|
891
|
+
}
|
|
892
|
+
}, SOURCE_OUTAGE_BACKOFF_MS[attempt]);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Retries the moment the device has a route again, so an outage longer than
|
|
896
|
+
* the ladder still ends in playback. Without it a Wi-Fi drop that outlasts
|
|
897
|
+
* the budget leaves the session unrecoverable until the viewer reloads.
|
|
898
|
+
*/
|
|
899
|
+
_watchForConnectivityReturn() {
|
|
900
|
+
if (this._onlineHandler !== undefined)
|
|
901
|
+
return;
|
|
902
|
+
this._onlineHandler = () => {
|
|
903
|
+
// A fresh outage deserves the whole ladder: the attempts spent
|
|
904
|
+
// waiting for the network to come back say nothing about whether
|
|
905
|
+
// the server answers now.
|
|
906
|
+
this._netRetryCount = 0;
|
|
907
|
+
this._sawConnectionFailure = false;
|
|
908
|
+
if (this._retryTimer !== undefined) {
|
|
909
|
+
clearTimeout(this._retryTimer);
|
|
910
|
+
this._retryTimer = undefined;
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
this.hls?.startLoad();
|
|
914
|
+
}
|
|
915
|
+
catch { /* the ladder's next failure reports it; a throw here is not the viewer's problem */ }
|
|
916
|
+
};
|
|
917
|
+
globalThis.addEventListener?.('online', this._onlineHandler);
|
|
814
918
|
}
|
|
815
919
|
/**
|
|
816
920
|
* Escalate a fatal HLS error to a player-level error event.
|
|
@@ -836,8 +940,8 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
836
940
|
*
|
|
837
941
|
* Decision tree per hls.js error semantics:
|
|
838
942
|
* - Non-fatal → emit `stream:error` with `fatal: false`, no escalation.
|
|
839
|
-
* - Fatal NETWORK →
|
|
840
|
-
*
|
|
943
|
+
* - Fatal NETWORK → ride the outage out on `SOURCE_OUTAGE_BACKOFF_MS`,
|
|
944
|
+
* for as many rungs as the failure earns. If exhausted, escalate.
|
|
841
945
|
* - Fatal MEDIA → call `hls.recoverMediaError()`. If a second media
|
|
842
946
|
* error fires within 5 s, escalate.
|
|
843
947
|
* - Fatal MUX / other → destroy + reload via `load()`. If that fails, escalate.
|
|
@@ -848,7 +952,6 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
848
952
|
_attachHlsErrorHandler(Hls, url) {
|
|
849
953
|
if (!this.hls)
|
|
850
954
|
return;
|
|
851
|
-
const MAX_NET_RETRIES = 3;
|
|
852
955
|
this.hls.on(Hls.Events.ERROR, (_e, data) => {
|
|
853
956
|
if (!data.fatal) {
|
|
854
957
|
if (data.details === 'bufferIncompatibleCodecsError') {
|
|
@@ -868,24 +971,7 @@ export class Html5VideoBackend extends MediaElementBackend {
|
|
|
868
971
|
return;
|
|
869
972
|
}
|
|
870
973
|
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
|
871
|
-
|
|
872
|
-
this._escalateHlsError(data.details, `HLS network error after ${MAX_NET_RETRIES} retries: ${data.details}`);
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
this._netRetryCount++;
|
|
876
|
-
const delayMs = 1_000 * (2 ** (this._netRetryCount - 1)); // 1s, 2s, 4s
|
|
877
|
-
this.emit('stream:recovering', { details: data.details, attempt: this._netRetryCount, maxAttempts: MAX_NET_RETRIES });
|
|
878
|
-
this._retryTimer = setTimeout(() => {
|
|
879
|
-
this._retryTimer = undefined;
|
|
880
|
-
if (!this.hls)
|
|
881
|
-
return;
|
|
882
|
-
try {
|
|
883
|
-
this.hls.startLoad();
|
|
884
|
-
}
|
|
885
|
-
catch {
|
|
886
|
-
this._escalateHlsError(data.details, `HLS startLoad failed: ${data.details}`);
|
|
887
|
-
}
|
|
888
|
-
}, delayMs);
|
|
974
|
+
this._rideOutNetworkError(data);
|
|
889
975
|
}
|
|
890
976
|
else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
891
977
|
const now = Date.now();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling "the bytes are not there right now" apart from "this media is broken".
|
|
3
|
+
*
|
|
4
|
+
* A media-server restart produces two failure shapes in one outage:
|
|
5
|
+
* connection-refused while the host is down, then a bad HTTP status the moment
|
|
6
|
+
* it is listening again but has not finished mapping its routes. Treating only
|
|
7
|
+
* the first as recoverable ends the restart on a 404 no retry ladder ever sees,
|
|
8
|
+
* and the session dies under an error overlay while the server is two seconds
|
|
9
|
+
* from answering again.
|
|
10
|
+
*
|
|
11
|
+
* The previous ladder here was three tries over seven seconds, which is shorter
|
|
12
|
+
* than a .NET host takes to warm EF Core and reload its plugins — so the common
|
|
13
|
+
* case, a server the viewer themselves just restarted, always lost.
|
|
14
|
+
*
|
|
15
|
+
* Kept separate from the backend so the decision can be checked without a
|
|
16
|
+
* browser: every answer below is a number-to-verdict lookup, and a lookup that
|
|
17
|
+
* has drifted is invisible in a screenshot. Playback still fails, it just fails
|
|
18
|
+
* for a reason nobody chose.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Backoff for re-loading after the server drops out mid-playback. Starts tight
|
|
22
|
+
* so a momentary blip is invisible, then settles to a steady 15 s poll — long
|
|
23
|
+
* enough to sit out a host restart without hammering a server that is still
|
|
24
|
+
* booting. The ladder's length IS the give-up budget (~105 s); past that the
|
|
25
|
+
* failure is real and the error surfaces.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SOURCE_OUTAGE_BACKOFF_MS: readonly number[];
|
|
28
|
+
/**
|
|
29
|
+
* Rungs an HTTP-status failure gets when nothing connection-level preceded it
|
|
30
|
+
* (~30 s). A video whose URL is genuinely gone answers 404 identically every
|
|
31
|
+
* time, and making the viewer watch a spinner for the full budget before being
|
|
32
|
+
* told so is its own bug.
|
|
33
|
+
*/
|
|
34
|
+
export declare const HTTP_STATUS_RETRY_LIMIT = 5;
|
|
35
|
+
/** Total wall-clock the ladder covers before a failure is treated as real. */
|
|
36
|
+
export declare function sourceOutageBudgetMs(): number;
|
|
37
|
+
/**
|
|
38
|
+
* True for a status that means "the server is not there", as opposed to "the
|
|
39
|
+
* server is there and this file is not".
|
|
40
|
+
*
|
|
41
|
+
* Measured on a real outage: a NoMercy server behind cloudflared never refuses
|
|
42
|
+
* a connection while it restarts — the edge answers 530, and 52x for its other
|
|
43
|
+
* failure modes. So the shape a restart actually takes is a bad HTTP status
|
|
44
|
+
* with no connection error anywhere in front of it, which is exactly the case a
|
|
45
|
+
* connection-failure-gated budget would cut short.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isOriginDownStatus(httpStatus: number): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* How many rungs this failure is allowed.
|
|
50
|
+
*
|
|
51
|
+
* @param httpStatus - The status the load gave up on, or 0 when the failure
|
|
52
|
+
* carried no response at all, which is what a refused connection looks like
|
|
53
|
+
* from a browser.
|
|
54
|
+
* @param sawConnectionFailure - Whether anything in this outage already failed
|
|
55
|
+
* below the HTTP layer. That is what tells a restarting server's 404 apart
|
|
56
|
+
* from a video that is genuinely gone.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sourceOutageRetryLimit(httpStatus: number, sawConnectionFailure: boolean): number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling "the bytes are not there right now" apart from "this media is broken".
|
|
3
|
+
*
|
|
4
|
+
* A media-server restart produces two failure shapes in one outage:
|
|
5
|
+
* connection-refused while the host is down, then a bad HTTP status the moment
|
|
6
|
+
* it is listening again but has not finished mapping its routes. Treating only
|
|
7
|
+
* the first as recoverable ends the restart on a 404 no retry ladder ever sees,
|
|
8
|
+
* and the session dies under an error overlay while the server is two seconds
|
|
9
|
+
* from answering again.
|
|
10
|
+
*
|
|
11
|
+
* The previous ladder here was three tries over seven seconds, which is shorter
|
|
12
|
+
* than a .NET host takes to warm EF Core and reload its plugins — so the common
|
|
13
|
+
* case, a server the viewer themselves just restarted, always lost.
|
|
14
|
+
*
|
|
15
|
+
* Kept separate from the backend so the decision can be checked without a
|
|
16
|
+
* browser: every answer below is a number-to-verdict lookup, and a lookup that
|
|
17
|
+
* has drifted is invisible in a screenshot. Playback still fails, it just fails
|
|
18
|
+
* for a reason nobody chose.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Backoff for re-loading after the server drops out mid-playback. Starts tight
|
|
22
|
+
* so a momentary blip is invisible, then settles to a steady 15 s poll — long
|
|
23
|
+
* enough to sit out a host restart without hammering a server that is still
|
|
24
|
+
* booting. The ladder's length IS the give-up budget (~105 s); past that the
|
|
25
|
+
* failure is real and the error surfaces.
|
|
26
|
+
*/
|
|
27
|
+
export const SOURCE_OUTAGE_BACKOFF_MS = [
|
|
28
|
+
1_000,
|
|
29
|
+
2_000,
|
|
30
|
+
4_000,
|
|
31
|
+
8_000,
|
|
32
|
+
15_000,
|
|
33
|
+
15_000,
|
|
34
|
+
15_000,
|
|
35
|
+
15_000,
|
|
36
|
+
15_000,
|
|
37
|
+
15_000,
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Rungs an HTTP-status failure gets when nothing connection-level preceded it
|
|
41
|
+
* (~30 s). A video whose URL is genuinely gone answers 404 identically every
|
|
42
|
+
* time, and making the viewer watch a spinner for the full budget before being
|
|
43
|
+
* told so is its own bug.
|
|
44
|
+
*/
|
|
45
|
+
export const HTTP_STATUS_RETRY_LIMIT = 5;
|
|
46
|
+
/** Total wall-clock the ladder covers before a failure is treated as real. */
|
|
47
|
+
export function sourceOutageBudgetMs() {
|
|
48
|
+
return SOURCE_OUTAGE_BACKOFF_MS.reduce((total, rung) => total + rung, 0);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* True for a status that means "the server is not there", as opposed to "the
|
|
52
|
+
* server is there and this file is not".
|
|
53
|
+
*
|
|
54
|
+
* Measured on a real outage: a NoMercy server behind cloudflared never refuses
|
|
55
|
+
* a connection while it restarts — the edge answers 530, and 52x for its other
|
|
56
|
+
* failure modes. So the shape a restart actually takes is a bad HTTP status
|
|
57
|
+
* with no connection error anywhere in front of it, which is exactly the case a
|
|
58
|
+
* connection-failure-gated budget would cut short.
|
|
59
|
+
*/
|
|
60
|
+
export function isOriginDownStatus(httpStatus) {
|
|
61
|
+
return (httpStatus >= 520 && httpStatus <= 530)
|
|
62
|
+
|| httpStatus === 502
|
|
63
|
+
|| httpStatus === 503
|
|
64
|
+
|| httpStatus === 504;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* How many rungs this failure is allowed.
|
|
68
|
+
*
|
|
69
|
+
* @param httpStatus - The status the load gave up on, or 0 when the failure
|
|
70
|
+
* carried no response at all, which is what a refused connection looks like
|
|
71
|
+
* from a browser.
|
|
72
|
+
* @param sawConnectionFailure - Whether anything in this outage already failed
|
|
73
|
+
* below the HTTP layer. That is what tells a restarting server's 404 apart
|
|
74
|
+
* from a video that is genuinely gone.
|
|
75
|
+
*/
|
|
76
|
+
export function sourceOutageRetryLimit(httpStatus, sawConnectionFailure) {
|
|
77
|
+
if (sawConnectionFailure)
|
|
78
|
+
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
79
|
+
if (isOriginDownStatus(httpStatus))
|
|
80
|
+
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
81
|
+
if (httpStatus > 0)
|
|
82
|
+
return HTTP_STATUS_RETRY_LIMIT;
|
|
83
|
+
return SOURCE_OUTAGE_BACKOFF_MS.length;
|
|
84
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -103,12 +103,6 @@ export declare class NMVideoPlayer<T extends VideoPlaylistItem = VideoPlaylistIt
|
|
|
103
103
|
timeData: () => KitTimeState;
|
|
104
104
|
/** Seek to a position expressed as a percentage (0–100) of total duration. */
|
|
105
105
|
seekByPercentage: (pct: number, opts?: ActionOptions) => void;
|
|
106
|
-
/** @internal */
|
|
107
|
-
private _checkItemEndingSoon;
|
|
108
|
-
/** @internal */
|
|
109
|
-
private _timeStateAt;
|
|
110
|
-
/** @internal */
|
|
111
|
-
private _mediaIsStale;
|
|
112
106
|
playbackRate: {
|
|
113
107
|
(): number;
|
|
114
108
|
(rate: number): Promise<void>;
|
|
@@ -258,7 +252,6 @@ export declare class NMVideoPlayer<T extends VideoPlaylistItem = VideoPlaylistIt
|
|
|
258
252
|
togglePip(): void;
|
|
259
253
|
toggleTheater(): void;
|
|
260
254
|
cycleSubtitles(): void;
|
|
261
|
-
cycleAudioTracks(): void;
|
|
262
255
|
private _aspectRatio;
|
|
263
256
|
private static readonly _OBJECT_FIT_MAP;
|
|
264
257
|
aspectRatio(): Stretching;
|
|
@@ -335,6 +328,8 @@ export declare class NMVideoPlayer<T extends VideoPlaylistItem = VideoPlaylistIt
|
|
|
335
328
|
(): CurrentAudioTrackSelection | null;
|
|
336
329
|
(idx: number): Promise<void>;
|
|
337
330
|
};
|
|
331
|
+
/** Composed in via `mediaTracksMethods` — see that mixin for the full contract. */
|
|
332
|
+
cycleAudioTracks: () => void;
|
|
338
333
|
qualityLevels: {
|
|
339
334
|
(): QualityLevel[];
|
|
340
335
|
(opts: {
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,7 @@ function _matchLanguage(candidates, target) {
|
|
|
63
63
|
*/
|
|
64
64
|
export class NMVideoPlayer extends EventEmitter {
|
|
65
65
|
playerId = '';
|
|
66
|
-
container
|
|
66
|
+
container;
|
|
67
67
|
videoElement;
|
|
68
68
|
get id() {
|
|
69
69
|
return this.playerId;
|
|
@@ -528,24 +528,7 @@ export class NMVideoPlayer extends EventEmitter {
|
|
|
528
528
|
const next = current >= list.length - 1 ? -1 : current + 1;
|
|
529
529
|
this.subtitle(next === -1 ? null : next);
|
|
530
530
|
}
|
|
531
|
-
cycleAudioTracks()
|
|
532
|
-
let list = [];
|
|
533
|
-
try {
|
|
534
|
-
list = this.audioTracks();
|
|
535
|
-
}
|
|
536
|
-
catch { /* tracks API not implemented yet — treat as empty */ }
|
|
537
|
-
if (!list || list.length === 0)
|
|
538
|
-
return;
|
|
539
|
-
let current = -1;
|
|
540
|
-
try {
|
|
541
|
-
const sel = this.audioTrack();
|
|
542
|
-
if (sel != null && typeof sel.index === 'number' && sel.index >= 0)
|
|
543
|
-
current = sel.index;
|
|
544
|
-
}
|
|
545
|
-
catch { /* state unavailable — start from 0 */ }
|
|
546
|
-
const next = current >= list.length - 1 ? 0 : current + 1;
|
|
547
|
-
this.audioTrack(next);
|
|
548
|
-
}
|
|
531
|
+
// cycleAudioTracks — composed in via `mediaTracksMethods` mixin (core, shared with music).
|
|
549
532
|
_aspectRatio = 'uniform';
|
|
550
533
|
static _OBJECT_FIT_MAP = {
|
|
551
534
|
uniform: 'contain',
|