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){}
});