diff --git a/web/player.html b/web/player.html
index efdddc7..9931d3a 100644
--- a/web/player.html
+++ b/web/player.html
@@ -12,6 +12,7 @@
margin: 0;
padding: 0;
box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
}
html,
@@ -23,6 +24,7 @@
display: flex;
justify-content: center;
align-items: center;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#player-container {
@@ -46,6 +48,7 @@
color: #fff;
text-align: center;
font-family: Arial, sans-serif;
+ z-index: 30;
}
#error-icon {
@@ -63,15 +66,16 @@
color: #fff;
text-align: center;
font-family: Arial, sans-serif;
+ z-index: 20;
}
.spinner {
width: 48px;
height: 48px;
- border: 4px solid #fff;
- border-top-color: transparent;
+ border: 4px solid rgba(255, 255, 255, 0.2);
+ border-top-color: #fff;
border-radius: 50%;
- animation: spin 1s linear infinite;
+ animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
margin: 0 auto 16px;
}
@@ -80,20 +84,59 @@
transform: rotate(360deg);
}
}
+
+ /* Safari Autoplay Hint Overlay */
+ #play-overlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 90px;
+ height: 90px;
+ background: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: none;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ z-index: 25;
+ transition: transform 0.2s;
+ }
+
+ #play-overlay:active {
+ transform: translate(-50%, -50%) scale(0.9);
+ }
+
+ #play-overlay svg {
+ width: 44px;
+ height: 44px;
+ fill: white;
+ margin-left: 5px;
+ }
-
-
+
+
+
+
+
-
Loading stream...
+
Chargement du flux...
⚠
-
Failed to load stream
+
Erreur de chargement
@@ -103,347 +146,176 @@
const loadingText = document.getElementById('loading-text');
const error = document.getElementById('error');
const errorMessage = document.getElementById('error-message');
+ const playOverlay = document.getElementById('play-overlay');
- // Get parameters from query string
const urlParams = new URLSearchParams(window.location.search);
let streamUrl = urlParams.get('url');
const startTime = parseFloat(urlParams.get('t') || '0');
- const streamType = urlParams.get('type') || ''; // 'live' or 'vod'
- const injectedDuration = parseFloat(urlParams.get('duration') || '0'); // Fix for infinite HLS duration
+ const streamType = urlParams.get('type') || '';
+ const injectedDuration = parseFloat(urlParams.get('duration') || '0');
- function log(msg) {
- console.log('[Player] ' + msg);
- }
+ function log(msg) { console.log('[Player] ' + msg); }
function showLoading(msg) {
loading.style.display = 'block';
- loadingText.textContent = msg || 'Loading stream...';
+ loadingText.textContent = msg || 'Chargement du flux...';
error.style.display = 'none';
}
- function hideLoading() {
- loading.style.display = 'none';
- }
+ function hideLoading() { loading.style.display = 'none'; }
function showError(message) {
hideLoading();
error.style.display = 'block';
- errorMessage.textContent = message || 'Failed to load stream';
+ errorMessage.textContent = message || 'Erreur de chargement';
+ playOverlay.style.display = 'none';
}
- // Seek to startTime when video is ready
+ playOverlay.addEventListener('click', () => {
+ log('Manual play triggered');
+ video.play();
+ playOverlay.style.display = 'none';
+ });
+
video.addEventListener('loadedmetadata', () => {
if (startTime > 0 && startTime < video.duration) {
video.currentTime = startTime;
- log('Resuming from ' + Math.floor(startTime / 60) + 'm ' + Math.floor(startTime % 60) + 's');
}
});
- // Report position to parent for resume feature
let lastReportedTime = 0;
function reportPosition() {
if (video.currentTime > 0 && !video.paused && video.readyState > 2) {
- if (Math.abs(video.currentTime - lastReportedTime) >= 1) { // Report more frequently for smoother UI
+ if (Math.abs(video.currentTime - lastReportedTime) >= 1) {
lastReportedTime = video.currentTime;
-
let duration = video.duration;
- // Priority: 1) HLS dynamic duration, 2) video.duration, 3) injectedDuration
const hlsDuration = window.currentHlsDuration || 0;
if (!isFinite(duration) || isNaN(duration) || duration < 1) {
duration = hlsDuration > 0 ? hlsDuration : (injectedDuration > 0 ? injectedDuration : 0);
} else if (hlsDuration > duration) {
- duration = hlsDuration; // Use HLS duration if larger (growing playlist)
+ duration = hlsDuration;
} else if (injectedDuration > 0 && duration < injectedDuration * 0.9) {
- duration = injectedDuration; // Fallback to injected if significantly larger
+ duration = injectedDuration;
}
-
- window.parent.postMessage({
- type: 'playback_position',
- currentTime: video.currentTime,
- duration: duration
- }, '*');
+ window.parent.postMessage({ type: 'playback_position', currentTime: video.currentTime, duration: duration }, '*');
}
}
}
setInterval(reportPosition, 5000);
- // Report on pause and end
video.addEventListener('pause', () => {
- let duration = video.duration;
- if (!isFinite(duration) || isNaN(duration)) {
- duration = injectedDuration > 0 ? injectedDuration : 0;
- }
-
- window.parent.postMessage({
- type: 'playback_position',
- currentTime: video.currentTime,
- duration: duration
- }, '*');
-
- // Also report status
- window.parent.postMessage({
- type: 'playback_status',
- status: 'paused'
- }, '*');
- });
-
- video.addEventListener('ended', () => {
- let duration = video.duration;
- if (!isFinite(duration) || isNaN(duration)) {
- duration = injectedDuration > 0 ? injectedDuration : 0;
- }
-
- window.parent.postMessage({
- type: 'playback_ended',
- duration: duration
- }, '*');
+ window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
+ if (loading.style.display === 'none') playOverlay.style.display = 'flex';
});
video.addEventListener('playing', () => {
- window.parent.postMessage({
- type: 'playback_status',
- status: 'playing'
- }, '*');
+ window.parent.postMessage({ type: 'playback_status', status: 'playing' }, '*');
+ hideLoading();
+ playOverlay.style.display = 'none';
});
- // Report user activity (mouse movement or touch) to parent for auto-hide controls
- let lastActivityTime = 0;
- function reportActivity() {
- const now = Date.now();
- if (now - lastActivityTime > 100) { // Throttle to 100ms
- lastActivityTime = now;
- window.parent.postMessage({
- type: 'user_activity'
- }, '*');
- }
- }
+ video.addEventListener('ended', () => {
+ window.parent.postMessage({ type: 'playback_ended', duration: isFinite(video.duration) ? video.duration : injectedDuration }, '*');
+ });
+
+ function reportActivity() { window.parent.postMessage({ type: 'user_activity' }, '*'); }
document.addEventListener('mousemove', reportActivity);
document.addEventListener('touchstart', reportActivity);
- document.addEventListener('touchmove', reportActivity);
document.addEventListener('click', reportActivity);
- document.addEventListener('touchend', reportActivity);
- // Handle commands from Flutter (Play, Pause, Seek, Volume)
window.addEventListener('message', (event) => {
const data = event.data;
if (!data) return;
-
switch (data.type) {
- case 'play': video.play().catch(e => log('Play fail: ' + e)); break;
+ case 'play': video.play().catch(() => playOverlay.style.display = 'flex'); break;
case 'pause': video.pause(); break;
- case 'seek':
- if (Number.isFinite(data.value)) video.currentTime = data.value;
- break;
- case 'set_volume':
- video.volume = Math.max(0, Math.min(1, data.value));
+ case 'seek': if (Number.isFinite(data.value)) video.currentTime = data.value; break;
+ case 'set_volume': video.volume = Math.max(0, Math.min(1, data.value)); video.muted = video.volume === 0; break;
+ case 'request_fullscreen':
+ const el = document.documentElement;
+ const rf = el.requestFullscreen || el.webkitRequestFullscreen || video.webkitEnterFullscreen;
+ if (rf) rf.call(el === document.documentElement ? el : video).catch(e => log('FS Error: ' + e));
break;
}
});
async function playWithHLS(url) {
log('Trying HLS.js with: ' + url);
- showLoading('Loading HLS stream...');
+ showLoading('Chargement HLS...');
if (!Hls.isSupported()) {
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
- video.addEventListener('canplay', () => {
- hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
- });
+ video.load();
+ video.play().catch(() => playOverlay.style.display = 'flex');
return true;
}
return false;
}
- const hls = new Hls({
- maxBufferLength: 30,
- maxMaxBufferLength: 60,
- enableWorker: true
- });
-
+ const hls = new Hls({ maxBufferLength: 45, enableWorker: true });
window.hlsInstance = hls;
hls.loadSource(url);
hls.attachMedia(video);
-
hls.on(Hls.Events.MANIFEST_PARSED, () => {
- log('HLS manifest parsed');
hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
+ video.play().catch(() => playOverlay.style.display = 'flex');
});
-
- // Track growing duration as more segments are added (for EVENT playlists)
- hls.on(Hls.Events.LEVEL_LOADED, (event, data) => {
- const levelDetails = data.details;
- if (levelDetails && levelDetails.totalduration) {
- const currentLevelDuration = levelDetails.totalduration;
- log('Level loaded, current duration: ' + currentLevelDuration);
-
- // Update injectedDuration if HLS gives us a better value
- // This allows the progress bar to grow as transcoding progresses
- if (currentLevelDuration > injectedDuration) {
- window.currentHlsDuration = currentLevelDuration;
- }
+ hls.on(Hls.Events.LEVEL_LOADED, (e, d) => {
+ if (d.details && d.details.totalduration > injectedDuration) window.currentHlsDuration = d.details.totalduration;
+ });
+ hls.on(Hls.Events.ERROR, (e, d) => {
+ if (d.fatal) {
+ if (d.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
+ else if (d.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
+ else showError('HLS Error: ' + d.details);
}
});
-
- hls.on(Hls.Events.ERROR, (event, data) => {
- log('HLS Error: ' + data.type + ' - ' + data.details);
- if (data.fatal) {
- if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
- hls.startLoad();
- } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
- hls.recoverMediaError();
- } else {
- showError('HLS Error: ' + data.details);
- hls.destroy();
- }
- }
- });
-
return true;
}
async function playWithMpegTS(url, isLive = true) {
- log('Trying mpegts.js with: ' + url + ' (isLive: ' + isLive + ')');
- showLoading('Loading MPEG-TS stream...');
-
- if (!mpegts.isSupported()) {
- return false;
- }
-
- const playerConfig = {
- enableWorker: true,
- enableStashBuffer: true,
- stashInitialSize: 1024 * 1024,
- autoCleanupSourceBuffer: true,
- };
-
- let player = mpegts.createPlayer({
- type: 'mpegts',
- isLive: isLive,
- url: url,
- hasAudio: true,
- hasVideo: true,
- }, playerConfig);
-
+ log('Trying mpegts.js with: ' + url);
+ showLoading('Chargement MPEG-TS...');
+ if (!mpegts.isSupported()) return false;
+ const player = mpegts.createPlayer({ type: 'mpegts', isLive: isLive, url: url }, { enableWorker: true, stashInitialSize: 1024 * 1024 });
player.attachMediaElement(video);
player.load();
- player.play().catch(e => log('Autoplay prevented: ' + e));
-
- player.on(mpegts.Events.ERROR, (errType, errDetail) => {
- log('mpegts error: ' + errType + ' - ' + errDetail);
- });
-
+ player.play().catch(() => playOverlay.style.display = 'flex');
+ player.on(mpegts.Events.ERROR, () => showError('Stream error'));
return true;
}
async function playDirect(url) {
- log('Trying direct playback: ' + url);
- showLoading('Loading video...');
+ log('Direct play logic for Safari stability: ' + url);
+ showLoading('Lancement...');
video.src = url;
video.load();
-
- return new Promise((resolve) => {
- const onCanPlay = () => {
- hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
- resolve(true);
- };
-
- const onError = async () => {
- // If direct fail, try HLS
- if (await playWithHLS(url)) {
- resolve(true);
- } else {
- showError('Format not supported.');
- resolve(false);
- }
- };
-
- video.addEventListener('canplay', onCanPlay, { once: true });
- video.addEventListener('error', onError, { once: true });
+ video.play().catch(async () => {
+ log('Direct play blocked, trying fallbacks...');
+ if (url.toLowerCase().includes('.m3u8')) await playWithHLS(url);
+ else playOverlay.style.display = 'flex';
});
+ video.addEventListener('canplay', () => hideLoading(), { once: true });
+ video.addEventListener('error', () => showError('Format non supporté'), { once: true });
+ return true;
}
async function initPlayer() {
- if (!streamUrl) {
- showError('No stream URL provided');
- return;
- }
-
- log('Stream URL: ' + streamUrl);
- log('Stream type: ' + streamType);
+ if (!streamUrl) return showError('No URL');
const lowerUrl = streamUrl.toLowerCase();
-
- // LIVE TV
if (streamType === 'live') {
- await playWithMpegTS(streamUrl, true);
- return;
- }
-
- // VOD
- if (streamType === 'vod') {
- // If M3U8 -> HLS
- if (lowerUrl.includes('.m3u8')) {
- await playWithHLS(streamUrl);
- } else {
- // Try direct (MKV? wrapped in proxy?)
- // If proxy returns M3U8 content for MKV url, playWithHLS handles it? No, hls.js needs .m3u8 extension usually or explicit config.
- // But our VOD URL ends in /playlist.m3u8 so it enters the if above.
- await playDirect(streamUrl);
- }
- return;
- }
-
- // Fallback auto-detect
- if (lowerUrl.includes('.m3u8')) {
- await playWithHLS(streamUrl);
+ if (lowerUrl.includes('.m3u8')) await playWithHLS(streamUrl);
+ else await playWithMpegTS(streamUrl, true);
+ } else if (streamType === 'vod') {
+ if (lowerUrl.includes('.m3u8')) await playWithHLS(streamUrl);
+ else await playDirect(streamUrl);
} else {
- await playDirect(streamUrl);
+ if (lowerUrl.includes('.m3u8')) await playWithHLS(streamUrl);
+ else await playDirect(streamUrl);
}
}
- // Listen for messages from parent (Flutter)
- window.addEventListener('message', (event) => {
- const data = event.data;
- if (!data) return;
-
- log('Received message: ' + data.type);
-
- switch (data.type) {
- case 'play':
- video.play().catch(e => log('Play failed: ' + e));
- break;
- case 'pause':
- video.pause();
- break;
- case 'seek':
- const time = parseFloat(data.value);
- if (isFinite(time) && time >= 0) {
- video.currentTime = time;
- log('Seek to: ' + time);
- }
- break;
- case 'set_volume':
- const vol = parseFloat(data.value);
- if (isFinite(vol) && vol >= 0 && vol <= 1) {
- video.volume = vol;
- video.muted = vol === 0;
- }
- break;
- case 'request_fullscreen':
- if (video.requestFullscreen) {
- video.requestFullscreen();
- } else if (video.webkitRequestFullscreen) {
- video.webkitRequestFullscreen();
- } else if (video.msRequestFullscreen) {
- video.msRequestFullscreen();
- }
- break;
- }
- });
-
initPlayer();
diff --git a/web/player_lite.html b/web/player_lite.html
index 23aefcc..dd9c2c5 100644
--- a/web/player_lite.html
+++ b/web/player_lite.html
@@ -12,6 +12,7 @@
margin: 0;
padding: 0;
box-sizing: border-box;
+ -webkit-tap-highlight-color: transparent;
}
html,
@@ -23,6 +24,7 @@
display: flex;
justify-content: center;
align-items: center;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#player-container {
@@ -45,7 +47,7 @@
transform: translate(-50%, -50%);
color: #fff;
text-align: center;
- font-family: Arial, sans-serif;
+ z-index: 30;
}
#error-icon {
@@ -62,16 +64,16 @@
transform: translate(-50%, -50%);
color: #fff;
text-align: center;
- font-family: Arial, sans-serif;
+ z-index: 20;
}
.spinner {
width: 48px;
height: 48px;
- border: 4px solid #fff;
- border-top-color: transparent;
+ border: 4px solid rgba(255, 255, 255, 0.2);
+ border-top-color: #fff;
border-radius: 50%;
- animation: spin 1s linear infinite;
+ animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
margin: 0 auto 16px;
}
@@ -80,20 +82,54 @@
transform: rotate(360deg);
}
}
+
+ /* Safari Autoplay Hint Overlay */
+ #play-overlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 80px;
+ height: 80px;
+ background: rgba(0, 0, 0, 0.6);
+ border-radius: 50%;
+ display: none;
+ justify-content: center;
+ align-items: center;
+ cursor: pointer;
+ border: 2px solid rgba(255, 255, 255, 0.3);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ z-index: 25;
+ }
+
+ #play-overlay svg {
+ width: 40px;
+ height: 40px;
+ fill: white;
+ margin-left: 4px;
+ }
-
-
+
+
+
+
+
-
Loading stream...
+
Chargement du flux...
⚠
-
Failed to load stream
+
Erreur de chargement
@@ -103,6 +139,7 @@
const loadingText = document.getElementById('loading-text');
const error = document.getElementById('error');
const errorMessage = document.getElementById('error-message');
+ const playOverlay = document.getElementById('play-overlay');
// Get parameters from query string
const urlParams = new URLSearchParams(window.location.search);
@@ -116,7 +153,7 @@
function showLoading(msg) {
loading.style.display = 'block';
- loadingText.textContent = msg || 'Loading stream...';
+ loadingText.textContent = msg || 'Chargement du flux...';
error.style.display = 'none';
}
@@ -127,18 +164,23 @@
function showError(message) {
hideLoading();
error.style.display = 'block';
- errorMessage.textContent = message || 'Failed to load stream';
+ errorMessage.textContent = message || 'Erreur de chargement';
}
+ playOverlay.addEventListener('click', () => {
+ log('Manual play triggered');
+ video.play();
+ playOverlay.style.display = 'none';
+ });
+
// Seek to startTime when video is ready
video.addEventListener('loadedmetadata', () => {
if (startTime > 0 && startTime < video.duration) {
video.currentTime = startTime;
- log('Resuming from ' + Math.floor(startTime / 60) + 'm ' + Math.floor(startTime % 60) + 's');
}
});
- // Report position to parent for resume feature
+ // Report position to parent
let lastReportedTime = 0;
function reportPosition() {
if (video.currentTime > 0 && !video.paused && video.readyState > 2) {
@@ -154,23 +196,17 @@
}
setInterval(reportPosition, 5000);
- // Report on pause and end
video.addEventListener('pause', () => {
- window.parent.postMessage({
- type: 'playback_status',
- status: 'paused'
- }, '*');
+ window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
+ if (loading.style.display === 'none') {
+ playOverlay.style.display = 'flex';
+ }
});
video.addEventListener('playing', () => {
- window.parent.postMessage({
- type: 'playback_status',
- status: 'playing'
- }, '*');
-
- // Also reset reconnect attempts on successful playing
- reconnectAttempts = 0;
+ window.parent.postMessage({ type: 'playback_status', status: 'playing' }, '*');
hideLoading();
+ playOverlay.style.display = 'none';
});
video.addEventListener('ended', () => {
@@ -180,49 +216,33 @@
}, '*');
});
- // Report user activity (mouse movement or touch) to parent for auto-hide controls
- let lastActivityTime = 0;
+ // Activity reporting
function reportActivity() {
- const now = Date.now();
- if (now - lastActivityTime > 100) { // Throttle to 100ms
- lastActivityTime = now;
- window.parent.postMessage({
- type: 'user_activity'
- }, '*');
- }
+ window.parent.postMessage({ type: 'user_activity' }, '*');
}
document.addEventListener('mousemove', reportActivity);
document.addEventListener('touchstart', reportActivity);
- document.addEventListener('touchmove', reportActivity);
document.addEventListener('click', reportActivity);
- document.addEventListener('touchend', reportActivity);
async function playWithHLS(url) {
log('Trying HLS.js with: ' + url);
- showLoading('Loading HLS stream...');
+ showLoading('Chargement HLS...');
if (!Hls.isSupported()) {
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
- video.addEventListener('canplay', () => {
- hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
- });
+ video.load();
+ video.play().catch(() => playOverlay.style.display = 'flex');
return true;
}
return false;
}
const hls = new Hls({
- maxBufferLength: 60, // doubled for stability on mobile
+ maxBufferLength: 60,
maxMaxBufferLength: 120,
- liveSyncDurationCount: 5, // more segments for safety
- liveMaxLatencyDurationCount: 10,
- liveDurationInfinity: true,
enableWorker: true,
- backStep: 0.2, // step back slightly on stall
- nudgeOffset: 0.1,
- nudgeMaxRetry: 5
+ liveSyncDurationCount: 5
});
window.hlsInstance = hls;
@@ -230,22 +250,15 @@
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
- log('HLS manifest parsed');
hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
+ video.play().catch(() => playOverlay.style.display = 'flex');
});
hls.on(Hls.Events.ERROR, (event, data) => {
- log('HLS Error: ' + data.type + ' - ' + data.details);
if (data.fatal) {
- if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
- hls.startLoad();
- } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
- hls.recoverMediaError();
- } else {
- showError('HLS Error: ' + data.details);
- hls.destroy();
- }
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) hls.startLoad();
+ else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) hls.recoverMediaError();
+ else showError('HLS Error: ' + data.details);
}
});
@@ -253,282 +266,50 @@
}
async function playWithMpegTS(url, isLive = true) {
- log('Trying mpegts.js with: ' + url + ' (isLive: ' + isLive + ')');
- showLoading('Loading MPEG-TS stream...');
+ log('Trying mpegts.js with: ' + url);
+ showLoading('Chargement MPEG-TS...');
- if (!mpegts.isSupported()) {
- return false;
- }
+ if (!mpegts.isSupported()) return false;
- if (mpegts.getLoggingControl) {
- mpegts.getLoggingControl().enableAll = false;
- mpegts.getLoggingControl().enableError = true;
- }
-
- // Optimized configuration for stable Live TV playback
- const playerConfig = {
+ const player = mpegts.createPlayer({
+ type: 'mpegts',
+ isLive: isLive,
+ url: url
+ }, {
enableWorker: true,
- enableStashBuffer: true, // Enable buffering for stability
- stashInitialSize: 1024 * 1024, // 1MB initial buffer
- autoCleanupSourceBuffer: true,
- autoCleanupMaxBackwardDuration: 60, // Keep 1 min of buffer
- autoCleanupMinBackwardDuration: 30, // Cleanup after 30s
- fixAudioTimestampGap: true, // Fix audio sync issues
- liveBufferLatencyChasing: false, // Disable for stability (prevents skipping)
- liveSync: false, // Don't auto-sync to edge
- lazyLoad: false, // Load immediately
- lazyLoadMaxDuration: 0,
- lazyLoadRecoverDuration: 0,
- };
-
- let player = null;
- let reconnectAttempts = 0;
- const maxReconnectAttempts = 10;
- let isDestroyed = false;
-
- 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');
- hideLoading();
- video.play().catch(e => log('Autoplay prevented: ' + e));
+ stashInitialSize: 1024 * 1024,
+ enableStashBuffer: true
});
- video.addEventListener('error', () => {
- if (!isLive || reconnectAttempts >= maxReconnectAttempts) {
- showError('Video error: ' + (video.error?.message || 'Unknown error'));
- }
- });
+ player.attachMediaElement(video);
+ player.load();
+ player.play().catch(() => playOverlay.style.display = 'flex');
- // Cleanup function
- window.cleanupMpegtsPlayer = () => {
- isDestroyed = true;
- if (player) {
- try {
- player.unload();
- player.detachMediaElement();
- player.destroy();
- } catch (e) { }
- }
- };
+ player.on(mpegts.Events.ERROR, () => showError('MpegTS Error'));
return true;
}
- async function playDirect(url) {
- log('Trying direct playback: ' + url);
- showLoading('Loading video...');
-
- video.preload = 'auto';
- let playStarted = false;
-
- video.addEventListener('waiting', () => {
- log('Buffering...');
- showLoading('Buffering...');
- });
-
- video.addEventListener('playing', () => {
- playStarted = true;
- hideLoading();
- });
-
- const onCanPlay = () => {
- playStarted = true;
- hideLoading();
- // FORCE AUDIO SETTINGS
- video.muted = false;
- video.volume = 1.0;
-
- video.play().catch(e => {
- log('Autoplay prevented: ' + e);
- // If autoplay prevents sound/video, we should show a UI to user but native controls are there
- // Try playing muted if failed
- video.muted = true;
- video.play().then(() => {
- log('Autoplay started muted');
- showError('Click to unmute'); // User can verify context
- }).catch(e2 => log('Autoplay failed completely: ' + e2));
- });
- };
-
- const onError = async () => {
- if (!playStarted) {
- log('Direct playback failed, trying fallbacks...');
- video.removeEventListener('canplay', onCanPlay);
- video.removeEventListener('error', onError);
-
- // Fallback 1: HLS (common for streams)
- log('Trying HLS fallback...');
- if (await playWithHLS(url)) return;
-
- // Fallback 2: MpegTS (for TS streams)
- log('Comparing HLS failed, trying MpegTS fallback...');
- if (await playWithMpegTS(url)) return;
-
- showError('Format not supported. Try Standard player mode.');
- }
- };
-
- video.addEventListener('canplay', onCanPlay, { once: true });
- video.addEventListener('error', onError, { once: true });
-
- video.src = url;
- video.load();
- return true;
- }
-
async function initPlayer() {
- if (!streamUrl) {
- showError('No stream URL provided');
- return;
- }
-
+ if (!streamUrl) return showError('No URL');
log('Stream URL: ' + streamUrl);
- log('Stream type: ' + streamType);
+
const lowerUrl = streamUrl.toLowerCase();
+ const isHLS = lowerUrl.includes('.m3u8') || lowerUrl.includes('/playlist.m3u8');
- // LIVE TV: Try HLS first if URL contains m3u8, otherwise mpegts
- if (streamType === 'live') {
- if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/playlist.m3u8')) {
- log('Live TV mode - HLS detected, using HLS.js');
- await playWithHLS(streamUrl);
- } else {
- log('Live TV mode - using mpegts.js direct');
- showLoading('Connecting to live stream...');
- await playWithMpegTS(streamUrl, true);
- }
- return;
- }
-
- // VOD: Direct HTML5 playback with proxy
- if (streamType === 'vod') {
- log('VOD mode: Using direct playback');
- showLoading('Loading video...');
- await playDirect(streamUrl);
- return;
- }
-
- // Fallback: detect from URL pattern
- if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) {
+ if (isHLS) {
await playWithHLS(streamUrl);
- } else if (lowerUrl.includes('.mp4') || lowerUrl.includes('.mkv') ||
- lowerUrl.includes('/movie/') || lowerUrl.includes('/series/')) {
- await playDirect(streamUrl);
- } else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/')) {
- await playWithMpegTS(streamUrl, true);
} else {
- await playDirect(streamUrl);
+ await playWithMpegTS(streamUrl, streamType === 'live');
}
}
- // Listen for messages from Flutter
window.addEventListener('message', (event) => {
const data = event.data;
- if (!data) return;
-
- log('Received message from parent: ' + JSON.stringify(data));
-
- if (data.type === 'play') {
- video.play().catch(e => log('Play command failed: ' + e));
- } else if (data.type === 'pause') {
- video.pause();
- } else if (data.type === 'set_volume') {
- video.volume = Math.max(0, Math.min(1, parseFloat(data.value)));
- video.muted = video.volume === 0;
- } else if (data.type === 'seek') {
- if (isFinite(video.duration)) {
- video.currentTime = parseFloat(data.value);
- }
- } else if (data.type === 'request_fullscreen') {
- log('Fullscreen requested from parent');
- const el = document.documentElement;
- const requestMethod = el.requestFullscreen || el.webkitRequestFullscreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen;
-
- if (requestMethod) {
- // Browsers require a user gesture for fullscreen.
- // This message usually comes from a Flutter button tap, so it should count as a gesture.
- requestMethod.call(el).catch(e => {
- log('Fullscreen API error: ' + e);
- // Fallback: try element-specific request for iOS
- if (video.webkitEnterFullscreen) {
- video.webkitEnterFullscreen();
- }
- });
- } else if (video.webkitEnterFullscreen) {
- video.webkitEnterFullscreen();
- }
- }
+ if (data.type === 'play') video.play().catch(() => playOverlay.style.display = 'flex');
+ else if (data.type === 'pause') video.pause();
});
- // Start player
initPlayer();