const FORCE_STREAM_URL="https://live.fixnet.id/hls/stream/index.m3u8"; Fixnet Live Player
/* ---------- Helpers ---------- */ function showSpinner(){ spinner.style.display="flex"; } function hideSpinner(){ spinner.style.display="none"; } function showThumb(){ thumb.style.display="block"; playIcon.style.display="block"; video.style.display="none"; } function hideThumb(){ thumb.style.display="none"; playIcon.style.display="none"; video.style.display="block"; } /* ---------- INIT PLAYER & HLS handling ---------- */ function initPlayer(stream){ console.log("[player] init ->",stream); showSpinner(); // clean previous clearIntervals(); try{ if(hls){ hls.destroy(); hls=null; } }catch(e){console.warn(e);} if(Hls.isSupported()){ hls=new Hls({ manifestLoadingRetryDelay:2000, manifestLoadingMaxRetry:40, autoStartLoad:true, maxBufferLength: 30 }); // Mount events hls.on(Hls.Events.ERROR,(ev,data)=>{ console.warn("[hls.error]",data); // fatal handling if(data.fatal){ if(data.type === Hls.ErrorTypes.NETWORK_ERROR){ // network problems → try fallback handleFallback(stream); } else if(data.type === Hls.ErrorTypes.MEDIA_ERROR){ // try recover try{ hls.recoverMediaError(); }catch(e){ handleFallback(stream); } } else { handleFallback(stream); } } }); hls.on(Hls.Events.MANIFEST_PARSED,()=>{ hideSpinner(); hideThumb(); video.currentTime=0; video.play().catch(()=>{}); startFreezeCheck(); populateQuality(); startStats(); }); hls.on(Hls.Events.LEVEL_SWITCHED,()=>updateStatsImmediate()); hls.loadSource(FORCE_STREAM_URL); hls.attachMedia(video);hls.on(Hls.Events.MANIFEST_PARSED,function(){video.muted=true;video.play().catch(function(){});}); } else { // Native HLS (Safari) video.src = FORCE_STREAM_URL; video.play().catch(()=>{}); hideSpinner(); hideThumb(); startFreezeCheck(); } } /* ---------- QUALITY UI (Auto / manual) ---------- */ function populateQuality(){ if(!hls || !hls.levels || hls.levels.length===0){ qualitySel.style.display="none"; controlsTop.style.display="block"; return; } // Build options qualitySel.innerHTML = ''; hls.levels.forEach((lvl, idx)=>{ // label prefer height or bitrate const label = (lvl.height? lvl.height+'p' : (Math.round(lvl.bitrate/1000)+'kb')) + (lvl.audioCodec? '':''); const opt = document.createElement('option'); opt.value = idx; opt.textContent = label; qualitySel.appendChild(opt); }); qualitySel.style.display="inline-block"; controlsTop.style.display="block"; // default to Auto qualitySel.value = -1; qualitySel.onchange = ()=>{ const v = parseInt(qualitySel.value,10); if(hls){ if(v===-1){ hls.currentLevel = -1; // auto console.log('[quality] auto'); } else { hls.currentLevel = v; // manual switch console.log('[quality] manual ->',v); } } }; } /* ---------- STATS overlay ---------- */ function startStats(){ statsBox.style.display='block'; updateStatsImmediate(); if(statTimer) clearInterval(statTimer); statTimer = setInterval(updateStatsImmediate,1500); } function updateStatsImmediate(){ if(!hls){ statsBox.textContent = '—'; return; } // bandwidth estimate (bytes/s) const bw = hls.bandwidthEstimate ? Math.round(hls.bandwidthEstimate()/1000) : 0; const level = (hls.currentLevel>=0 && hls.levels[hls.currentLevel]) ? (hls.levels[hls.currentLevel].height || Math.round(hls.levels[hls.currentLevel].bitrate/1000)+'kb') : 'Auto'; // dropped frames via getVideoPlaybackQuality (standard) or webkit let dropped = 'n/a', fps = 'n/a'; try{ const q = video.getVideoPlaybackQuality ? video.getVideoPlaybackQuality() : (video.webkitDecodedFrameCount ? {droppedVideoFrames: (video.webkitDroppedFrameCount||0), totalVideoFrames: video.webkitDecodedFrameCount||0} : null); if(q){ dropped = q.droppedVideoFrames ?? (video.webkitDroppedFrameCount||0); const total = q.totalVideoFrames ?? (video.webkitDecodedFrameCount||0); fps = total && lastTime ? Math.round( (total - (video._lastTotal||0)) / (1.5) ) : '—'; video._lastTotal = total; } }catch(e){} statsBox.textContent = `lvl:${level} | bw:${bw}kb/s | drop:${dropped}`; } /* ---------- FREEZE / AUTO-RECOVER ---------- */ function startFreezeCheck(){ if(freezeTimer) clearInterval(freezeTimer); lastTime = video.currentTime; freezeTimer = setInterval(()=>{ if(video.currentTime === lastTime){ console.warn("[freeze] detected, try recover"); handleFallback(preferredSrc); } lastTime = video.currentTime; },6000); } /* ---------- FALLBACK logic ---------- */ function handleFallback(current){ // If current is CDN, try origin; if current is origin, show thumb + retry origin clearInterval(reconnectTimer); clearInterval(freezeTimer); clearInterval(statTimer); try{ if(hls){ hls.destroy(); hls=null; } }catch(e){} video.pause(); video.removeAttribute('src'); video.load(); showThumb(); hideSpinner(); // If currently using CDN, switch to origin first if(current === CDN_SRC || current === preferredSrc){ console.warn("[fallback] CDN -> origin"); reconnectTimer = setInterval(()=>{ fetch(ORIGIN_SRC,{method:'HEAD',cache:'no-store'}).then(r=>{ if(r.ok){ clearInterval(reconnectTimer); preferredSrc = ORIGIN_SRC; initPlayer(ORIGIN_SRC); } }).catch(()=>{}); },3500); } else { // origin failed too: keep checking origin occasionally console.warn("[fallback] origin failed -> keep checking"); reconnectTimer = setInterval(()=>{ fetch(ORIGIN_SRC,{method:'HEAD',cache:'no-store'}).then(r=>{ if(r.ok){ clearInterval(reconnectTimer); preferredSrc = ORIGIN_SRC; initPlayer(ORIGIN_SRC); } }).catch(()=>{}); },5000); } } /* ---------- UTIL: clear all timers ---------- */ function clearIntervals(){ if(freezeTimer) { clearInterval(freezeTimer); freezeTimer=null; } if(reconnectTimer){ clearInterval(reconnectTimer); reconnectTimer=null; } if(statTimer){ clearInterval(statTimer); statTimer=null; } } /* ---------- STARTUP: test preferredSrc then init ---------- */ function probeAndStart(s){ // try HEAD first fetch(s,{method:'HEAD',cache:'no-store'}).then(r=>{ if(r.ok){ preferredSrc = s; initPlayer(s); } else { // fallback to origin if(s !== ORIGIN_SRC){ console.warn("[probe] CDN HEAD failed -> try origin"); probeAndStart(ORIGIN_SRC); } else { // both fail: show thumb and keep retrying origin showThumb(); handleFallback(ORIGIN_SRC); } } }).catch(()=>{ if(s !== ORIGIN_SRC) probeAndStart(ORIGIN_SRC); else { showThumb(); handleFallback(ORIGIN_SRC); } }); } /* ---------- UI small handlers (like/share) ---------- */ let liked=false; document.getElementById("likeBtn").onclick=()=>{ liked=!liked; const i=document.getElementById("likeIcon"), t=document.getElementById("likeText"); i.className = liked ? "fa-solid fa-heart" : "fa-regular fa-heart"; i.style.color = liked ? "#ff4b4b" : "#fff"; t.textContent = liked ? "Disukai" : "Suka"; }; document.getElementById("shareBtn").onclick = e => { e.stopPropagation(); const m=document.getElementById("shareMenu"); m.style.display = m.style.display === "flex" ? "none" : "flex"; }; document.addEventListener("click",()=>document.getElementById("shareMenu").style.display="none"); function shareTo(p){ const url="https://live.kbmtv.id/live"; const text=encodeURIComponent("Tonton live streaming KBMTV!"); let s=""; if(p==="facebook")s=`https://www.facebook.com/sharer/sharer.php?u=${url}`; if(p==="whatsapp")s=`https://api.whatsapp.com/send?text=${text}%20${url}`; if(p==="x")s=`https://twitter.com/intent/tweet?text=${text}&url=${url}`; if(p==="telegram")s=`https://t.me/share/url?url=${url}&text=${text}`; window.open(s,"_blank"); } function copyLink(){ navigator.clipboard.writeText("https://live.kbmtv.id/live"); alert("Link berhasil disalin!"); } /* ---------- Kickoff ---------- */ controlsTop.style.display='none'; // hidden until populateQuality decides probeAndStart(preferredSrc); /* clean before unload */ window.addEventListener('beforeunload',()=>{ clearIntervals(); try{ if(hls) hls.destroy(); }catch(e){} });