IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA13/32-dars22 daqiqa
Mundarija (31)

Video va audio React'da: pleyer, subtitr, HLS oqim va YouTube embed

Qisqacha: React'da media elementining "haqiqiy" holati — <video>/<audio> ning o'zida. Komponent uni ref orqali boshqaradi (play(), pause(), playbackRate) va holatni hodisalardan o'qiydi (play, pause, timeupdate). Subtitr — <track> va VTT fayl. Uzun video — HLS oqimi: Safari o'zi o'ynaydi, boshqalarda hls.js (+172.6 KB gzip, CSP'da media-src blob:). YouTube — "fasad": avval rasm va tugma, iframe faqat bosilganda (+2.6 KB). Tayyor pleyerlar (react-player +579.1 KB, Vidstack +112.9 KB) vazifalar ning Trusted Types siyosati ostida ochilmadi.

Bu darsda

  • Media elementini ref bilan boshqarasiz va holatni hodisalardan oladigan hook yozasiz.
  • React'dagi media atributlari tuzog'ini (muted) va <track> subtitrini bilasiz, VTT faylni koddan yasaysiz.
  • HLS oqimi qanday ishlashini tushuntirasiz va hls.js'ni effekt ichida to'g'ri ulaysiz.
  • YouTube'ni fasad bilan joylaysiz va Media Session API bilan telefon qulf ekranidagi tugmalarni ulaysiz.
  • Tayyor pleyerlarni hajm va xavfsizlik siyosati natijasi bo'yicha solishtirasiz.

Oldin bilishingiz kerak: Audio va videoni JS'dan boshqarish, video va audio, Subtitrlar va media accessibility, useRef II: DOM elementlari, useEffect I.

1. Nega bu kerak?

Oldingi darsda rasmlar bilan ishladik. «Bahor» endi oshpazlik maktabi ochmoqchi: Jasur aka «Osh damlash» kabi retsept videolarini yozdi, Malika uchun esa audio-ma'ruzalar ("Bahor hikoyalari") ham bo'ladi. Talablar:

  • o'z tugmalarimiz, tezlik (1.5× — ma'ruzani tezroq tinglash), subtitr;
  • 40 daqiqalik video sekin internetda ham to'xtab qolmasin;
  • telefonni qulflasa ham audio davom etsin va qulf ekranida «Pauza» ishlasin;
  • ba'zi videolar YouTube'da — sahifani og'irlashtirmasdan qo'yish.

Audio va videoni JS'dan boshqarish darsida bularning ko'pini oddiy JavaScript bilan yozgansiz: play() Promise qaytaradi, timeupdate, autoplay siyosati, <track>. Bu dars ularni React komponentiga o'tkazadi va uchta yangi narsa qo'shadi: HLS oqimi, YouTube fasadi va Media Session.

Hayotiy o'xshatish: televizor va pult. Kanal, ovoz, "hozir nima ko'rsatilyapti" — televizorning o'zida. Pult faqat buyruq beradi va ekrandagi holatni ko'radi. React komponenti — pult. Media elementi — televizor.

2. Media elementi React'da

2.1 Holat qayerda?

React'da odatda "haqiqat state'da" bo'ladi: useState → JSX → ekran. Media bilan bunday emas. Video to'xtashining sabablari ko'p: foydalanuvchi bosdi, internet uzildi, video tugadi, telefonga qo'ng'iroq keldi, boshqa tab ovoz chiqardi. Ularning ko'pchiligi bizning kodimizsiz yuz beradi.

Shuning uchun qoida:

  • Buyruq — ref orqali: ref.current.play(), ref.current.pause(), ref.current.playbackRate = 1.5.
  • Holat — media hodisalaridan: play, pause, timeupdate, ratechange, ended. Ular kelganda elementning o'zidan o'qib, state'ga yozamiz.

Bu — useEffect I darsidagi g'oya: media elementi — tashqi tizim, effekt unga obuna bo'ladi. "Bosganda setPlaying(true)" deb yozish xato: play() rad etilishi mumkin (autoplay siyosati), tugma esa "Pauza" deb yolg'on gapiradi. Audio va videoni JS'dan boshqarish darsidagi "Tugma holatini qo'lda yuritish" xatosi React'da ham xuddi shunday.

2.2 JSX atributlari va muted tuzog'i

Media atributlari JSX'da camelCase: autoPlay, playsInline, crossOrigin, srcLang (<track> da). Bitta tuzoq bor — buni tekshiruvchimiz ham, Chrome ham bir xil ko'rsatadi:

jsx
export default function App() {
  return (
    <video
      muted
      autoPlay
      playsInline
      controls
      ref={(el) => el && console.log(el.outerHTML, el.muted)}
    />
  );
}

Konsolda:

text
<video autoplay="" playsinline="" controls=""></video> true

HTML tegida muted atributi yo'q, lekin xususiyat (el.muted) — true. React muted ni atribut qilib yozmaydi, faqat DOM xususiyatini o'rnatadi. Brauzerda ishlayotgan ilovada farqi yo'q: autoplay siyosati xususiyatni tekshiradi va ovozsiz video avtomatik boshlanadi. Lekin HTML'ni serverda tayyorlaydigan freymvorklarda (23-qismda — Next.js) bu muhim: React yuklanguncha brauzer muted siz tegni ko'radi va autoplay'ni to'sadi. Shuning uchun ba'zi loyihalarda autoplay video ref orqali, effektda ishga tushiriladi.

Tekshirib ko'ring: Nega ref={(el) => el && …} da el && bor?

Javob

Callback ref ikki marta chaqiriladi. Element DOM'ga qo'shilganda el — element, olib tashlanganda — null. null.outerHTML — TypeError. el && ikkinchi chaqiruvni o'tkazib yuboradi. useRef II darsida React 19 ning callback ref'dan tozalash funksiyasi qaytarish imkoniyatini ko'rgansiz — u ham shu muammoni hal qiladi.

2.3 useMediaState: holatni hodisalardan olish

Endi qoidani hook'ka aylantiramiz va kichik audio pleyer yasaymiz. Natija oynasida tashqi fayl yo'q — makeTone yordamchisi 2 soniyalik tovushni WAV Blob qilib yasaydi (13-qismdagi makeWav kabi; ichini tushunish shart emas). Bu blok faqat saytda ishlaydi: tekshiruvchimiz (jsdom) media o'ynatmaydi. Biz uni Chrome 155 da sinadik — «Ijro», bir soniyadan keyin «1.5×», keyin «Pauza»:

jsx
import { useEffect, useRef, useState } from "react";

// Qora quti: sinus tovush — WAV Blob (8 kHz, 8 bit)
function makeTone(seconds, freq) {
  const rate = 8000;
  const n = rate * seconds;
  const v = new DataView(new ArrayBuffer(44 + n));
  const text = (o, s) =>
    [...s].forEach((c, i) => v.setUint8(o + i, c.charCodeAt(0)));
  text(0, "RIFF");
  v.setUint32(4, 36 + n, true);
  text(8, "WAVEfmt ");
  v.setUint32(16, 16, true);
  v.setUint16(20, 1, true);
  v.setUint16(22, 1, true);
  v.setUint32(24, rate, true);
  v.setUint32(28, rate, true);
  v.setUint16(32, 1, true);
  v.setUint16(34, 8, true);
  text(36, "data");
  v.setUint32(40, n, true);
  for (let i = 0; i < n; i++) {
    const wave = Math.sin((2 * Math.PI * freq * i) / rate);
    v.setUint8(44 + i, 128 + Math.round(60 * wave));
  }
  return new Blob([v.buffer], { type: "audio/wav" });
}

const EVENTS = [
  "loadedmetadata",
  "play",
  "pause",
  "timeupdate",
  "ratechange",
  "ended",
];

function useMediaState(ref) {
  const [state, setState] = useState({
    paused: true,
    time: 0,
    duration: 0,
    rate: 1,
  });
  useEffect(() => {
    const media = ref.current;
    const read = (event) => {
      if (event.type !== "timeupdate") console.log(event.type);
      setState({
        paused: media.paused,
        time: media.currentTime,
        duration: media.duration || 0,
        rate: media.playbackRate,
      });
    };
    for (const e of EVENTS) media.addEventListener(e, read);
    return () => {
      for (const e of EVENTS) media.removeEventListener(e, read);
    };
  }, [ref]);
  return state;
}

const clock = (s) => `0:${String(Math.floor(s)).padStart(2, "0")}`;

export default function App() {
  const audio = useRef(null);
  const [src] = useState(() => URL.createObjectURL(makeTone(2, 440)));
  const { paused, time, duration, rate } = useMediaState(audio);

  function toggle() {
    if (paused) audio.current.play();
    else audio.current.pause();
  }

  return (
    <div>
      <audio ref={audio} src={src} preload="metadata" />
      <button onClick={toggle}>
        {paused ? "Ijro" : "Pauza"}
      </button>{" "}
      <button
        onClick={() => {
          audio.current.playbackRate = rate === 1 ? 1.5 : 1;
        }}
      >
        {rate === 1 ? "1.5×" : "1×"}
      </button>
      <p>
        {clock(time)} / {clock(duration)}
      </p>
    </div>
  );
}

Chrome 155 da (saytdagi natija oynasi hujjati — o'sha React, headless) konsol:

text
loadedmetadata
play
ratechange
pause

Ekranda pauzadan keyin «0:01 / 0:02» chiqdi (soniya — taxminiy, vaqtga bog'liq). Har qadamda nima bo'lganini jadvalda ko'ring — tugma matni bosishdan emas, keyingi hodisadan o'zgaradi:

Qadam Kelgan hodisa paused / rate Ekranda
sahifa ochildi loadedmetadata true / 1 «Ijro», «1.5×», «0:00 / 0:02»
«Ijro» bosildi play false / 1 «Pauza», vaqt yura boshlaydi
«1.5×» bosildi ratechange false / 1.5 «1×»
«Pauza» bosildi pause true / 1.5 «Ijro», «0:01 / 0:02»

Kodni tahlil qilamiz:

  • Tugma matni — paused dan, paused esa play/pause hodisasidan. Biz hech qayerda setPaused yozmadik. Agar play() rad etilsa, play hodisasi kelmaydi va tugma «Ijro» bo'lib qoladi — haqiqatga mos.
  • Tezlik — playbackRate ga yozamiz, tugma matni esa ratechange dan keyin o'zgaradi. Konsoldagi uchinchi qator — shu hodisa.
  • duration || 0 — metama'lumot kelguncha duration NaN bo'ladi. NaN ni chiqarish o'rniga 0.
  • Tozalash — effekt qaytargan funksiya hamma tinglovchilarni olib tashlaydi. Komponent yopilsa yoki ref boshqa elementga o'tsa, eski elementga obuna qolmaydi.

timeupdate sekundiga 4–66 marta keladi (brauzerga bog'liq). Har safar butun komponent qayta chiziladi. Kichik pleyerda bu muammo emas. Murakkab sahifada vaqtni alohida kichik komponentga ajrating (Kompozitsiya orqali optimallashtirish) — qolgan qism har soniya qayta chizilmaydi.

Maslahat: useSyncExternalStore (useSyncExternalStore) ham shu ishni qila oladi: media elementi — tashqi manba, subscribe — hodisalarga obuna, getSnapshot — media.paused. Lekin snapshot oddiy qiymat bo'lishi kerak: bir nechta maydon uchun har biriga alohida hook yoki keshlangan obyekt kerak bo'ladi. useEffect + useState varianti soddaroq.

3. Subtitr: <track> va VTT

3.1 VTT fayl koddan

Subtitr — Malika va ovozsiz tomosha qiladiganlar uchun (Subtitrlar va media accessibility). Format — WebVTT: WEBVTT sarlavhasi, keyin har bir cue (bitta subtitr yozuvi): vaqt oralig'i va matn. Ko'pincha subtitrlar bazada ma'lumot bo'lib saqlanadi va fayl koddan yasaladi:

js
function vttTime(sec) {
  const ms = Math.round(sec * 1000);
  const pad = (n, len = 2) => String(n).padStart(len, "0");
  const h = pad(Math.floor(ms / 3600000));
  const m = pad(Math.floor(ms / 60000) % 60);
  const s = pad(Math.floor(ms / 1000) % 60);
  return `${h}:${m}:${s}.${pad(ms % 1000, 3)}`;
}

function toVtt(cues) {
  const blocks = cues.map(
    (c) => `${vttTime(c.start)} --> ${vttTime(c.end)}\n${c.text}`,
  );
  return ["WEBVTT", ...blocks].join("\n\n") + "\n";
}

console.log(
  toVtt([
    { start: 0, end: 3.5, text: "Avval sabzini to'g'raymiz." },
    { start: 3.5, end: 65, text: "Go'shtni qizartiramiz." },
  ]),
);

Konsolda:

text
WEBVTT

00:00:00.000 --> 00:00:03.500
Avval sabzini to'g'raymiz.

00:00:03.500 --> 00:01:05.000
Go'shtni qizartiramiz.

Vaqt formati qat'iy: soat, daqiqa, soniya va nuqtadan keyin millisekund (00:01:05.000). SRT formatida esa vergul (00:01:05,000) — boshqa formatdan ko'chirganda eng ko'p uchraydigan xato. Cue'lar orasida bo'sh qator shart.

3.2 React'da <track>

Yasalgan matn — Blob → obyekt URL → <track src>:

jsx
const [trackUrl] = useState(() =>
  URL.createObjectURL(new Blob([toVtt(cues)], { type: "text/vtt" })),
);
useEffect(() => () => URL.revokeObjectURL(trackUrl), [trackUrl]);

<video src={video} controls crossOrigin="anonymous">
  <track
    kind="subtitles"
    src={trackUrl}
    srcLang="uz"
    label="O'zbekcha"
    default
  />
</video>;

Ikki tuzoq. Birinchisi — srcLang (camelCase), srclang deb yozsangiz React ogohlantiradi. Ikkinchisi — CORS: video boshqa domendan (CDN) kelsa, VTT fayl ham boshqa domendan bo'lishi mumkin. Brauzer subtitrni CORS qoidasi bilan yuklaydi: <video crossOrigin="anonymous"> va server Access-Control-Allow-Origin javobi bo'lmasa, subtitr jim ko'rinmaydi.

4. HLS: uzun video oqimi

4.1 Bitta katta fayl emas — bo'laklar

40 daqiqalik videoni bitta osh.mp4 qilib bersangiz, ikki muammo: sekin internetda brauzer kerakli qismni yuklay olmay to'xtaydi, tez internetda esa past sifat ko'rinadi. HLS (HTTP Live Streaming) buni boshqacha hal qiladi: video bir necha sifatda tayyorlanadi va har biri 2–6 soniyalik bo'laklarga (segment) bo'linadi. Ro'yxat fayli — .m3u8 pleylist:

flowchart TB
  P["Pleyer"] --> M["master.m3u8 — sifatlar ro'yxati"]
  M --> L["360p.m3u8"]
  M --> H["1080p.m3u8"]
  L --> S1["bo'lak 1, 2, 3 … (.ts / .m4s)"]
  H --> S2["bo'lak 1, 2, 3 …"]
  P -. "internet tezligini o'lchaydi" .-> Q{"Qaysi sifat?"}
  Q -- sekin --> L
  Q -- tez --> H

Diagrammaga qarang: pleyer avval sifatlar ro'yxatini oladi, keyin bo'laklarni birin-ketin yuklaydi. Har bo'lakdan keyin tezlikni o'lchab, keyingisini qaysi sifatda olishni tanlaydi. Bu — moslashuvchan bitreyt (adaptive bitrate): internet sekinlashsa, video to'xtamaydi, sifati pasayadi. YouTube, kurs platformalari, onlayn TV — hammasi shunday (HLS yoki unga o'xshash DASH).

4.2 Kim o'ynaydi: brauzermi, hls.js mi

Safari (macOS va iOS) HLS'ni o'zi o'ynaydi: <video src="master.m3u8"> yetadi. Boshqa brauzerlarda tarixan hls.js kerak edi. U pleylistni o'qiydi, bo'laklarni fetch qiladi va Media Source Extensions (MSE) orqali <video> ga beradi. MSE — JavaScript'ga video oqimini bo'lak-bo'lak "ovqatlantirish" imkonini beradigan brauzer API'si.

Qaysi biri kerakligini imkoniyatni tekshirish (feature detection) bilan aniqlaymiz: video.canPlayType("application/vnd.apple.mpegurl"). Bu tekshiruv bizda qiziq natija berdi: jsdom'da "" (yo'q), headless Chrome 155 da esa "maybe". Biz Chrome'da haqiqiy HLS oqimini o'ynatib ko'rmadik — shuning uchun hls.js hujjatidagi tartibni saqlang: avval canPlayType, bo'lmasa Hls.isSupported(), va ikkala yo'lni ham haqiqiy brauzerlarda sinang.

4.3 hls.js React effektida

hls.js obyekt yaratadi, tarmoqqa ulanadi va xotira oladi. Demak, u — tashqi tizim, ulash effektda, tozalash shart (kod hujjat bo'yicha, natija oynasida hls.js yo'q):

jsx
import { useEffect, useRef } from "react";
import Hls from "hls.js";

export function HlsVideo({ src }) {
  const ref = useRef(null);
  useEffect(() => {
    const video = ref.current;
    if (video.canPlayType("application/vnd.apple.mpegurl")) {
      video.src = src; // Safari: o'zi o'ynaydi
      return;
    }
    if (!Hls.isSupported()) return;
    const hls = new Hls();
    hls.loadSource(src);
    hls.attachMedia(video);
    hls.on(Hls.Events.ERROR, (_, data) => {
      if (data.fatal) console.error("HLS:", data.type);
    });
    // tozalash: tarmoq, bufer, MSE — hammasi bo'shaydi
    return () => hls.destroy();
  }, [src]);
  return <video ref={ref} controls playsInline />;
}

Uch nuqta:

  • destroy() — tozalash funksiyasida. Dev rejimidagi StrictMode effektni ikki marta ishga tushiradi: tozalash bo'lmasa, ikkita hls.js bitta videoni yuklab, bir-birini buzadi. src o'zgarganda ham eski nusxa yopiladi.
  • Kech yuklash — hls.js kanon tajribasida +172.6 KB gzip. Uni hamma sahifaga qo'shmang: video sahifasida lazy bilan.
  • CSP — MSE videoga blob: manzil beradi. vazifalar CSP'sida media-src yo'q (u default-src 'self' dan oladi) va tajribada haqiqiy buzilish chiqdi: Loading media from 'blob:http://127.0.0.1:5870/b3f5…' violates the fo… (xom faylda shunday kesilgan). Tarjimasi: "blob: dan media yuklash siyosatni buzadi". Davosi — media-src 'self' blob:.

Tekshirib ko'ring: useEffect ga [src] o'rniga [] yozildi. Foydalanuvchi boshqa darsni tanladi — src o'zgardi. Nima bo'ladi?

Javob

Effekt qayta ishlamaydi — hls.js eski pleylistda qoladi. Ekranda yangi dars sarlavhasi, videoda esa eski dars. Bundan tashqari, ESLint'ning react-hooks/exhaustive-deps qoidasi src yetishmayotganini aytadi (useEffect II). [src] bilan: avval eski nusxaning destroy(), keyin yangi Hls yangi manzil bilan.

5. YouTube va fasad naqshi

5.1 Iframe narxi

YouTube videosini qo'yishning eng oddiy yo'li — <iframe src="https://www.youtube.com/embed/…">. Lekin iframe sahifa ochilishi bilan YouTube pleyerining o'z JavaScript'ini, shriftlarini va kuzatuv skriptlarini yuklaydi — foydalanuvchi videoni bosmasa ham. Galereyada 10 ta video bo'lsa — 10 ta pleyer.

Fasad (facade) naqshi: avval faqat videoning rasmi va ▶ tugmasi. Haqiqiy iframe — faqat bosilganda. Do'kon vitrinasidagi televizor maketi kabi: haqiqiy televizorni faqat xaridor so'rasa yoqasiz.

5.2 O'z fasadimiz

jsx
import { useState } from "react";

const thumb = (id) => `https://i.ytimg.com/vi/${id}/hqdefault.jpg`;

function YouTubeFacade({ id, title }) {
  const [active, setActive] = useState(false);
  if (active) {
    const host = "https://www.youtube-nocookie.com";
    const src = `${host}/embed/${id}?autoplay=1`;
    console.log("iframe:", src);
    return (
      <iframe
        src={src}
        title={title}
        width={320}
        height={180}
        allow="autoplay; encrypted-media; picture-in-picture"
        allowFullScreen
      />
    );
  }
  return (
    <button
      aria-label={`Videoni ko'rish: ${title}`}
      onClick={() => setActive(true)}
      style={{
        width: 320,
        height: 180,
        background: `#000 url(${thumb(id)})`,
        backgroundSize: "cover",
        color: "white",
      }}
    >
      ▶ {title}
    </button>
  );
}

export default function App() {
  return <YouTubeFacade id="VIDEO_ID" title="Osh damlash" />;
}

Konsolda:

text
iframe: https://www.youtube-nocookie.com/embed/VIDEO_ID?autoplay=1

VIDEO_ID o'rniga videoning 11 belgili identifikatori qo'yiladi. Natija oynasida tugma bosilgach, iframe ochilmaydi — va bu darsning foydali qismi. Saytimizning xavfsizlik siyosatida (CSP) YouTube'ga ruxsat yo'q, natija oynasi esa uni meros oladi. Sizning loyihangizda ham xuddi shunday bo'ladi. Fasad ishlashi uchun CSP'ga ikki joy qo'shiladi:

  • img-src https://i.ytimg.com — oldindan ko'rish rasmi;
  • frame-src https://www.youtube-nocookie.com — bosilgandan keyingi pleyer.

youtube-nocookie.com — YouTube'ning "kengaytirilgan maxfiylik" domeni: video ijro etilmaguncha kuzatuv cookie'larini yozmaslikka va'da beradi. ?autoplay=1 — foydalanuvchi allaqachon bosdi, ikkinchi bosish kerak emas. aria-label — tugmada faqat ▶ belgisi va nom, Malikaga esa "nima bo'ladi" ham kerak.

5.3 react-lite-youtube-embed

Tayyor fasad — react-lite-youtube-embed: <LiteYouTubeEmbed id="…" title="…" />. Kanon tajribasida (3.7.0) +2.6 KB JS va +1.3 KB CSS. vazifalar CSP'si ostidagi haqiqiy natija:

text
embed-youtube-lite     JS   70426 (+   2.6 KB) CSS   1308 | CSP img-src×4,frame-src×1 | xato: Loading the image 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg' violates the following Content  ; Loading the image 'https://i.ytimg.com/vi/aqz-KE-bpKQ/hqdefault.jpg' violates the following Content  | ochiq: tashqi fonts.gstatic.com,i.ytimg.com,www.youtube-nocookie.com

To'rtta img-src (rasm va kutubxonaning data: ikonkalari) va bitta frame-src buzilishi — yuqoridagi ro'yxat bilan bir xil. CSP'siz ochilganda brauzer uchta tashqi hostga bordi. Har biri — foydalanuvchining IP manzili va brauzer ma'lumoti begona serverga ketishi. Bu mavzu Uchinchi tomon skriptlar va embedlar darsida davom etadi.

6. Media Session: qulf ekranidagi tugmalar

Telefon qulflanganda yoki quloqchin tugmasi bosilganda brauzer kimga murojaat qiladi? Media Session API shu savolga javob: navigator.mediaSession ga nom, muallif va rasm, hamda "ijro", "pauza", "keyingi" bosilganda nima qilish kerakligi beriladi. Kod hujjat bo'yicha (natija oynasida tizim pleyeri yo'q):

jsx
useEffect(() => {
  if (!("mediaSession" in navigator)) return;
  const media = audio.current;
  navigator.mediaSession.metadata = new MediaMetadata({
    title: "Osh damlash",
    artist: "«Bahor» oshpazlik maktabi",
    artwork: [{ src: "/rasm/osh-512.png", sizes: "512x512" }],
  });
  const actions = {
    play: () => media.play(),
    pause: () => media.pause(),
    seekbackward: () => (media.currentTime -= 10),
    seekforward: () => (media.currentTime += 10),
  };
  for (const [name, fn] of Object.entries(actions)) {
    navigator.mediaSession.setActionHandler(name, fn);
  }
  return () => {
    for (const name of Object.keys(actions)) {
      navigator.mediaSession.setActionHandler(name, null);
    }
  };
}, []);
  • metadata — qulf ekranida va bildirishnomada ko'rinadigan nom, muallif, rasm.
  • setActionHandler — tizim tugmalari. Berilmagan amal tugmasi ko'rinmaydi. Tozalashda null — boshqa sahifaga o'tilganda eski pleyer tugmalari ishlamay qolsin.
  • "mediaSession" in navigator — imkoniyat tekshiruvi. Chrome 155 da true (tekshirdik), jsdom'da — yo'q.

Bu API ishlashi uchun media haqiqatan ijro etilayotgan bo'lishi kerak: tizim "hozir ovoz chiqarayotgan" tabni tanlaydi.

7. Tayyor pleyerlar va biz sinab ko'rgan narsa

7.1 Uchta kutubxona

Kutubxona Nima uchun Kanon tajribasida
react-player 3.4.0 YouTube, Vimeo, fayl — bitta <ReactPlayer src> +579.1 KB, Trusted Types: to'sildi
Vidstack 1.15.7 to'liq pleyer UI, HLS, subtitr +112.9 KB (+10.3 CSS), Trusted Types: to'sildi
hls.js 1.7.3 faqat HLS → <video> +172.6 KB, media-src blob: kerak

react-player'ning v3 dagi asosiy prop'i src (eski versiyalarda — url):

jsx
import ReactPlayer from "react-player";

<ReactPlayer
  src="https://www.youtube.com/watch?v=VIDEO_ID"
  controls
/>;

Qulay — bitta komponent ko'p manbani o'ynaydi. Hujjatga ko'ra uning light prop'i fasadni ham beradi: avval faqat rasm, pleyer — bosilganda. Lekin narxi og'ir. Tajribadagi haqiqiy xato:

text
This document requires 'TrustedHTML' assignment. The action has been blocked.
Failed to set the 'innerHTML' property on 'ShadowRoot': This document requires 'TrustedHTML' assignm

Tarjimasi: "Hujjat TrustedHTML talab qiladi, amal to'sildi"; "ShadowRoot'ning innerHTML iga yozib bo'lmadi". Pleyer o'z ichki qismlarini Shadow DOM ga satr sifatida yozadi — Trusted Types buni to'sadi. Vidstack ham xuddi shu sababli to'sildi.

Vidstack haqida yana bir halol fakt: o'rnatishda npm shu ogohlantirishni chiqardi (kutubxonalar-ornatish.txt):

text
npm warn deprecated @vidstack/react@1.15.7: Vidstack is deprecated and gets security fixes only until January 2028. Use Video.js 10: https://videojs.org/docs/framework/react/guides/migrate-from-vidstack?utm_source=vidstack (coding agents: run npx @videojs/cli agents skills, then follow the guide's AI Quickstart)

Tarjimasi: "Vidstack eskirgan, xavfsizlik tuzatishlari faqat 2028-yil yanvarigacha. Video.js 10 ga o'ting". Ya'ni yangi loyihada Vidstack emas, Video.js 10. Uni biz o'lchamadik. Bu — kutubxona tanlashdan oldin npm view va ogohlantirishlarni o'qishning sababi (npm bilan ishlash).

7.2 Hajm

Video kutubxonalari: JS (gzip, React'dan tashqari)
  • react-lite-youtube-embed 3.7.0+1.3 KB CSS2,6 KB
  • Vidstack 1.15.7+10.3 KB CSS, TT to'sdi112,9 KB
  • hls.js 1.7.3media-src blob: kerak172,6 KB
  • react-player 3.4.0TT to'sdi579,1 KB

Manba: O'lchandi: kanon tajribasi, Vite 8.3.3, gzip -9, Chrome 155, natijalar/tajriba/kutubxonalar-21.txt, 2026-10-17

Taqqoslash uchun: React va react-dom'ning o'zi — 66.1 KB. react-player React'dan 8 baravar og'ir, chunki u ko'p xizmatning pleyerlarini o'z ichiga oladi. Bitta YouTube video uchun fasad — 2.6 KB.

7.3 Nega vazifalar da media yo'q

vazifalar — vazifalar ro'yxati: unda video ham, audio ham yo'q. Shuning uchun bu darsda kanon kodi o'zgarmaydi. Kanon kutubxonalarni alohida tajribada o'lchadi. Media kerak bo'lgan loyihada tanlov tayyor: o'z <video> + useMediaState, uzun video — hls.js (kech yuklanadi, CSP'da media-src blob:), YouTube — fasad.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
tugma «Pauza», video esa to'xtagan holat bosishda qo'lda yozilgan holat — play/pause hodisalaridan
vaqt NaN:NaN duration metama'lumotdan oldin loadedmetadata dan keyin, `
ikkita ovoz, video "sakraydi" hls.js effektida destroy() yo'q tozalash funksiyasi
subtitr ko'rinmaydi, xato yo'q VTT boshqa domendan, CORS yo'q crossOrigin + server sarlavhasi
subtitr fayli ochilmaydi SRT vaqti 00:00:05,000 VTT'da nuqta: 00:00:05.000
violates … media-src MSE blob: manzili media-src 'self' blob:
YouTube oynasi bo'sh CSP'da frame-src yo'q frame-src https://www.youtube-nocookie.com
requires 'TrustedHTML' pleyer Shadow DOM'ga innerHTML boshqa pleyer yoki o'z <video>

9. Mashqlar

Mashqlar kurs/mashqlar/21/13-video-audio/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Media holatini React state'ga yozish uchun tinglanadigan hodisa (ijro boshlandi):
  • VTT'da millisekund oldidagi belgi:
  • hls.js effektining tozalash funksiyasida chaqiriladigan metod: hls.()
  • YouTube'ning maxfiyroq embed domeni: www..com
  • MSE ishlatadigan pleyer uchun CSP direktivasi: media-src 'self'
Yechim
  • play — play() chaqirilgani emas, ijro haqiqatan boshlangani haqidagi hodisa.
  • nuqta — 00:00:03.500; vergul — SRT formati.
  • destroy — tarmoq so'rovlari, bufer va MSE bo'shaydi; StrictMode'da ikki nusxa bo'lmaydi.
  • youtube-nocookie — ijrodan oldin kuzatuv cookie'lari yozilmaydi.
  • blob: — MSE videoga blob: manzil beradi.

2-mashq (o'rta): bosqichlar ro'yxati

Retsept videosining bosqichlari bor. chapters massividan ro'yxat chizing, hozirgi bosqich aria-current="step" va «▶» belgisi bilan ajralsin. Hozirgi vaqt — time state'i (haqiqiy pleyerda u timeupdate dan keladi). Sinov uchun «+30 s» tugmasi vaqtni 30 soniya oldinga suradi. Uch marta bosilgach (90 s) — «Go'sht» bosqichi faol.

Ishora: faol bosqich — start qiymati time dan kichik yoki teng bo'lganlarning oxirgisi: chapters.findLast((c) => c.start <= time) (Massivda qidirish va tekshirish).

Yechim
jsx
import { useState } from "react";

const chapters = [
  { start: 0, title: "Sabzi" },
  { start: 45, title: "Go'sht" },
  { start: 120, title: "Guruch" },
];

export default function App() {
  const [time, setTime] = useState(0);
  const current = chapters.findLast((c) => c.start <= time);
  return (
    <>
      <button onClick={() => setTime((t) => t + 30)}>+30 s</button>
      <p>Vaqt: {time} s</p>
      <ol>
        {chapters.map((c) => (
          <li
            key={c.start}
            aria-current={c === current ? "step" : undefined}
          >
            {c === current ? "▶ " : ""}
            {c.title}
          </li>
        ))}
      </ol>
    </>
  );
}

Ekranda:

text
+30 s
Vaqt: 90 s
Sabzi
▶ Go'sht
Guruch

findLast — oxiridan qidiradi: 90 dan kichik start lar — 0 va 45, oxirgisi — 45. aria-current={undefined} — atribut umuman chiqmaydi. Haqiqiy pleyerda bosqichni bosganda media.currentTime = c.start (seek) — 13-qismdagi pleyer kabi.

3-mashq (qiyin): VTT'ni tekshiruvchi

Foydalanuvchi subtitr faylini yuklaydi. checkVtt(text) funksiyasini yozing — u xatolar massivini qaytarsin (bo'sh massiv — fayl to'g'ri):

  1. Birinchi qator WEBVTT bilan boshlanmasa — "WEBVTT sarlavhasi yo'q".
  2. Har vaqt qatori (--> bor qator) 00:00:00.000 --> 00:00:00.000 ko'rinishida bo'lsin; vergulli vaqt bo'lsa — "N-qator: vergul emas, nuqta", boshqa xato — "N-qator: vaqt noto'g'ri" (N — 1 dan boshlab).
  3. Boshlanish oxiridan kichik bo'lsin, aks holda — "N-qator: oxiri boshidan oldin".

Ishora: satrni qatorlarga — text.split("\n"); vaqt — muntazam ifoda /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/ (guruhlar); soniyaga aylantirish — h * 3600 + m * 60 + s + ms / 1000.

Yechim
js
const TIME = /^(\d{2}):(\d{2}):(\d{2})\.(\d{3})$/;

function toSeconds(t) {
  const m = t.match(TIME);
  if (!m) return null;
  const [, h, min, s, ms] = m.map(Number);
  return h * 3600 + min * 60 + s + ms / 1000;
}

function checkVtt(text) {
  const lines = text.split("\n");
  const errors = [];
  if (!lines[0].startsWith("WEBVTT")) {
    errors.push("WEBVTT sarlavhasi yo'q");
  }
  lines.forEach((line, i) => {
    if (!line.includes("-->")) return;
    const n = i + 1;
    const [from, to] = line.split("-->").map((p) => p.trim());
    if (from.includes(",") || to.includes(",")) {
      errors.push(`${n}-qator: vergul emas, nuqta`);
      return;
    }
    const a = toSeconds(from);
    const b = toSeconds(to);
    if (a === null || b === null) {
      errors.push(`${n}-qator: vaqt noto'g'ri`);
    } else if (a >= b) {
      errors.push(`${n}-qator: oxiri boshidan oldin`);
    }
  });
  return errors;
}

const good = "WEBVTT\n\n00:00:00.000 --> 00:00:03.500\nSabzi";
const bad = [
  "1",
  "00:00:01,000 --> 00:00:02,000",
  "Salom",
  "",
  "00:00:09.000 --> 00:00:05.000",
  "Xayr",
  "",
  "0:00:10.000 --> 00:00:12.000",
].join("\n");

console.log(checkVtt(good));
console.log(checkVtt(bad));

Konsolda:

text
[]
[
  "WEBVTT sarlavhasi yo'q",
  '2-qator: vergul emas, nuqta',
  '5-qator: oxiri boshidan oldin',
  "8-qator: vaqt noto'g'ri"
]

Xatolar qator raqami bilan — foydalanuvchi faylni tuzatishi oson (Fayl yuklash UI dagi qoida: xabar aniq va foydali). m.map(Number) birinchi elementni ham (butun moslik) songa aylantiradi — destrukturizatsiyada uni , bilan tashlab ketamiz. Node satrlarni apostrof bo'lsa qo'shtirnoqda, bo'lmasa bittalik tirnoqda chiqaradi — ma'nosi bir xil.

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. Alohida Vite + React loyihada «Bahor» retsept sahifasini yasang: HlsVideo komponenti lazy bilan, fasadli YouTube video, vazifalar dagi CSP <meta> i. Ochiq test oqimi sifatida hls.js hujjatidagi namunaviy .m3u8 manzillaridan birini oling. Konsolni kuzating: qaysi direktivani qo'shish kerak bo'ldi?

Yechim

Tekshiruv ro'yxati:

  • npm i -E hls.js@1.7.3 — build'da hls.js alohida chunk'da (Vite oxirida gzip hajmini ko'rsatadi; kanonda ~172 KB).
  • CSP bilan birinchi urinish: media-src buzilishi (blob:). Qo'shish: media-src 'self' blob:. Tashqi oqim uchun connect-src ga uning hostini ham qo'shasiz — hls.js bo'laklarni fetch bilan oladi.
  • YouTube fasadi: img-src https://i.ytimg.com, frame-src https://www.youtube-nocookie.com.
  • DevTools → Network: bo'laklar (.ts yoki .m4s) birin-ketin keladi; tarmoqni "Slow 4G" ga qo'ysangiz, sifat pasayadi.
  • Sahifadan chiqib qayting: konsolda ikkinchi hls.js nusxasi qolmagan (tozalash ishlayapti).

10. Real ishda

  • Kurs platformalari va media saytlar — HLS (yoki DASH) oqimi, o'z pleyer tugmalari, tezlik, subtitr, "qayerda to'xtadingiz". Videoni HLS'ga aylantirish — serverda (FFmpeg) yoki bulut xizmatida.
  • Marketing sahifalari — YouTube/Vimeo fasad bilan: Lighthouse'dagi "uchinchi tomon kodi" ogohlantirishini kamaytiradi.
  • Podkast va musiqa — Media Session: qulf ekrani, quloqchin tugmalari, avtomobil ekrani.
  • Intervyu savollari: "React'da pleyer holatini qayerda saqlaysiz?", "HLS nima va adaptive bitrate qanday ishlaydi?", "YouTube embed sahifani qanday sekinlashtiradi?", "hls.js'ni effektda qanday ulaysiz?".

Xulosa

  • Media holati elementning o'zida: buyruq — ref orqali, holat — hodisalardan (useMediaState).
  • React muted ni atribut qilib yozmaydi — server HTML'ida autoplay uchun tuzoq.
  • Subtitr — <track srcLang> va WebVTT (nuqtali millisekund); boshqa domendan — CORS bilan.
  • HLS — sifatlar va bo'laklar pleylisti; Safari o'zi, boshqalar — hls.js (MSE), effektda destroy(), CSP'da media-src blob:.
  • YouTube — fasad: rasm + tugma, iframe bosilganda (youtube-nocookie, img-src va frame-src).
  • react-player (+579.1 KB) va Vidstack (+112.9) Trusted Types ostida ochilmadi; Vidstack eskirgan → Video.js 10.

Keyingi dars: PDF: ko'rsatish va yaratish — brauzerda PDF ko'rsatish (pdf.js), chekni klientda yaratish (jsPDF, @react-pdf/renderer), chop etish CSS'i va qachon PDF serverda yaratilishi kerak.

Manbalar

  • React: <video>/<audio> komponentlari va media hodisalari — react.dev/reference/react-dom/components/common
  • MDN: HTMLMediaElement, WebVTT format, <track>, Media Session API, Media Source Extensions
  • hls.js: "Getting started", API (isSupported, loadSource, attachMedia, destroy) — github.com/video-dev/hls.js
  • react-player v3 — npm; Vidstack → Video.js 10 migratsiyasi — videojs.org; react-lite-youtube-embed — npm
  • YouTube: "Privacy-enhanced mode" (youtube-nocookie.com) — support.google.com/youtube
  • Kanon: natijalar/tajriba/kutubxonalar-21.txt, kutubxonalar-ornatish.txt, tajriba/kutubxonalar/src/k/media-*.tsx, embed-youtube-lite.tsx (2026-10-17)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Video va audio React'da: pleyer, subtitr, HLS oqim va YouTube embed — IlmHamroh