@nomercy-entertainment/nomercy-video-player 2.2.0 → 2.2.2

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.2.2] — 2026-09-04
6
+
7
+ ### Fixed
8
+
9
+ - 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.
10
+ - 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.
11
+
12
+
13
+ ## [2.2.1] — 2026-09-04
14
+
15
+ ### Changed
16
+
17
+ - Built against `@nomercy-entertainment/nomercy-player-core@2.2.1`. A browser declining autoplay without a user gesture no longer escapes as an uncaught `NotAllowedError`, and `play()` no longer leaves the player reporting `PLAYING` over a stopped video — a UI that renders its play/pause button from state showed Pause over a video that never started.
18
+
19
+
5
20
  ## [2.1.10] — 2026-08-31
6
21
 
7
22
  ### Fixed
@@ -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)'],
@@ -860,6 +860,15 @@ export class Html5VideoBackend extends MediaElementBackend {
860
860
  if (httpStatus === 0)
861
861
  this._sawConnectionFailure = true;
862
862
  const limit = sourceOutageRetryLimit(httpStatus, this._sawConnectionFailure);
863
+ // The server answered that this item has no media. Waiting cannot change
864
+ // that, and a consumer can act on it — offer the next item — but only if
865
+ // it is told which failure this was, so it gets its own code rather than
866
+ // the generic network one every outage ends with.
867
+ if (isMediaAbsentStatus(httpStatus) && !this._sawConnectionFailure) {
868
+ this._recoveringFromOutage = false;
869
+ this._escalateHlsError(MEDIA_ABSENT, `media absent (HTTP ${httpStatus}): ${data.details}`);
870
+ return;
871
+ }
863
872
  if (this._netRetryCount >= limit) {
864
873
  // The ladder is spent, but a device that gets a route back later
865
874
  // 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';