diff --git a/web/player.html b/web/player.html
index 2826249..87d5673 100644
--- a/web/player.html
+++ b/web/player.html
@@ -315,39 +315,43 @@
return false;
}
- // Optimized HLS.js configuration for stable live streaming
+ // Optimized HLS.js configuration for stable streaming (prevents micro-cutouts)
const hls = new Hls({
- // Increase buffer for smoother playback
- maxBufferLength: 30, // Max 30 seconds of buffer
- maxMaxBufferLength: 60, // Allow up to 60 seconds in stressed conditions
- maxBufferSize: 60 * 1000000, // 60MB max buffer size
- maxBufferHole: 0.5, // Tolerate 0.5s gaps in buffer
+ // LARGER buffers to prevent stalls
+ maxBufferLength: 60, // Max 60 seconds of buffer (doubled)
+ maxMaxBufferLength: 120, // Allow up to 120 seconds in stressed conditions
+ maxBufferSize: 120 * 1000000, // 120MB max buffer size
+ maxBufferHole: 1.0, // Tolerate 1s gaps in buffer (more forgiving)
- // Low latency settings (balanced)
- liveSyncDurationCount: 3, // Sync to 3 segments behind live
- liveMaxLatencyDurationCount: 6, // Max 6 segments behind before seeking
+ // Live settings - more tolerant
+ liveSyncDurationCount: 4, // Sync to 4 segments behind live (more buffer)
+ liveMaxLatencyDurationCount: 10, // Max 10 segments behind before seeking
liveDurationInfinity: true, // Treat live streams as infinite
+ liveBackBufferLength: 30, // Keep 30s of back buffer
- // Faster recovery from errors
- manifestLoadingTimeOut: 15000, // 15s timeout for manifest
- manifestLoadingMaxRetry: 4, // Retry manifest 4 times
- manifestLoadingRetryDelay: 1000, // 1s between retries
- levelLoadingTimeOut: 15000, // 15s for level playlist
- levelLoadingMaxRetry: 4,
- levelLoadingRetryDelay: 1000,
- fragLoadingTimeOut: 20000, // 20s for fragments
- fragLoadingMaxRetry: 6, // Retry fragments 6 times
- fragLoadingRetryDelay: 1000,
+ // AGGRESSIVE retry settings for network issues
+ manifestLoadingTimeOut: 20000, // 20s timeout for manifest
+ manifestLoadingMaxRetry: 8, // Retry manifest 8 times
+ manifestLoadingRetryDelay: 500, // 500ms between retries (faster)
+ levelLoadingTimeOut: 20000, // 20s for level playlist
+ levelLoadingMaxRetry: 8,
+ levelLoadingRetryDelay: 500,
+ fragLoadingTimeOut: 30000, // 30s for fragments (longer)
+ fragLoadingMaxRetry: 10, // Retry fragments 10 times
+ fragLoadingRetryDelay: 500, // 500ms between retries
// Better quality selection
startLevel: -1, // Auto-select start level
abrEwmaDefaultEstimate: 500000, // Start assuming 500kbps
- abrBandWidthFactor: 0.95, // Use 95% of measured bandwidth
- abrBandWidthUpFactor: 0.7, // Be cautious when upgrading quality
+ abrBandWidthFactor: 0.9, // Use 90% of measured bandwidth (more conservative)
+ abrBandWidthUpFactor: 0.6, // Be more cautious when upgrading quality
// Enable worker for better performance
enableWorker: true,
+ // Lowlatency mode disabled for stability
+ lowLatencyMode: false,
+
// Don't add extra headers
xhrSetup: function (xhr, url) { }
});
@@ -366,25 +370,38 @@
});
});
- // Better error recovery
+ // Better error recovery (including non-fatal bufferStalledError)
hls.on(Hls.Events.ERROR, (event, data) => {
log('HLS Error: ' + data.type + ' - ' + data.details);
- if (data.fatal) {
- switch (data.type) {
- case Hls.ErrorTypes.NETWORK_ERROR:
- log('Network error, attempting recovery...');
- hls.startLoad(); // Try to recover network errors
- break;
- case Hls.ErrorTypes.MEDIA_ERROR:
- log('Media error, attempting recovery...');
- hls.recoverMediaError(); // Try to recover media errors
- break;
- default:
- showError('HLS Error: ' + data.details);
- hls.destroy();
- break;
+ // Handle non-fatal buffer errors (bufferStalledError)
+ if (!data.fatal) {
+ if (data.details === 'bufferStalledError') {
+ log('Buffer stalled, attempting recovery...');
+ // Nudge playback if stuck
+ if (video.paused) {
+ video.play().catch(e => log('Resume failed: ' + e));
+ }
+ // Force reload from current position
+ hls.startLoad(video.currentTime);
}
+ return; // Non-fatal errors can often be ignored
+ }
+
+ // Handle fatal errors
+ switch (data.type) {
+ case Hls.ErrorTypes.NETWORK_ERROR:
+ log('Network error, attempting recovery...');
+ hls.startLoad(); // Try to recover network errors
+ break;
+ case Hls.ErrorTypes.MEDIA_ERROR:
+ log('Media error, attempting recovery...');
+ hls.recoverMediaError(); // Try to recover media errors
+ break;
+ default:
+ showError('HLS Error: ' + data.details);
+ hls.destroy();
+ break;
}
});
diff --git a/web/player_lite.html b/web/player_lite.html
index fda7f50..a05f9de 100644
--- a/web/player_lite.html
+++ b/web/player_lite.html
@@ -268,16 +268,80 @@
lazyLoadRecoverDuration: 0,
};
- const player = mpegts.createPlayer({
- type: 'mpegts',
- isLive: isLive,
- url: url,
- hasAudio: true,
- hasVideo: true,
- }, playerConfig);
+ let player = null;
+ let reconnectAttempts = 0;
+ const maxReconnectAttempts = 10;
+ let isDestroyed = false;
- player.attachMediaElement(video);
- player.load();
+ function createPlayer() {
+ if (isDestroyed) return;
+
+ player = mpegts.createPlayer({
+ type: 'mpegts',
+ isLive: isLive,
+ url: url,
+ hasAudio: true,
+ hasVideo: true,
+ }, playerConfig);
+
+ player.attachMediaElement(video);
+ player.load();
+
+ // Handle stream end (auto-reconnect for live streams)
+ player.on(mpegts.Events.LOADING_COMPLETE, () => {
+ if (isLive && !isDestroyed && reconnectAttempts < maxReconnectAttempts) {
+ log('Stream ended, auto-reconnecting... (' + (reconnectAttempts + 1) + '/' + maxReconnectAttempts + ')');
+ reconnectAttempts++;
+
+ // Destroy and recreate player after delay
+ setTimeout(() => {
+ if (!isDestroyed) {
+ try {
+ player.unload();
+ player.detachMediaElement();
+ player.destroy();
+ } catch (e) { }
+
+ showLoading('Reconnecting...');
+ setTimeout(createPlayer, 500);
+ }
+ }, 1000);
+ }
+ });
+
+ // Reset reconnect counter on successful playback
+ video.addEventListener('playing', () => {
+ reconnectAttempts = 0;
+ hideLoading();
+ });
+
+ player.on(mpegts.Events.ERROR, (errType, errDetail) => {
+ log('mpegts error: ' + errType + ' - ' + errDetail);
+
+ // Try to reconnect on network errors for live streams
+ if (isLive && !isDestroyed && reconnectAttempts < maxReconnectAttempts) {
+ log('Error occurred, attempting reconnect...');
+ reconnectAttempts++;
+
+ setTimeout(() => {
+ if (!isDestroyed) {
+ try {
+ player.unload();
+ player.detachMediaElement();
+ player.destroy();
+ } catch (e) { }
+
+ showLoading('Reconnecting...');
+ setTimeout(createPlayer, 1000);
+ }
+ }, 500);
+ } else {
+ showError('Stream error: ' + errDetail);
+ }
+ });
+ }
+
+ createPlayer();
video.addEventListener('canplay', () => {
log('MPEG-TS stream ready');
@@ -286,13 +350,22 @@
});
video.addEventListener('error', () => {
- showError('Video error: ' + (video.error?.message || 'Unknown error'));
+ if (!isLive || reconnectAttempts >= maxReconnectAttempts) {
+ showError('Video error: ' + (video.error?.message || 'Unknown error'));
+ }
});
- player.on(mpegts.Events.ERROR, (errType, errDetail) => {
- log('mpegts error: ' + errType + ' - ' + errDetail);
- showError('Stream error: ' + errDetail);
- });
+ // Cleanup function
+ window.cleanupMpegtsPlayer = () => {
+ isDestroyed = true;
+ if (player) {
+ try {
+ player.unload();
+ player.detachMediaElement();
+ player.destroy();
+ } catch (e) { }
+ }
+ };
return true;
}