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:

  1. ubah 1 item di data.json ke format sources
{
  "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);
  });
});
  1. 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 🔥