Nah ini baru setup yang powerful 🔥
Kamu sudah punya semua “senjata”:
- R2 ✅
- Bunny CDN ✅
- dood / upload18 (backup / throwaway) ✅
👉 Tinggal kita rapikan jadi sistem yang konsisten, bukan acak.
🎯 NEXT STEP — MULTI SOURCE SYSTEM (SMART)
Sekarang masalah kamu sebenarnya:
“punya banyak sumber video, tapi belum terstruktur”
Kita bikin supaya:
👉 1 video = bisa punya banyak source
👉 player pilih otomatis / manual
⚡ Konsep yang kita pakai
Sekarang data.json kamu:
{
"type": "mp4",
"src": "..."
}
👉 kita upgrade jadi:
{
"title": "video1",
"kind": "video",
"sources": [
{
"type": "mp4",
"label": "R2",
"src": "https://r2-url/video1.mp4"
},
{
"type": "mp4",
"label": "Bunny",
"src": "https://bunny-url/video1.mp4"
},
{
"type": "iframe",
"label": "Dood",
"src": "https://doodstream.com/e/xxxx"
}
],
"thumb": "media/video1.jpg"
}
🧠 Kenapa ini penting
👉 kalau 1 source mati:
- tinggal switch
- tidak perlu edit ulang sistem
👉 ini cara kerja website streaming beneran
⚡ STEP 1 — update app.js (support multi source)
Ganti openPlayer() jadi ini:
function openPlayer(item) {
modal.classList.remove("hidden");
if (currentHls) {
currentHls.destroy();
currentHls = null;
}
player.innerHTML = "";
// 🔥 kalau pakai multi source
if (item.sources && item.sources.length > 0) {
renderMultiSource(item);
return;
}
// fallback (old system)
renderSingle(item);
}
⚡ STEP 2 — tambah function ini
function renderMultiSource(item) {
const first = item.sources[0];
player.innerHTML = `
<div id="sourceButtons"></div>
<div id="videoContainer"></div>
`;
const btnContainer = document.getElementById("sourceButtons");
const container = document.getElementById("videoContainer");
item.sources.forEach((src, index) => {
const btn = document.createElement("button");
btn.textContent = src.label;
btn.className = "source-btn";
btn.onclick = () => {
loadSource(src, container);
};
btnContainer.appendChild(btn);
// load pertama
if (index === 0) {
loadSource(src, container);
}
});
}
⚡ STEP 3 — loader universal
function loadSource(src, container) {
if (currentHls) {
currentHls.destroy();
currentHls = null;
}
if (src.type === "mp4") {
container.innerHTML = `
<video controls autoplay>
<source src="${src.src}" type="video/mp4">
</video>
`;
}
else if (src.type === "iframe") {
container.innerHTML = `
<iframe src="${src.src}" allowfullscreen></iframe>
`;
}
else if (src.type === "m3u8") {
container.innerHTML = `<video id="hlsPlayer" controls autoplay></video>`;
const video = document.getElementById("hlsPlayer");
if (Hls.isSupported()) {
currentHls = new Hls();
currentHls.loadSource(src.src);
currentHls.attachMedia(video);
} else {
video.src = src.src;
}
}
}
⚡ STEP 4 — CSS tombol source
Tambahin:
.source-btn {
margin: 5px;
padding: 6px 10px;
background: #222;
color: white;
border: 1px solid #444;
cursor: pointer;
border-radius: 6px;
}
🎯 HASIL
Sekarang:
- klik video
- muncul tombol:
- R2
- Bunny
- Dood
- user bisa switch source 🔥
💬 Real talk (ini penting banget buat kamu)
Sekarang kamu sudah punya:
✅ multi source system
✅ fallback manual
✅ scalable video source
👉 ini sudah:
“streaming architecture”
🚀 NEXT LEVEL (ini gila sih kalau kamu lanjut)
Kalau kamu mau upgrade:
🔥 AUTO FAILOVER
- kalau R2 error → auto pindah Bunny
🔥 SPEED BASED SOURCE
- pilih CDN terdekat
🔥 SOURCE PRIORITY
"priority": ["R2", "Bunny", "Dood"]
👉 Sekarang
Coba dulu:
- ubah 1 item di
data.jsonke formatsources
{
"name": "video-site",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "node build.js",
"generate": "node generate.js",
"thumb": "node generate-thumb.js"
}
}app.js Full
const gallery = document.getElementById("gallery");
const modal = document.getElementById("playerModal");
const player = document.getElementById("player");
const filterButtons = document.querySelectorAll(".filter-btn");
let currentHls = null;
let allItems = [];
function formatDuration(seconds) {
if (!seconds || isNaN(seconds)) return "";
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
if (hrs > 0) {
return `${hrs}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
}
return `${mins}:${String(secs).padStart(2, "0")}`;
}
function getBadgeText(item) {
if (item.kind === "video") return "VIDEO";
if (item.kind === "stream") return "STREAM";
if (item.kind === "image") return "IMAGE";
return item.type ? item.type.toUpperCase() : "MEDIA";
}
function destroyHls() {
if (currentHls) {
currentHls.destroy();
currentHls = null;
}
}
function renderGallery(items) {
gallery.innerHTML = "";
if (!items.length) {
gallery.innerHTML = `<p>Tidak ada item untuk filter ini.</p>`;
return;
}
items.forEach(item => {
const div = document.createElement("div");
div.className = "card";
const durationText =
item.kind === "video" && item.duration
? `<span class="card-duration">${formatDuration(item.duration)}</span>`
: "";
div.innerHTML = `
<div class="thumb-wrap">
<img src="${item.thumb}" alt="${item.title}">
<span class="card-badge">${getBadgeText(item)}</span>
${durationText}
</div>
<div class="card-body">
<p class="card-title">${item.title}</p>
</div>
`;
div.onclick = () => openPlayer(item);
gallery.appendChild(div);
});
}
function applyFilter(filter) {
if (filter === "all") {
renderGallery(allItems);
return;
}
const filtered = allItems.filter(item => item.kind === filter);
renderGallery(filtered);
}
function openPlayer(item) {
modal.classList.remove("hidden");
destroyHls();
player.innerHTML = "";
// support format baru: multi source
if (item.sources && Array.isArray(item.sources) && item.sources.length > 0) {
renderMultiSource(item);
return;
}
// support format lama
renderSingle(item);
}
function renderSingle(item) {
if (item.type === "mp4") {
player.innerHTML = `
<div class="video-wrapper">
<video controls autoplay playsinline>
<source src="${item.src}" type="video/mp4">
</video>
</div>
`;
} else if (item.type === "image") {
player.innerHTML = `
<div class="image-wrapper">
<img src="${item.src}" alt="${item.title}">
</div>
`;
} else if (item.type === "iframe") {
player.innerHTML = `
<div class="iframe-wrapper">
<iframe
src="${item.src}"
allowfullscreen
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
`;
} else if (item.type === "m3u8") {
player.innerHTML = `
<div class="video-wrapper">
<video id="hlsPlayer" controls autoplay playsinline></video>
</div>
`;
const video = document.getElementById("hlsPlayer");
if (window.Hls && Hls.isSupported()) {
currentHls = new Hls();
currentHls.loadSource(item.src);
currentHls.attachMedia(video);
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = item.src;
} else {
player.innerHTML = `<p>Browser tidak mendukung HLS.</p>`;
}
} else {
player.innerHTML = `<p>Tipe media tidak dikenali.</p>`;
}
}
function renderMultiSource(item) {
player.innerHTML = `
<div class="source-switcher" id="sourceButtons"></div>
<div id="videoContainer"></div>
`;
const btnContainer = document.getElementById("sourceButtons");
const container = document.getElementById("videoContainer");
item.sources.forEach((source, index) => {
const btn = document.createElement("button");
btn.className = "source-btn";
btn.textContent = source.label || `Source ${index + 1}`;
btn.addEventListener("click", () => {
document.querySelectorAll(".source-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
loadSource(source, container, item.title);
});
btnContainer.appendChild(btn);
if (index === 0) {
btn.classList.add("active");
loadSource(source, container, item.title);
}
});
}
function loadSource(source, container, title = "") {
destroyHls();
container.innerHTML = "";
if (source.type === "mp4") {
container.innerHTML = `
<div class="video-wrapper">
<video controls autoplay playsinline>
<source src="${source.src}" type="video/mp4">
</video>
</div>
`;
} else if (source.type === "iframe") {
container.innerHTML = `
<div class="iframe-wrapper">
<iframe
src="${source.src}"
allowfullscreen
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
`;
} else if (source.type === "m3u8") {
container.innerHTML = `
<div class="video-wrapper">
<video id="hlsPlayer" controls autoplay playsinline></video>
</div>
`;
const video = document.getElementById("hlsPlayer");
if (window.Hls && Hls.isSupported()) {
currentHls = new Hls();
currentHls.loadSource(source.src);
currentHls.attachMedia(video);
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = source.src;
} else {
container.innerHTML = `<p>Browser tidak mendukung HLS.</p>`;
}
} else if (source.type === "image") {
container.innerHTML = `
<div class="image-wrapper">
<img src="${source.src}" alt="${title}">
</div>
`;
} else {
container.innerHTML = `<p>Source tidak dikenali.</p>`;
}
}
function closePlayer() {
modal.classList.add("hidden");
destroyHls();
player.innerHTML = "";
}
fetch("data.json")
.then(res => res.json())
.then(data => {
allItems = data;
renderGallery(allItems);
})
.catch(err => {
console.error("Gagal load data.json:", err);
gallery.innerHTML = `<p>Gagal memuat data gallery.</p>`;
});
filterButtons.forEach(btn => {
btn.addEventListener("click", () => {
filterButtons.forEach(b => b.classList.remove("active"));
btn.classList.add("active");
applyFilter(btn.dataset.filter);
});
});- test di browser
Kalau sudah:
👉 bilang "multi source jalan"
Kalau error:
👉 kirim data.json sample + error
Gue bantu sampai sistem kamu jadi level streaming site beneran 🔥