Use Cases (Diperbarui: 7/6/2026)

Bikin video player React sendiri: subtitle, kecepatan, sampai analitik tonton

Bikin video player untuk kursus dan media dengan Claude Code: kontrol pemutaran, subtitle, aksesibilitas, dan analitik, plus kode React.

Bikin video player React sendiri: subtitle, kecepatan, sampai analitik tonton

Sehari setelah saya menanam video kursus berbayar, masuk satu email dari peserta. “Bisa diputar 1.25x nggak ya?”

Waktu itu saya pakai library video player yang tampilannya bagus. Tombol ubah kecepatan ada. Tapi posisi subtitle berantakan di ponsel, posisi terakhir tidak tersimpan, dan saya sama sekali tidak tahu di mana orang berhenti menonton. Singkatnya: videonya bisa diputar, tapi “tidak layak dipakai sebagai kursus”.

Akhirnya library itu saya buang dan saya susun ulang dari elemen <video> native. Hari ini saya tulis seluruh implementasinya, lengkap dengan kode React yang tinggal salin-tempel.

Poin penting

  • Video player itu bukan soal “tombol play”, melainkan pengalaman lengkap: posisi lanjut, subtitle, kecepatan, sampai analitik tonton. Di sinilah tingkat penyelesaian dan omzet ditentukan.
  • Fondasinya elemen <video> native. Menyinkronkan currentTime dan paused dari HTMLMediaElement ke React adalah inti implementasi yang stabil.
  • State React jangan diubah berdasarkan tebakan saat tombol ditekan; sinkronkan dari event play/pause. Karena pembatasan autoplay bisa membuat video.play() gagal, try/catch wajib ada.
  • Aksesibilitas hanya soal “mengembalikan kontrol setara setelah kontrol native disembunyikan”. Cukup button asli, input type="range", dan role="alert".
  • Analitik jangan dikirim tiap detik. Cukup 7 titik: mulai putar, 25/50/75%, selesai, error, dan klik CTA. Itu sudah cukup untuk perbaikan.

Video player bukan sekadar “tombol play”

Video player adalah UI yang memuat berkas atau stream video lalu membuat pembaca bisa mengontrol pemutaran, jeda, seek, volume, subtitle, dan kecepatan. Di Web, biasanya elemen <video> native dijadikan fondasi, lalu dari JavaScript kita membaca dan menulis currentTime, duration, paused, volume, muted, dan playbackRate milik HTMLMediaElement.

Definisi ini saya taruh di depan karena ada alasannya: video player bukan hiasan.

Untuk produk pembelajaran, apakah peserta bisa melanjutkan dari tengah, terbantu subtitle, dan mengulang dengan kecepatan lebih lambat — semua ini berhubungan langsung dengan tingkat penyelesaian. Email pembuka tadi persis soal itu. Untuk situs media, apakah video mengganggu tampilan awal artikel, apakah membuat pembaca menunggu di jaringan seluler, apakah pembaca yang selesai menonton mengalir mulus ke artikel lain atau pendaftaran. Untuk demo produk SaaS, pengalaman pemutaran itu sendiri menjadi rasa percaya.

Sebelum mulai mengoding, sekali saja baca referensi MDN elemen <video> dan HTMLMediaElement API; nanti pertanyaan “properti ini buat apa ya” jadi berkurang. Claude Code jago merapikan komponen, tes, review aksesibilitas, dan event pengukuran sekaligus. Tapi “pengalaman mana yang dijaga” tetap keputusan sisi produk — itu tidak bisa dilempar mentah-mentah ke AI.

Memilih native, kustom, atau streaming

Hal pertama yang harus diputuskan bukan tampilan tombol, melainkan metode pemutaran. Lewati langkah ini, dan Anda terpaksa membangun ulang dari fondasi nanti (itu yang saya alami).

Elemen <video controls> native paling cepat dipublikasikan, dan browser mengurus operasi keyboard serta tampilan subtitle dasar untuk Anda. Sebaliknya, kalau butuh simpan progres, CTA untuk member, penanda bab, atau pengukuran sendiri, pakai kontrol kustom yang mengoperasikan HTMLMediaElement langsung. Kalau videonya panjang, penontonnya banyak, atau perbedaan negara dan jaringan besar, streaming lewat HLS/DASH atau layanan distribusi video juga masuk pertimbangan.

MetodeCocok untukYang dilihat saat produksi
<video controls> nativeSematan pendek dalam artikel, dokumen internal, LP sederhanaImplementasi cepat. Ekspresi brand dan pengukuran detail terbatas, tapi keandalan operasi dasar tinggi
Kontrol kustom di atas HTMLMediaElementProduk belajar, media, demo SaaS, video memberBisa kontrol UI, posisi lanjut, tampilan CTA, dan analitik. Imbalannya: tanggung jawab aksesibilitas dan penanganan error ada di Anda
HLS/DASH atau platform distribusiKursus panjang, katalog besar, live, video yang perlu proteksiPerlu rancang transkoding, manifest, CDN, bitrate, otorisasi, dan library pemutar

Kalau Anda membuat player pertama dengan Claude Code, realistisnya mulai dari MP4 pendek, preload="metadata", gambar poster, dan berkas subtitle. Saat butuh tautan bab, pencarian subtitle, rasio penyelesaian, tampilan khusus pembeli, atau integrasi SSO internal, baru kustomkan; dan begitu sadar “satu MP4 saja nggak cukup”, barulah pindah ke streaming. Urutan ini membuat Anda tidak menggendong infrastruktur berlebih sejak awal.

Bagi per lapis, permintaan jadi spesifik

Kalau UI video dipikirkan sebagai satu gumpalan, setiap perbaikan jadi urusan besar. Dibagi per lapis, permintaan ke Claude Code bisa dipotong kecil-kecil.

LapisPeranYang diperiksakan ke Claude Code
Manajemen asetSiapkan MP4/WebM, poster, subtitle, manifestURL, MIME type, CORS, URL berbatas waktu, teks alternatif
Elemen mediaSerahkan pemutaran, pemuatan, waktu, subtitle, error ke browserpreload, playsInline, track, teks fallback, langganan event
Manajemen stateCerminkan currentTime, paused, muted, kecepatan ke ReactApakah state disinkronkan dari media event, bukan dari tebakan
UI kontrolSediakan play, seek, volume, kecepatan, subtitle, fullscreenPakai button dan input, jaga operasi keyboard dan label
State berkelanjutanSimpan posisi lanjut, status selesai, kecepatan, setelan muteMinimalkan cakupan simpanan; situs publik sertakan penjelasan privasi
PengukuranMulai putar, 25/50/75%, selesai, error, klik CTAJangan kirim tiap detik; simpan hanya metrik untuk perbaikan
PerformaPoster, CDN, lazy loading, bitrateCLS, transfer awal, jaringan seluler, header cache

Dengan pembagian ini, permintaan jadi konkret: “review aksesibilitas seek bar saja”, “perbaiki ukuran poster dan strategi pemuatannya saja”, “tes supaya event selesai tidak terkirim dobel”. Perbaikan kecil di UI video pun tidak ikut menyeret infrastruktur distribusi atau desain analitik sampai jadi insiden.

Empat situasi yang efektif

1. Lesson kursus berbayar Peserta pergi sebentar di tengah, balik dari perangkat lain, lalu mengulang dengan kecepatan 1.25x. Yang dibutuhkan bukan animasi cantik, melainkan subtitle, posisi lanjut, syarat selesai, dan jalur ke lesson berikutnya. Event selesai dikirim bukan saat halaman tampil, tapi saat persentase tertentu sudah ditonton.

2. Sematan di artikel media Pembaca menimbang “nonton videonya atau tidak” sambil membaca teks. Letakkan gambar poster dan transkrip di dekatnya, dan jangan memuat berat video utama sampai benar-benar dibutuhkan. Untuk pembaca yang selesai menonton, tampilkan jalur ke artikel terkait, newsletter, pendaftaran member, atau laporan berbayar.

3. Demo produk SaaS Mengubah bab per fitur, halaman harga, dokumentasi API, dan tombol kontak menyesuaikan posisi tontonan akan mengubah sekadar video menjadi “UI penjelas sebelum meeting penjualan”. Yang penting bukan menonton sampai habis, tapi bab mana yang ditonton dan CTA mana yang diklik.

4. Pelatihan internal dan support Kalau Anda memvideokan onboarding, kepatuhan, atau contoh penanganan inquiry, yang efektif adalah pemutaran stabil di balik SSO, subtitle, log audit, dan panduan saat error. Bukan kemeriahan, melainkan kemampuan mengecek “siapa nonton sampai mana” seperlunya — itulah nilainya.

Kode React/TypeScript siap salin-tempel

Mulai dari sini bagian intinya. Bisa langsung ditaruh di Vite, Next.js, atau React island di Astro. Tidak bergantung pada library player eksternal — hanya <video> native dan event HTMLMediaElement.

Ingat satu titik saja: isPlaying di React tidak ditimpa pada saat tombol ditekan. Kita berlangganan event play/pause lalu menyinkronkan dari state nyata browser. Bug “tombolnya bereaksi tapi videonya nggak jalan” yang saya alami di awal, akarnya persis di sini.

import { useEffect, useRef, useState, type ChangeEvent } from "react";

// Definisi satu track subtitle
type CaptionTrack = {
  src: string;
  srcLang: string;
  label: string;
  default?: boolean;
};

type ProductionVideoPlayerProps = {
  src: string;
  title: string;
  poster?: string;
  captions?: CaptionTrack[];
};

// Merapikan jumlah detik jadi tampilan seperti 1:05
function formatTime(value: number) {
  if (!Number.isFinite(value)) return "0:00";
  const minutes = Math.floor(value / 60);
  const seconds = Math.floor(value % 60).toString().padStart(2, "0");
  return `${minutes}:${seconds}`;
}

export function ProductionVideoPlayer({
  src,
  title,
  poster,
  captions = [],
}: ProductionVideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(0.8);
  const [rate, setRate] = useState(1);
  const [error, setError] = useState("");

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    // Cerminkan state nyata browser ke React (jangan jalan dari tebakan)
    const syncTime = () => setCurrentTime(video.currentTime);
    const syncDuration = () => {
      setDuration(Number.isFinite(video.duration) ? video.duration : 0);
    };
    const syncPlayState = () => setIsPlaying(!video.paused);
    const syncVolume = () => setVolume(video.muted ? 0 : video.volume);

    video.addEventListener("timeupdate", syncTime);
    video.addEventListener("loadedmetadata", syncDuration);
    video.addEventListener("durationchange", syncDuration);
    video.addEventListener("play", syncPlayState);
    video.addEventListener("pause", syncPlayState);
    video.addEventListener("volumechange", syncVolume);

    return () => {
      video.removeEventListener("timeupdate", syncTime);
      video.removeEventListener("loadedmetadata", syncDuration);
      video.removeEventListener("durationchange", syncDuration);
      video.removeEventListener("play", syncPlayState);
      video.removeEventListener("pause", syncPlayState);
      video.removeEventListener("volumechange", syncVolume);
    };
  }, []);

  async function togglePlay() {
    const video = videoRef.current;
    if (!video) return;

    if (video.paused) {
      try {
        // Bisa gagal karena pembatasan autoplay, jadi selalu pakai try/catch
        await video.play();
        setError("");
      } catch {
        setError("Pemutaran diblokir. Tekan play sekali lagi, atau periksa setelan browser.");
      }
    } else {
      video.pause();
    }
  }

  function seek(event: ChangeEvent<HTMLInputElement>) {
    const video = videoRef.current;
    if (!video) return;
    const nextTime = Number(event.target.value);
    video.currentTime = nextTime;
    setCurrentTime(nextTime);
  }

  function changeVolume(event: ChangeEvent<HTMLInputElement>) {
    const video = videoRef.current;
    if (!video) return;
    const nextVolume = Number(event.target.value);
    video.volume = nextVolume;
    video.muted = nextVolume === 0;
    setVolume(nextVolume);
  }

  function changeRate(event: ChangeEvent<HTMLSelectElement>) {
    const video = videoRef.current;
    if (!video) return;
    const nextRate = Number(event.target.value);
    video.playbackRate = nextRate;
    setRate(nextRate);
  }

  function toggleMute() {
    const video = videoRef.current;
    if (!video) return;
    video.muted = !video.muted;
  }

  return (
    <section className="production-video-player" aria-label={`Video player ${title}`}>
      <video ref={videoRef} poster={poster} preload="metadata" playsInline>
        <source src={src} type={src.endsWith(".webm") ? "video/webm" : "video/mp4"} />
        {captions.map((track) => (
          <track
            key={track.src}
            kind="captions"
            src={track.src}
            srcLang={track.srcLang}
            label={track.label}
            default={track.default}
          />
        ))}
        Browser Anda tidak mendukung elemen video.
      </video>

      <div role="group" aria-label="Kontrol video">
        <button type="button" onClick={togglePlay} aria-label={isPlaying ? "Jeda" : "Putar"}>
          {isPlaying ? "Jeda" : "Putar"}
        </button>

        <label>
          <span>Seek</span>
          <input
            type="range"
            min="0"
            max={duration || 0}
            step="0.1"
            value={duration ? currentTime : 0}
            onChange={seek}
            aria-valuetext={`${formatTime(currentTime)} / ${formatTime(duration)}`}
          />
        </label>

        <output>
          {formatTime(currentTime)} / {formatTime(duration)}
        </output>

        <button type="button" onClick={toggleMute} aria-label={volume === 0 ? "Aktifkan suara" : "Bisukan"}>
          {volume === 0 ? "Aktifkan suara" : "Bisukan"}
        </button>

        <label>
          <span>Volume</span>
          <input type="range" min="0" max="1" step="0.05" value={volume} onChange={changeVolume} />
        </label>

        <label>
          <span>Kecepatan</span>
          <select value={rate} onChange={changeRate}>
            {[0.75, 1, 1.25, 1.5, 2].map((speed) => (
              <option key={speed} value={speed}>
                {speed}x
              </option>
            ))}
          </select>
        </label>
      </div>

      {error ? <p role="alert">{error}</p> : null}
    </section>
  );
}

Hanya dengan komponen ini, permintaan “mau nonton 1.25x” dari email pembuka sudah terjawab lewat ubah kecepatan di <select>. Penyimpanan posisi lanjut cukup ditambah beberapa baris: simpan currentTime ke localStorage secara dijarangkan pada timeupdate, lalu tulis balik setelah loadedmetadata. Saran saya, jangan sekaligus serba lengkap dari awal — jadikan ini fondasi lalu pasang fitur satu per satu.

Analitik tonton cukup 7 titik

Yang ingin saya bahas mendalam di sini adalah pengukuran. Pemborosan terbesar di UI video adalah mengirim event tiap detik sampai log meluap, dan ujung-ujungnya tetap tidak tahu di mana orang berhenti. Itu persis kegagalan pertama saya.

Yang perlu dikirim cuma 7 titik: mulai putar, 25%, 50%, 75%, selesai, error, klik CTA. Semua ini dikirim di dalam timeupdate “hanya sekali tepat saat ambang terlewati”.

// Implementasi minimal: kirim event progres sekali saja (tambahkan ke dalam useEffect komponen di atas)
const sent = new Set<string>();

const track = (name: string) => {
  if (sent.has(name)) return; // Cegah pengiriman dobel untuk event yang sama
  sent.add(name);
  // Ganti dengan tujuan kirim sebenarnya (GA4 / API sendiri, dll.)
  window.dispatchEvent(new CustomEvent("video-metric", { detail: { name, src } }));
};

const onTime = () => {
  if (!video.duration) return;
  const ratio = video.currentTime / video.duration;
  if (ratio >= 0.25) track("p25");
  if (ratio >= 0.5) track("p50");
  if (ratio >= 0.75) track("p75");
};

video.addEventListener("play", () => track("start"), { once: true });
video.addEventListener("timeupdate", onTime);
video.addEventListener("ended", () => track("complete"));
video.addEventListener("error", () => track("error"));

Mengelola “sudah terkirim” dengan Set adalah kunci yang tampak sepele. timeupdate menyala beberapa kali per detik, jadi tanpa ini Anda akan mengirim p50 berpuluh kali. Status selesai diambil dari event ended, bukan dari tampilan halaman. Cukup ini saja: untuk layanan belajar, “lesson mana yang rasio penyelesaiannya rendah” jadi terlihat; untuk media, “apakah pembaca lanjut mendaftar setelah menonton” jadi kelihatan.

Kalau ingin mempertajam sudut pandang pengukuran, strategi tes dengan Claude Code menjelaskan cara menurunkan “tidak terkirim dobel” menjadi sebuah tes.

Jebakan aksesibilitas dan performa

Jebakan terbesar adalah menyembunyikan kontrol native tapi tidak mengembalikan operasi yang setara. Tombol play dibuat dari div, seek bar tanpa label, kecepatan tidak bisa diubah lewat keyboard, subtitle tidak ada — sebagus apa pun tampilannya, ini tidak layak dipakai sebagai kursus. Tombol pakai button asli, seek pakai input type="range", dan error disampaikan lewat role="alert". Dasarnya cuma itu. Sudut pandang detail soal operasi keyboard dan subtitle saya rangkum di menjamin aksesibilitas dengan Claude Code.

Dari sisi performa, yang berbahaya adalah merancang daftar artikel atau LP memuat banyak video sekaligus. Beri lebar dan tinggi pada gambar poster, set video tontonan opsional ke preload="metadata", dan sajikan berkas utama dari CDN. Mengirim kursus panjang sebagai satu MP4 raksasa membuat semuanya merugi: jaringan seluler, penonton mancanegara, dan orang yang berhenti di tengah. Kalau berat tampilan awal mengganggu, baca juga mengoptimalkan performa dengan Claude Code. Untuk kasus audio saja, membuat audio player dengan Claude Code punya susunan yang mirip.

Sebelum publikasi, 8 poin ini selalu saya cek setiap kali.

  • Sudah memeriksa keyboard, sentuh, mouse, dan label untuk screen reader
  • Menyiapkan subtitle atau transkrip, tidak mengurung informasi penting hanya di dalam video
  • Di ponsel playsInline berfungsi dan tidak terjadi transisi fullscreen yang tak diinginkan
  • Rasio dan ukuran gambar poster dipatok, tidak menimbulkan CLS
  • Tidak menyematkan preload="auto" pada video yang tak perlu di tampilan awal
  • Sudah menguji URL kedaluwarsa, subtitle hilang, jaringan lambat, dan autoplay terblokir
  • Sudah menentukan nama metrik untuk mulai putar, progres, selesai, error, dan klik CTA
  • Menyiapkan prosedur kembali ke controls native saat kontrol kustom rusak

Pertanyaan umum

T. Video player sebaiknya bikin sendiri atau pakai library? Untuk sematan pendek di dalam artikel, <video controls> sudah cukup. Kalau butuh simpan progres, CTA, penanda bab, dan pengukuran sendiri, bikin sendiri (komponen di atas) lebih cocok. Untuk durasi panjang, katalog besar, dan live, gunakan library berbasis streaming. Pilih berdasarkan kebutuhan — itu jawabannya.

T. Kenapa video.play() tidak memutar? Karena pembatasan autoplay browser. Pemutaran tanpa interaksi pengguna, atau autoplay dengan suara, akan diblokir. Selalu bungkus await video.play() dengan try/catch, dan saat gagal, minta pengguna mengulang operasi atau coba lagi dengan muted.

T. Bagaimana mengimplementasikan posisi lanjut (putar dari tengah)? Cukup simpan currentTime ke localStorage secara dijarangkan pada timeupdate, lalu tulis balik ke video.currentTime setelah loadedmetadata. Di situs publik, sertakan satu kalimat penjelasan privasi tentang apa yang disimpan.

T. Kapan analitik tonton dikirim? Bukan tiap detik. Kirim 7 titik — mulai putar, 25/50/75%, selesai, error, klik CTA — sekali masing-masing sambil mencegah pengiriman dobel dengan Set. Kuncinya, status selesai diambil dari event ended, bukan dari tampilan halaman.

T. Apa kiat menyuruh Claude Code membuat video player? Potong permintaan jadi kecil seperti tabel lapis tadi. Kalau Anda menyerahkan “aksesibilitas seek bar saja” atau “tes pengiriman dobel event complete saja”, perbaikan tidak akan ikut menyeret infrastruktur distribusi. Saat review, tunjukkan dulu 3 hal ini agar cepat: “apakah state disinkronkan dari media event”, “apakah semua operasi bisa lewat keyboard”, “apakah berkas utama tidak dijatuhkan di pemuatan awal”.

Hasil setelah saya coba langsung

Sejak email “mau nonton 1.25x” itu, saya berhenti menganggap video player sebagai masalah “pilih library”. Yang saya lihat tiap kali tetap 3 hal ini: apakah state React disinkronkan dari event play/pause, apakah video.play() dilindungi try/catch, dan apakah pengukuran sudah dipersempit ke 7 titik.

Setelah library saya buang dan saya susun ulang dari <video> native, subtitle yang berantakan hilang, posisi lanjut bisa diimplementasikan beberapa baris saja, dan berkat pengukuran yang menghentikan pengiriman dobel dengan Set, untuk pertama kalinya “di lesson mana banyak orang berhenti” jadi kelihatan. Satu lesson dengan rasio penyelesaian rendah ternyata cuma karena 90 detik pembukanya terlalu panjang; setelah dipotong, penyelesaiannya naik. Daripada mencari library video yang pintar, tambahkan ke elemen native sebanyak yang dibutuhkan saja. Kelihatannya muter-muter, tapi inilah yang paling cepat menurut pengalaman saya sekarang.

Buat Anda yang ingin merancang UI video ini sesuai materi, pelatihan internal, atau media perusahaan, mampir lewat halaman pelatihan dan konsultasi. Kita bisa bahas bareng dari review implementasi sampai desain pengukuran. Buat yang ingin coba dulu sendiri, ada juga daftar materi.

#Claude Code #video player #React #aksesibilitas #analitik tonton
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.