prismcast 1.5.2 → 1.6.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/README.md +44 -28
- package/dist/app.d.ts +5 -0
- package/dist/app.js +63 -5
- package/dist/app.js.map +1 -1
- package/dist/browser/channelSelection.js +14 -2
- package/dist/browser/channelSelection.js.map +1 -1
- package/dist/browser/index.d.ts +23 -9
- package/dist/browser/index.js +205 -88
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/precaching.js +3 -3
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tuning/directv.js +13 -0
- package/dist/browser/tuning/directv.js.map +1 -1
- package/dist/browser/tuning/fox.js +12 -0
- package/dist/browser/tuning/fox.js.map +1 -1
- package/dist/browser/tuning/hbo.js +11 -0
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/tuning/hulu.js +23 -0
- package/dist/browser/tuning/hulu.js.map +1 -1
- package/dist/browser/tuning/sling.js +34 -4
- package/dist/browser/tuning/sling.js.map +1 -1
- package/dist/browser/tuning/spectrum.js +12 -0
- package/dist/browser/tuning/spectrum.js.map +1 -1
- package/dist/browser/tuning/xfinity.d.ts +2 -0
- package/dist/browser/tuning/xfinity.js +607 -0
- package/dist/browser/tuning/xfinity.js.map +1 -0
- package/dist/browser/tuning/youtubeTv.js +14 -0
- package/dist/browser/tuning/youtubeTv.js.map +1 -1
- package/dist/browser/video.d.ts +2 -0
- package/dist/browser/video.js +99 -17
- package/dist/browser/video.js.map +1 -1
- package/dist/channels/index.js +173 -56
- package/dist/channels/index.js.map +1 -1
- package/dist/config/health.d.ts +28 -28
- package/dist/config/health.js +52 -52
- package/dist/config/health.js.map +1 -1
- package/dist/config/paths.d.ts +12 -0
- package/dist/config/paths.js +16 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/profiles.d.ts +3 -2
- package/dist/config/profiles.js +45 -34
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/providers.d.ts +7 -0
- package/dist/config/providers.js +14 -0
- package/dist/config/providers.js.map +1 -1
- package/dist/config/sites.d.ts +28 -0
- package/dist/config/sites.js +93 -122
- package/dist/config/sites.js.map +1 -1
- package/dist/config/userChannels.d.ts +3 -0
- package/dist/config/userChannels.js +10 -3
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +1 -0
- package/dist/config/userConfig.js +4 -0
- package/dist/config/userConfig.js.map +1 -1
- package/dist/config/userProfiles.d.ts +0 -1
- package/dist/config/userProfiles.js +18 -10
- package/dist/config/userProfiles.js.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/native/decrypt.d.ts +32 -0
- package/dist/native/decrypt.js +85 -0
- package/dist/native/decrypt.js.map +1 -0
- package/dist/native/index.d.ts +58 -0
- package/dist/native/index.js +341 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/intercept.d.ts +35 -0
- package/dist/native/intercept.js +184 -0
- package/dist/native/intercept.js.map +1 -0
- package/dist/native/probe.d.ts +48 -0
- package/dist/native/probe.js +270 -0
- package/dist/native/probe.js.map +1 -0
- package/dist/native/proxy.d.ts +52 -0
- package/dist/native/proxy.js +904 -0
- package/dist/native/proxy.js.map +1 -0
- package/dist/routes/config/channels/table.js +19 -25
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/providers.js +4 -4
- package/dist/routes/config/providers.js.map +1 -1
- package/dist/routes/debug.js +5 -5
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/hls.js +12 -2
- package/dist/routes/hls.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/root/content.js +80 -20
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/status.js +14 -13
- package/dist/routes/root/scripts/status.js.map +1 -1
- package/dist/routes/root/styles.js +4 -2
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +3 -0
- package/dist/streaming/fmp4Segmenter.js +143 -42
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +24 -19
- package/dist/streaming/hls.js +486 -261
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +7 -0
- package/dist/streaming/hlsResume.js +22 -2
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/hlsSegments.d.ts +42 -2
- package/dist/streaming/hlsSegments.js +109 -8
- package/dist/streaming/hlsSegments.js.map +1 -1
- package/dist/streaming/lifecycle.js +20 -2
- package/dist/streaming/lifecycle.js.map +1 -1
- package/dist/streaming/monitor.d.ts +1 -0
- package/dist/streaming/monitor.js +369 -21
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +52 -6
- package/dist/streaming/mp4Parser.js +156 -30
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/mpegts.d.ts +2 -2
- package/dist/streaming/mpegts.js +156 -109
- package/dist/streaming/mpegts.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +38 -0
- package/dist/streaming/playlistBuilder.js +66 -0
- package/dist/streaming/playlistBuilder.js.map +1 -0
- package/dist/streaming/preroll.d.ts +106 -0
- package/dist/streaming/preroll.js +373 -0
- package/dist/streaming/preroll.js.map +1 -0
- package/dist/streaming/pretune.d.ts +8 -0
- package/dist/streaming/pretune.js +226 -0
- package/dist/streaming/pretune.js.map +1 -0
- package/dist/streaming/registry.d.ts +24 -3
- package/dist/streaming/registry.js +23 -1
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +7 -0
- package/dist/streaming/setup.js +27 -6
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +24 -0
- package/dist/streaming/showInfo.js +55 -3
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +6 -4
- package/dist/streaming/statusEmitter.js +9 -3
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/channels.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/profiles.d.ts +8 -1
- package/dist/types/selection.d.ts +4 -1
- package/dist/types/streaming.d.ts +7 -0
- package/dist/utils/chromeFetch.d.ts +20 -0
- package/dist/utils/chromeFetch.js +43 -0
- package/dist/utils/chromeFetch.js.map +1 -0
- package/dist/utils/debugFilter.js +15 -0
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/utils/delay.d.ts +16 -0
- package/dist/utils/delay.js +15 -1
- package/dist/utils/delay.js.map +1 -1
- package/dist/utils/ffmpeg.d.ts +1 -7
- package/dist/utils/ffmpeg.js +86 -127
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/pid.d.ts +40 -0
- package/dist/utils/pid.js +83 -0
- package/dist/utils/pid.js.map +1 -0
- package/package.json +5 -5
|
@@ -2,10 +2,14 @@ import { EvaluateTimeoutError, LOG, formatError, getAbortSignal, isSessionClosed
|
|
|
2
2
|
import { RECOVERY_METHODS, capitalize, checkCircuitBreaker, createRecoveryMetrics, formatIssueType, formatRecoveryDuration, getIssueCategory, getIssueDescription, getRecoveryMethod, recordRecoveryAttempt, recordRecoverySuccess, resetCircuitBreaker } from "./recovery.js";
|
|
3
3
|
import { applyVideoStyles, buildVideoSelectorType, checkVideoPresence, enforceVideoVolume, ensurePlayback, findVideoContext, getVideoState, tuneToChannel, validateVideoElement, verifyFullscreen } from "../browser/video.js";
|
|
4
4
|
import { getChannelLogo, getShowName } from "./showInfo.js";
|
|
5
|
-
import { getLastSegmentSize, getStream, getStreamMemoryUsage } from "./registry.js";
|
|
5
|
+
import { getLastSegmentHasVideo, getLastSegmentSize, getStream, getStreamMemoryUsage } from "./registry.js";
|
|
6
6
|
import { CONFIG } from "../config/index.js";
|
|
7
|
+
import { clearProbeCache } from "../native/probe.js";
|
|
7
8
|
import { emitStreamHealthChanged } from "./statusEmitter.js";
|
|
8
9
|
import { getClientSummary } from "./clients.js";
|
|
10
|
+
import { getEffectiveViewport } from "../config/presets.js";
|
|
11
|
+
import { getProviderBySlug } from "../browser/channelSelection.js";
|
|
12
|
+
import { refreshNativeManifest } from "../native/index.js";
|
|
9
13
|
import { resizeAndMinimizeWindow } from "../browser/cdp.js";
|
|
10
14
|
export function monitorPlaybackHealth(page, context, profile, url, streamId, streamInfo, onCircuitBreak, onTabReplacement) {
|
|
11
15
|
/* Monitor state variables. These track the video's behavior over time and control recovery decisions.
|
|
@@ -90,11 +94,22 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
90
94
|
// segments contain only audio data. Audio is transcoded at a controlled bitrate (max 512Kbps), so audio-only segments are at most ~192KB for 3-second segments.
|
|
91
95
|
// The 500KB threshold catches both dead captures (18 bytes) and audio-only captures while staying well below the smallest video preset (480p/3Mbps ≈ 750KB/segment).
|
|
92
96
|
const TINY_SEGMENT_THRESHOLD = 512000; // 500KB - segments below this indicate dead or degraded capture.
|
|
93
|
-
const TINY_SEGMENT_COUNT_TRIGGER = 10; //
|
|
97
|
+
const TINY_SEGMENT_COUNT_TRIGGER = 10; // Default trigger count: 10 consecutive tiny segments (~20 seconds with 2-second segments).
|
|
98
|
+
// Resolve the provider-specific tiny segment count threshold once at monitor startup. Providers with extended static content (e.g., Xfinity commercial
|
|
99
|
+
// placeholders) set a higher value to tolerate longer periods of small segments without false positive tab replacements. Dead capture pipelines (segments with
|
|
100
|
+
// no video trafs) always use TINY_SEGMENT_COUNT_TRIGGER regardless of this setting.
|
|
101
|
+
const providerModule = streamInfo.providerTag ? getProviderBySlug(streamInfo.providerTag) : undefined;
|
|
102
|
+
const providerTinySegmentThreshold = providerModule?.tinySegmentThreshold ?? TINY_SEGMENT_COUNT_TRIGGER;
|
|
94
103
|
// Segment staleness timeout. When no new segments have been produced for this duration, the capture pipeline is considered dead even though the video element may
|
|
95
104
|
// appear healthy. This catches the case where Chrome's MediaRecorder silently stops emitting data without raising an error — the input stream stays "open" but no
|
|
96
105
|
// data events fire. The 20-second threshold is 4x the maximum expected moof delivery interval (5 seconds) to avoid false positives during normal bursty delivery.
|
|
97
106
|
const SEGMENT_STALENESS_TIMEOUT = 20000; // 20 seconds.
|
|
107
|
+
// Resolution degradation detection. When the video element's intrinsic resolution is significantly below the configured viewport, the provider's ABR is delivering
|
|
108
|
+
// low-quality content. The threshold is expressed as a ratio — if either dimension is below this fraction of the viewport, the resolution is considered degraded.
|
|
109
|
+
// 50% catches clear ABR degradation (768×432 on 1080p = 40%) while allowing legitimate 720p content on 1080p (67% > 50%).
|
|
110
|
+
const RESOLUTION_RATIO_THRESHOLD = 0.5;
|
|
111
|
+
// Grace period in milliseconds after stream start and after each recovery action. Gives ABR time to ramp up before flagging degradation.
|
|
112
|
+
const RESOLUTION_GRACE_PERIOD = 30000;
|
|
98
113
|
// Fixed margin in milliseconds before the maxContinuousPlayback limit at which a proactive reload is triggered. Two minutes provides enough time for page
|
|
99
114
|
// navigation and video reinitialization to complete before the site enforces its cutoff.
|
|
100
115
|
const PROACTIVE_RELOAD_MARGIN_MS = 120000;
|
|
@@ -107,6 +122,232 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
107
122
|
const selectorType = buildVideoSelectorType(profile);
|
|
108
123
|
// Capture stream context for re-establishing on each interval tick. AsyncLocalStorage context is lost when entering setInterval callbacks.
|
|
109
124
|
const streamContext = { channelName: streamInfo.channelName ?? undefined, streamId, url };
|
|
125
|
+
// Native stream health state. These variables are only used when the stream is in native mode. They track segment delivery health to detect stalled streams
|
|
126
|
+
// where the provider's manifest stops advancing or segments stop arriving. Native recovery uses the shared `recoveryInProgress` flag rather than a separate flag,
|
|
127
|
+
// since the interval callback already checks `recoveryInProgress` before dispatching to either the native or capture-mode health check path.
|
|
128
|
+
let nativeLastCheckedSegmentIndex = 0;
|
|
129
|
+
let nativeLastSegmentAdvanceTime = Date.now();
|
|
130
|
+
let nativeHealthIssueType = null;
|
|
131
|
+
let nativeHealthIssueTime = null;
|
|
132
|
+
let nativeRecoveryAttempts = 0;
|
|
133
|
+
// Resolution degradation monitoring. Separate from the existing recovery escalation (L1-L4) which handles broken playback. Resolution degradation is a quality
|
|
134
|
+
// issue — the stream works but at lower-than-expected resolution. Uses its own tracking and two-step escalation: page reload, then tab replacement.
|
|
135
|
+
let resolutionGraceEnd = Date.now() + RESOLUTION_GRACE_PERIOD;
|
|
136
|
+
let resolutionRecoveryAttempt = 0;
|
|
137
|
+
/**
|
|
138
|
+
* Checks segment delivery health for native streams. Detects stalled streams by comparing the proxy's segment index and last segment timestamp against thresholds.
|
|
139
|
+
* Recovery follows three escalation levels per the plan:
|
|
140
|
+
*
|
|
141
|
+
* - L1: Re-fetch manifest (handled by the proxy's internal retry loop — consecutive failures up to the threshold)
|
|
142
|
+
* - L2: Reload page for fresh tokens (same mechanism as proactive token refresh, but triggered by segment staleness)
|
|
143
|
+
* - L3: Fall back to capture mode via tab replacement (stops native proxy, creates fresh page with capture pipeline)
|
|
144
|
+
*
|
|
145
|
+
* Note: The `recoveryInProgress` guard at the top of the interval callback prevents re-entry during async L2/L3 recovery. The native path does not need its own
|
|
146
|
+
* guard — it reuses the shared flag.
|
|
147
|
+
*
|
|
148
|
+
* @param entry - The stream registry entry for the native stream.
|
|
149
|
+
*/
|
|
150
|
+
function checkNativeStreamHealth(entry) {
|
|
151
|
+
const proxy = entry.nativeProxy;
|
|
152
|
+
if (!proxy) {
|
|
153
|
+
emitStatusUpdate();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const currentSegmentIndex = proxy.getSegmentIndex();
|
|
158
|
+
const lastSegmentTime = proxy.getLastSegmentTime();
|
|
159
|
+
const targetDuration = proxy.getTargetDuration();
|
|
160
|
+
const consecutiveErrors = proxy.getConsecutiveErrors();
|
|
161
|
+
const storeKey = entry.info.storeKey;
|
|
162
|
+
// Fast path: if the proxy hit its error threshold and stopped itself, trigger L3 fallback immediately. This avoids waiting for the staleness threshold when hard
|
|
163
|
+
// errors (HTTP 403, network failures) have already been detected by the proxy's internal retry loop.
|
|
164
|
+
if (proxy.hasErrored()) {
|
|
165
|
+
LOG.debug("native:monitor", "Native proxy errored for %s. Initiating capture fallback.", storeKey);
|
|
166
|
+
nativeHealthIssueType = "proxy error";
|
|
167
|
+
nativeHealthIssueTime = now;
|
|
168
|
+
void runWithStreamContext(streamContext, async () => {
|
|
169
|
+
await executeNativeL3Fallback(entry);
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Check if new segments have been produced since the last tick.
|
|
174
|
+
if (currentSegmentIndex > nativeLastCheckedSegmentIndex) {
|
|
175
|
+
nativeLastCheckedSegmentIndex = currentSegmentIndex;
|
|
176
|
+
nativeLastSegmentAdvanceTime = now;
|
|
177
|
+
// Clear any previous issue tracking when segments are flowing.
|
|
178
|
+
if (nativeHealthIssueType) {
|
|
179
|
+
nativeHealthIssueType = null;
|
|
180
|
+
nativeHealthIssueTime = null;
|
|
181
|
+
nativeRecoveryAttempts = 0;
|
|
182
|
+
LOG.debug("native:monitor", "Native stream healthy for %s. Segments advancing (index %s).", storeKey, currentSegmentIndex);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Calculate staleness: time since the last new segment was produced.
|
|
186
|
+
const stalenessMs = now - nativeLastSegmentAdvanceTime;
|
|
187
|
+
const stalenessThreshold = targetDuration * 2 * 1000;
|
|
188
|
+
// Classify health based on segment delivery metrics.
|
|
189
|
+
let nativeHealth = "healthy";
|
|
190
|
+
if (consecutiveErrors > 0) {
|
|
191
|
+
nativeHealth = "recovering";
|
|
192
|
+
if (!nativeHealthIssueType) {
|
|
193
|
+
nativeHealthIssueType = "fetch errors";
|
|
194
|
+
nativeHealthIssueTime = now;
|
|
195
|
+
}
|
|
196
|
+
LOG.debug("native:monitor", "Native stream recovering for %s. Consecutive errors: %s.", storeKey, consecutiveErrors);
|
|
197
|
+
}
|
|
198
|
+
else if ((stalenessMs > stalenessThreshold) && (lastSegmentTime > 0)) {
|
|
199
|
+
// Only flag staleness after at least one segment has been produced (lastSegmentTime > 0) and the staleness exceeds the threshold.
|
|
200
|
+
nativeHealth = "stalled";
|
|
201
|
+
if (!nativeHealthIssueType) {
|
|
202
|
+
nativeHealthIssueType = "segment stall";
|
|
203
|
+
nativeHealthIssueTime = now;
|
|
204
|
+
}
|
|
205
|
+
const staleSec = Math.round(stalenessMs / 1000);
|
|
206
|
+
LOG.debug("native:monitor", "Native stream stalled for %s. No new segments in %ss (threshold: %ss).", storeKey, staleSec, Math.round(stalenessThreshold / 1000));
|
|
207
|
+
// L2: At 4× target duration, attempt a page reload for fresh tokens. This recovers from auth expiry where the manifest URL is still valid but segment URLs
|
|
208
|
+
// are rejected. The proxy continues serving cached segments during the reload.
|
|
209
|
+
if ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeRecoveryAttempts === 0)) {
|
|
210
|
+
LOG.warn("Native stream stalled for %s. No new segments in %ss. Attempting recovery.", storeKey, staleSec);
|
|
211
|
+
nativeRecoveryAttempts++;
|
|
212
|
+
void runWithStreamContext(streamContext, async () => {
|
|
213
|
+
await executeNativeL2Recovery(entry);
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// L3: At 6× target duration (or if L2 was already attempted), fall back to capture mode via tab replacement.
|
|
218
|
+
if ((stalenessMs > (targetDuration * 6 * 1000)) || ((stalenessMs > (targetDuration * 4 * 1000)) && (nativeRecoveryAttempts > 0))) {
|
|
219
|
+
LOG.warn("Falling back to capture mode for %s: native streaming stalled after recovery attempt.", storeKey);
|
|
220
|
+
void runWithStreamContext(streamContext, async () => {
|
|
221
|
+
await executeNativeL3Fallback(entry);
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
emitNativeStatus(entry, nativeHealth);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Emits a status update with native-specific health classification. Populates meaningful fields (health, issue tracking, memory, clients) and zeroes video-specific
|
|
230
|
+
* fields that are not applicable to native streams.
|
|
231
|
+
*
|
|
232
|
+
* @param entry - The stream registry entry.
|
|
233
|
+
* @param health - The health status to report.
|
|
234
|
+
*/
|
|
235
|
+
function emitNativeStatus(entry, health) {
|
|
236
|
+
if (intervalCleared) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const memoryBytes = getStreamMemoryUsage(entry).total;
|
|
241
|
+
const channelKey = entry.info.storeKey;
|
|
242
|
+
const clientSummary = getClientSummary(streamInfo.numericStreamId);
|
|
243
|
+
const escalation = (health === "stalled") ? 1 : ((health === "recovering") ? 2 : 0);
|
|
244
|
+
const status = {
|
|
245
|
+
bufferingDuration: null,
|
|
246
|
+
channel: streamInfo.channelName,
|
|
247
|
+
clientCount: clientSummary.total,
|
|
248
|
+
clients: clientSummary.clients,
|
|
249
|
+
currentTime: 0,
|
|
250
|
+
duration: Math.round((now - streamInfo.startTime.getTime()) / 1000),
|
|
251
|
+
escalationLevel: escalation,
|
|
252
|
+
health,
|
|
253
|
+
id: streamInfo.numericStreamId,
|
|
254
|
+
lastIssueTime: nativeHealthIssueTime,
|
|
255
|
+
lastIssueType: nativeHealthIssueType,
|
|
256
|
+
lastRecoveryTime: null,
|
|
257
|
+
logoUrl: channelKey ? (getChannelLogo(channelKey) ?? "") : "",
|
|
258
|
+
memoryBytes,
|
|
259
|
+
networkState: 0,
|
|
260
|
+
pageReloadsInWindow: 0,
|
|
261
|
+
providerName: streamInfo.providerName,
|
|
262
|
+
readyState: 0,
|
|
263
|
+
recoveryAttempts: nativeRecoveryAttempts,
|
|
264
|
+
showName: getShowName(streamInfo.numericStreamId),
|
|
265
|
+
startTime: streamInfo.startTime.toISOString(),
|
|
266
|
+
streamingMode: entry.streamingMode,
|
|
267
|
+
url
|
|
268
|
+
};
|
|
269
|
+
emitStreamHealthChanged(status);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* L2 recovery for native streams: reloads the page to get fresh authentication tokens and re-intercepts the manifest. Delegates to the shared refreshNativeManifest
|
|
273
|
+
* helper in the coordinator module, which handles interceptor installation, navigation, probing, isStopped() guards, and proxy updates.
|
|
274
|
+
*
|
|
275
|
+
* @param entry - The stream registry entry.
|
|
276
|
+
*/
|
|
277
|
+
async function executeNativeL2Recovery(entry) {
|
|
278
|
+
const proxy = entry.nativeProxy;
|
|
279
|
+
if (!proxy || proxy.isStopped()) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
recoveryInProgress = true;
|
|
283
|
+
LOG.debug("native:monitor", "Starting L2 recovery (page reload) for %s.", entry.info.storeKey);
|
|
284
|
+
try {
|
|
285
|
+
const success = await refreshNativeManifest({
|
|
286
|
+
channelName: entry.info.storeKey,
|
|
287
|
+
page: currentPage,
|
|
288
|
+
proxy,
|
|
289
|
+
streamIdStr: streamId,
|
|
290
|
+
url
|
|
291
|
+
});
|
|
292
|
+
if (success) {
|
|
293
|
+
// Reset staleness tracking so the monitor gives the refreshed stream time to produce segments.
|
|
294
|
+
nativeLastSegmentAdvanceTime = Date.now();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
recoveryInProgress = false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* L3 recovery for native streams: falls back to capture mode via tab replacement. Stops the native proxy, creates a fresh page with capture pipeline, and switches
|
|
303
|
+
* the stream to capture mode. The existing tab replacement infrastructure handles page creation, capture initialization, segmenter creation, and registry updates.
|
|
304
|
+
*
|
|
305
|
+
* If the proxy's onError fires concurrently (from the poll loop hitting its failure threshold), terminateStream runs before this async function gets a chance to
|
|
306
|
+
* execute. By the time L3 runs, the stream is already terminated and executeTabReplacement returns null, which we handle as a failed outcome.
|
|
307
|
+
*
|
|
308
|
+
* @param entry - The stream registry entry.
|
|
309
|
+
*/
|
|
310
|
+
async function executeNativeL3Fallback(entry) {
|
|
311
|
+
if (!onTabReplacement) {
|
|
312
|
+
LOG.warn("Capture fallback not available for %s: no tab replacement handler.", entry.info.storeKey);
|
|
313
|
+
onCircuitBreak();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
LOG.debug("native:monitor", "Starting L3 fallback (capture mode) for %s.", entry.info.storeKey);
|
|
317
|
+
// Stop the native proxy before tab replacement closes the page. The proxy may still be polling and would encounter errors when the page navigates away.
|
|
318
|
+
if (entry.nativeProxy) {
|
|
319
|
+
entry.nativeProxy.stop();
|
|
320
|
+
entry.nativeProxy = null;
|
|
321
|
+
}
|
|
322
|
+
// Use the existing tab replacement infrastructure. It sets recoveryInProgress = true internally and clears it in finalizeTabReplacement. It creates a new page
|
|
323
|
+
// with capture, navigates, sets up playback, creates a segmenter, and updates the registry entry (page, rawCaptureStream, ffmpegProcess, segmenter).
|
|
324
|
+
const outcome = await executeTabReplacement("native fallback to capture");
|
|
325
|
+
if (outcome.outcome === "success") {
|
|
326
|
+
// Tab replacement succeeded. Update the registry to reflect capture mode. The tab replacement handler already set page, rawCaptureStream, ffmpegProcess,
|
|
327
|
+
// and segmenter on the registry entry. We just need to update the streaming mode and clear audio state.
|
|
328
|
+
entry.streamingMode = "capture";
|
|
329
|
+
// Clear separate audio state from the native proxy. Without this, hasAudio remains true and the HLS handler continues serving the master playlist (referencing
|
|
330
|
+
// video.m3u8 and audio.m3u8) instead of the capture segmenter's variant playlist. Clients that cached the master playlist structure would request stale audio
|
|
331
|
+
// and video variant playlists pointing to segments that are no longer being updated.
|
|
332
|
+
entry.hls.hasAudio = false;
|
|
333
|
+
entry.hls.audioPlaylist = "";
|
|
334
|
+
entry.hls.audioSegments.clear();
|
|
335
|
+
entry.hls.videoPlaylist = "";
|
|
336
|
+
// Clear the probe cache so subsequent tunes to this channel don't re-attempt native streaming.
|
|
337
|
+
clearProbeCache(entry.info.storeKey);
|
|
338
|
+
LOG.info("Switched to capture mode for %s: native streaming failed.", entry.info.storeKey);
|
|
339
|
+
// The monitor's next tick will see streamingMode === "capture" and run the normal video element monitoring path. The state reset from
|
|
340
|
+
// applyTabReplacementSuccess (called by executeTabReplacement) already initialized all capture-mode monitor variables.
|
|
341
|
+
}
|
|
342
|
+
else if (outcome.outcome === "terminated") {
|
|
343
|
+
// Circuit breaker tripped during tab replacement. Stream is being terminated.
|
|
344
|
+
LOG.warn("Capture fallback failed for %s: circuit breaker tripped.", entry.info.storeKey);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Tab replacement failed but stream wasn't terminated. The circuit breaker will handle it on the next failure.
|
|
348
|
+
LOG.warn("Capture fallback failed for %s: tab replacement unsuccessful.", entry.info.storeKey);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
110
351
|
// Helper to mark a discontinuity in the HLS playlist after recovery events that disrupt the video source. The segmenter flushes its current fragment buffer and sets
|
|
111
352
|
// a pending discontinuity flag so the next segment boundary includes an #EXT-X-DISCONTINUITY tag. This tells HLS clients to flush their decoder state.
|
|
112
353
|
const markStreamDiscontinuity = () => {
|
|
@@ -143,6 +384,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
143
384
|
* Emits a status update for this stream.
|
|
144
385
|
*/
|
|
145
386
|
function emitStatusUpdate() {
|
|
387
|
+
// Do not emit after the monitor has been stopped. An in-flight async tick can resume from an await after terminateStream() has called stopMonitor() and
|
|
388
|
+
// emitStreamRemoved(). Without this guard, the emitStreamHealthChanged() call below would re-add the dead stream to the streamStatuses Map, creating a zombie
|
|
389
|
+
// entry that persists in SSE snapshots indefinitely.
|
|
390
|
+
if (intervalCleared) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
146
393
|
const now = Date.now();
|
|
147
394
|
// Get current memory usage from the stream's HLS segment buffers.
|
|
148
395
|
const entry = getStream(streamInfo.numericStreamId);
|
|
@@ -173,6 +420,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
173
420
|
recoveryAttempts: totalRecoveryAttempts,
|
|
174
421
|
showName: getShowName(streamInfo.numericStreamId),
|
|
175
422
|
startTime: streamInfo.startTime.toISOString(),
|
|
423
|
+
streamingMode: entry?.streamingMode ?? "capture",
|
|
176
424
|
url
|
|
177
425
|
};
|
|
178
426
|
emitStreamHealthChanged(status);
|
|
@@ -209,6 +457,27 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
209
457
|
videoNotFoundCount = 0;
|
|
210
458
|
stallCount = 0;
|
|
211
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* Resets resolution monitoring state. Called when resolution reaches expected levels or after any recovery action that restarts ABR negotiation.
|
|
462
|
+
*/
|
|
463
|
+
function resetResolutionState() {
|
|
464
|
+
resolutionGraceEnd = Date.now() + RESOLUTION_GRACE_PERIOD;
|
|
465
|
+
resolutionRecoveryAttempt = 0;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Checks the page reload rate limit. Prunes expired timestamps, checks if the limit has been reached, and records the current timestamp if allowed. Callers are
|
|
469
|
+
* responsible for logging and handling the rate-limited case — consequences differ by context (circuit break, deferral, fallback to L2).
|
|
470
|
+
* @returns True if a page reload is allowed and the timestamp has been recorded, false if the rate limit has been reached.
|
|
471
|
+
*/
|
|
472
|
+
function isPageReloadAllowed() {
|
|
473
|
+
const reloadWindow = Date.now() - CONFIG.playback.pageReloadWindow;
|
|
474
|
+
pageReloadTimestamps = pageReloadTimestamps.filter((ts) => ts > reloadWindow);
|
|
475
|
+
if (pageReloadTimestamps.length >= CONFIG.playback.maxPageReloads) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
pageReloadTimestamps.push(Date.now());
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
212
481
|
/**
|
|
213
482
|
* Resets escalation level and related flags. Called after successful recovery to allow the stream to start from level 0 on future issues.
|
|
214
483
|
*/
|
|
@@ -256,6 +525,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
256
525
|
resetRecoveryCounters();
|
|
257
526
|
resetEscalationState();
|
|
258
527
|
resetSegmentMonitoringState();
|
|
528
|
+
resetResolutionState();
|
|
259
529
|
setRecoveryGracePeriod(3);
|
|
260
530
|
resetCircuitBreaker(circuitBreaker);
|
|
261
531
|
}
|
|
@@ -423,6 +693,13 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
423
693
|
emitStatusUpdate();
|
|
424
694
|
return;
|
|
425
695
|
}
|
|
696
|
+
// For native streaming mode, monitor segment delivery health instead of video element state. We check the registry on each tick rather than caching the mode at
|
|
697
|
+
// startup because the streaming mode is set after the monitor starts (native streaming is attempted after setupStream returns).
|
|
698
|
+
const nativeEntry = getStream(streamInfo.numericStreamId);
|
|
699
|
+
if (nativeEntry?.streamingMode === "native") {
|
|
700
|
+
checkNativeStreamHealth(nativeEntry);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
426
703
|
// Re-establish stream context for this interval tick. AsyncLocalStorage context is lost when entering setInterval callbacks.
|
|
427
704
|
runWithStreamContext(streamContext, async () => {
|
|
428
705
|
try {
|
|
@@ -556,15 +833,12 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
556
833
|
recoveryInProgress = true;
|
|
557
834
|
recordRecoveryAttempt(metrics, RECOVERY_METHODS.pageNavigation);
|
|
558
835
|
// Check page reload limit before attempting recovery.
|
|
559
|
-
|
|
560
|
-
pageReloadTimestamps = pageReloadTimestamps.filter((ts) => ts > reloadWindow);
|
|
561
|
-
if (pageReloadTimestamps.length >= CONFIG.playback.maxPageReloads) {
|
|
836
|
+
if (!isPageReloadAllowed()) {
|
|
562
837
|
LOG.error("Page navigation rate limit reached (%s in %s minutes) — cannot recover without video element.", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
563
838
|
clearInterval(interval);
|
|
564
839
|
onCircuitBreak();
|
|
565
840
|
return;
|
|
566
841
|
}
|
|
567
|
-
pageReloadTimestamps.push(now);
|
|
568
842
|
// Use the unified recovery function with validation.
|
|
569
843
|
const recoveryResult = await performPageNavigationRecovery();
|
|
570
844
|
// Page navigation disrupted the video stream. Mark a discontinuity regardless of navigation success so HLS clients resynchronize their decoders.
|
|
@@ -583,6 +857,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
583
857
|
resetRecoveryCounters();
|
|
584
858
|
resetEscalationState();
|
|
585
859
|
resetSegmentMonitoringState();
|
|
860
|
+
resetResolutionState();
|
|
586
861
|
}
|
|
587
862
|
else {
|
|
588
863
|
consecutiveNavigationFailures++;
|
|
@@ -661,8 +936,14 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
661
936
|
if (segmentSize < TINY_SEGMENT_THRESHOLD) {
|
|
662
937
|
consecutiveTinySegments++;
|
|
663
938
|
wasInTinySegmentState = true;
|
|
664
|
-
|
|
665
|
-
|
|
939
|
+
// Check track composition to determine the effective threshold. Dead capture pipelines produce audio-only segments (hasVideo=false) and always use the
|
|
940
|
+
// default count for fast detection. Segments with video trafs present (hasVideo=true or null) use the provider-specific threshold, which may be higher
|
|
941
|
+
// for providers with extended static content (e.g., Xfinity commercial placeholders lasting several minutes).
|
|
942
|
+
const hasVideo = getLastSegmentHasVideo(sizeCheckEntry);
|
|
943
|
+
const effectiveThreshold = (hasVideo === false) ? TINY_SEGMENT_COUNT_TRIGGER : providerTinySegmentThreshold;
|
|
944
|
+
LOG.debug("recovery:tracks", "Below-threshold segment: %d bytes, hasVideo=%s, consecutive=%d, threshold=%d.", segmentSize, String(hasVideo), consecutiveTinySegments, effectiveThreshold);
|
|
945
|
+
if (consecutiveTinySegments >= effectiveThreshold) {
|
|
946
|
+
LOG.warn("Detected %d consecutive tiny segments (%d bytes, hasVideo=%s) — capture pipeline is not responding.", consecutiveTinySegments, segmentSize, String(hasVideo));
|
|
666
947
|
// Trigger tab replacement if available, otherwise let circuit breaker handle it via segmentProductionStalled. Return unconditionally after tab
|
|
667
948
|
// replacement (matching stalled-capture and unresponsive-tab triggers) to avoid falling through the rest of the tick with stale pre-replacement state.
|
|
668
949
|
if (onTabReplacement && !recoveryInProgress) {
|
|
@@ -679,7 +960,8 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
679
960
|
// Valid segment size. Check for spontaneous recovery from tiny segment state. We don't mark discontinuity here - only tab replacement marks discontinuity.
|
|
680
961
|
// Self-healing may be transient and not require decoder reset.
|
|
681
962
|
if (wasInTinySegmentState) {
|
|
682
|
-
|
|
963
|
+
const hasVideo = getLastSegmentHasVideo(sizeCheckEntry);
|
|
964
|
+
LOG.debug("recovery:segments", "Segment production self-healed (%d bytes, hasVideo=%s).", segmentSize, String(hasVideo));
|
|
683
965
|
}
|
|
684
966
|
// Reset tiny segment tracking.
|
|
685
967
|
consecutiveTinySegments = 0;
|
|
@@ -743,6 +1025,78 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
743
1025
|
fullscreenReapplyCount = 0;
|
|
744
1026
|
}
|
|
745
1027
|
}
|
|
1028
|
+
/* Resolution degradation detection. When a provider's ABR delivers content at a resolution significantly below the configured viewport, the captured frame
|
|
1029
|
+
* shows a small video in the top-left corner of a larger viewport. This is distinct from fullscreen issues (which the fullscreen reinforcement above handles) —
|
|
1030
|
+
* the video CSS fills the viewport, but the intrinsic media resolution is low. We monitor video.videoWidth/videoHeight and compare against the effective viewport
|
|
1031
|
+
* dimensions. The check is gated by healthy playback, outside recovery grace, and non-zero intrinsic dimensions. The 30-second grace period after stream start
|
|
1032
|
+
* and each recovery allows ABR time to ramp up quality. After two unsuccessful recovery attempts (page reload + tab replacement), the system accepts the
|
|
1033
|
+
* resolution to avoid infinite loops on legitimately low-resolution content.
|
|
1034
|
+
*/
|
|
1035
|
+
if (isProgressing && !state.paused && !state.error && !state.ended && !withinRecoveryGrace && (state.videoWidth > 0) && (state.videoHeight > 0)) {
|
|
1036
|
+
const viewport = getEffectiveViewport(CONFIG);
|
|
1037
|
+
const widthRatio = state.videoWidth / viewport.width;
|
|
1038
|
+
const heightRatio = state.videoHeight / viewport.height;
|
|
1039
|
+
const isDegraded = (widthRatio < RESOLUTION_RATIO_THRESHOLD) || (heightRatio < RESOLUTION_RATIO_THRESHOLD);
|
|
1040
|
+
if (isDegraded) {
|
|
1041
|
+
LOG.debug("recovery:resolution", "Video resolution: %s\u00d7%s (viewport: %s\u00d7%s, ratio: %s%%\u00d7%s%%).", String(state.videoWidth), String(state.videoHeight), String(viewport.width), String(viewport.height), String(Math.round(widthRatio * 100)), String(Math.round(heightRatio * 100)));
|
|
1042
|
+
}
|
|
1043
|
+
// Escalation step 1: page reload. Forces the provider's ABR to restart quality negotiation.
|
|
1044
|
+
if (isDegraded && (now >= resolutionGraceEnd) && (resolutionRecoveryAttempt === 0)) {
|
|
1045
|
+
LOG.warn("Video resolution degraded (%s\u00d7%s in %s\u00d7%s viewport). Recovering via %s.", String(state.videoWidth), String(state.videoHeight), String(viewport.width), String(viewport.height), RECOVERY_METHODS.pageNavigation);
|
|
1046
|
+
recoveryInProgress = true;
|
|
1047
|
+
// Check page reload rate limit before attempting.
|
|
1048
|
+
if (!isPageReloadAllowed()) {
|
|
1049
|
+
LOG.warn("Resolution recovery deferred — page navigation rate limit reached (%s in %s minutes).", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
1050
|
+
// Defer by pushing grace end forward to avoid re-triggering every 2 seconds.
|
|
1051
|
+
resolutionGraceEnd = now + RESOLUTION_GRACE_PERIOD;
|
|
1052
|
+
recoveryInProgress = false;
|
|
1053
|
+
emitStatusUpdate();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
pendingReMinimize = true;
|
|
1057
|
+
const recoveryResult = await performPageNavigationRecovery();
|
|
1058
|
+
markStreamDiscontinuity();
|
|
1059
|
+
resolutionRecoveryAttempt = 1;
|
|
1060
|
+
resolutionGraceEnd = now + RESOLUTION_GRACE_PERIOD;
|
|
1061
|
+
if (recoveryResult.success && recoveryResult.newContext) {
|
|
1062
|
+
currentContext = recoveryResult.newContext;
|
|
1063
|
+
lastPageNavigationTime = Date.now();
|
|
1064
|
+
LOG.info("Resolution recovery: page reload completed. Waiting for ABR ramp-up.");
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
LOG.warn("Resolution recovery: page reload unsuccessful.");
|
|
1068
|
+
}
|
|
1069
|
+
recoveryInProgress = false;
|
|
1070
|
+
emitStatusUpdate();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
// Escalation step 2: tab replacement. Creates a fresh page with new capture pipeline and network connections.
|
|
1074
|
+
if (isDegraded && (now >= resolutionGraceEnd) && (resolutionRecoveryAttempt === 1)) {
|
|
1075
|
+
if (onTabReplacement) {
|
|
1076
|
+
LOG.warn("Video resolution still degraded (%s\u00d7%s) after page reload. Recovering via %s.", String(state.videoWidth), String(state.videoHeight), RECOVERY_METHODS.tabReplacement);
|
|
1077
|
+
const outcome = await executeTabReplacement("resolution degraded");
|
|
1078
|
+
resolutionRecoveryAttempt = 2;
|
|
1079
|
+
resolutionGraceEnd = now + RESOLUTION_GRACE_PERIOD;
|
|
1080
|
+
if (outcome.outcome === "terminated") {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
// Tab replacement not available — skip directly to acceptance.
|
|
1086
|
+
resolutionRecoveryAttempt = 2;
|
|
1087
|
+
}
|
|
1088
|
+
// Acceptance: resolution still degraded after both recovery attempts. Log once and stop retrying.
|
|
1089
|
+
if (isDegraded && (now >= resolutionGraceEnd) && (resolutionRecoveryAttempt === 2)) {
|
|
1090
|
+
LOG.warn("Video resolution remains degraded (%s\u00d7%s in %s\u00d7%s viewport) after recovery attempts. Accepting current resolution.", String(state.videoWidth), String(state.videoHeight), String(viewport.width), String(viewport.height));
|
|
1091
|
+
resolutionRecoveryAttempt = 3;
|
|
1092
|
+
}
|
|
1093
|
+
// Resolution is good: clear tracking state so future degradation starts fresh.
|
|
1094
|
+
if (!isDegraded && (resolutionRecoveryAttempt > 0)) {
|
|
1095
|
+
LOG.info("Video resolution restored (%s\u00d7%s).", String(state.videoWidth), String(state.videoHeight));
|
|
1096
|
+
resolutionRecoveryAttempt = 0;
|
|
1097
|
+
resolutionGraceEnd = 0;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
746
1100
|
/* Stall counter management. We increment stallCount when the video is not progressing and not within buffering grace. We reset to 0 when progression resumes.
|
|
747
1101
|
* This hysteresis prevents reacting to single-frame hiccups.
|
|
748
1102
|
*/
|
|
@@ -811,9 +1165,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
811
1165
|
recoveryInProgress = true;
|
|
812
1166
|
// Check page reload rate limit before attempting. Proactive reload is best-effort maintenance — if the reload budget is exhausted from recent error
|
|
813
1167
|
// recoveries, we gracefully yield. If the site eventually cuts the stream, normal error recovery handles it.
|
|
814
|
-
|
|
815
|
-
pageReloadTimestamps = pageReloadTimestamps.filter((ts) => ts > reloadWindow);
|
|
816
|
-
if (pageReloadTimestamps.length >= CONFIG.playback.maxPageReloads) {
|
|
1168
|
+
if (!isPageReloadAllowed()) {
|
|
817
1169
|
LOG.warn("Proactive reload deferred — page navigation rate limit reached (%s in %s minutes).", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
818
1170
|
// Set a grace period to prevent this deferral from re-triggering every 2 seconds while the rate limit remains in effect. The 10-second L3 grace
|
|
819
1171
|
// period spaces out re-checks, and the rate-limit window (default 15 minutes) will eventually expire old timestamps to allow the proactive reload. We
|
|
@@ -824,7 +1176,6 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
824
1176
|
emitStatusUpdate();
|
|
825
1177
|
return;
|
|
826
1178
|
}
|
|
827
|
-
pageReloadTimestamps.push(now);
|
|
828
1179
|
const recoveryResult = await performPageNavigationRecovery();
|
|
829
1180
|
// Page navigation disrupted the video stream. Mark a discontinuity so HLS clients resynchronize their decoders.
|
|
830
1181
|
markStreamDiscontinuity();
|
|
@@ -835,6 +1186,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
835
1186
|
LOG.info("Proactive reload completed successfully.");
|
|
836
1187
|
resetRecoveryCounters();
|
|
837
1188
|
resetSegmentMonitoringState();
|
|
1189
|
+
resetResolutionState();
|
|
838
1190
|
}
|
|
839
1191
|
else {
|
|
840
1192
|
LOG.warn("Proactive reload unsuccessful. Will retry after recovery grace period.");
|
|
@@ -936,6 +1288,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
936
1288
|
}
|
|
937
1289
|
// Set grace period to give this recovery level time to take effect before the next check.
|
|
938
1290
|
recoveryGraceUntil = now + recoveryGracePeriods[escalationLevel];
|
|
1291
|
+
resetResolutionState();
|
|
939
1292
|
}
|
|
940
1293
|
else {
|
|
941
1294
|
/* Level 3: Page navigation recovery. This is the most aggressive recovery - we navigate to the URL again and reinitialize everything.
|
|
@@ -950,20 +1303,14 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
950
1303
|
sourceReloadAttempted = false;
|
|
951
1304
|
}
|
|
952
1305
|
else {
|
|
953
|
-
// Check page reload limit to prevent excessive navigations.
|
|
954
|
-
|
|
955
|
-
// Prune old timestamps outside the window.
|
|
956
|
-
pageReloadTimestamps = pageReloadTimestamps.filter((ts) => {
|
|
957
|
-
return ts > reloadWindow;
|
|
958
|
-
});
|
|
959
|
-
if (pageReloadTimestamps.length >= CONFIG.playback.maxPageReloads) {
|
|
1306
|
+
// Check page reload limit to prevent excessive navigations.
|
|
1307
|
+
if (!isPageReloadAllowed()) {
|
|
960
1308
|
LOG.warn("Page navigation rate limit reached (%s in %s minutes) — falling back to source reload.", CONFIG.playback.maxPageReloads, Math.round(CONFIG.playback.pageReloadWindow / 60000));
|
|
961
1309
|
escalationLevel = 2;
|
|
962
1310
|
// Reset source reload tracking so the fallback L2 gets a fair chance.
|
|
963
1311
|
sourceReloadAttempted = false;
|
|
964
1312
|
}
|
|
965
1313
|
else {
|
|
966
|
-
pageReloadTimestamps.push(now);
|
|
967
1314
|
// Use the unified recovery function with validation.
|
|
968
1315
|
const recoveryResult = await performPageNavigationRecovery();
|
|
969
1316
|
// Page navigation disrupted the video stream. Mark a discontinuity regardless of navigation success so HLS clients resynchronize their decoders.
|
|
@@ -982,6 +1329,7 @@ export function monitorPlaybackHealth(page, context, profile, url, streamId, str
|
|
|
982
1329
|
resetRecoveryCounters();
|
|
983
1330
|
resetEscalationState();
|
|
984
1331
|
resetSegmentMonitoringState();
|
|
1332
|
+
resetResolutionState();
|
|
985
1333
|
}
|
|
986
1334
|
else {
|
|
987
1335
|
consecutiveNavigationFailures++;
|