IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik17/32-dars27 daqiqa
Mundarija (35)

JavaScript audio va video: HTMLMediaElement bilan pleyerni boshqarish

Qisqacha: <audio> va <video> JavaScript'da bitta umumiy interfeysga ega — HTMLMediaElement. play() Promise qaytaradi va brauzer ijroni taqiqlasa (autoplay siyosati) rad etiladi, shuning uchun uni await + try/catch bilan chaqiring. currentTime — joriy soniya (yozsangiz, o'sha joyga o'tadi), duration — uzunlik, volume — 0 dan 1 gacha, playbackRate — tezlik. Pleyer holatini hodisalardan bilasiz: loadedmetadata, play, pause, timeupdate, ended. Subtitrlar — <track> va textTracks.

Bu darsda

  • play() va pause() ni to'g'ri chaqirasiz va play() rad etilishini ushlaysiz.
  • Vaqt, ovoz va tezlikni (currentTime, volume, playbackRate) boshqarib, o'z pleyeringizni yasaysiz.
  • Media hodisalarini va ular qaysi tartibda kelishini bilasiz.
  • Brauzerning avtomatik ijro (autoplay) siyosatini tushuntirib, unga mos UX qila olasiz.
  • Subtitrlarni JavaScript'dan yoqib-o'chirasiz va joriy subtitr matnini olasiz.

Oldin bilishingiz kerak: video va audio, Subtitrlar va media accessibility, async / await, Listener opsiyalari: once, passive, signal, File va Blob.

1. Nega bu kerak?

video va audio darsida brauzerning tayyor pleyerini qo'ydik: controls atributi — va hammasi ishlaydi. Unda nega JavaScript?

Jasur akaning ikki istagi bor:

  1. Oshxona ekrani. Buyurtma tayyor bo'lganda ofitsiantlar xonasida qo'ng'iroq chalinsin. Bu yerda pleyer tugmasi yo'q — tovushni kod chaladi, buyurtma kelganda.
  2. Retsept videosi. «Bahor» saytida "Osh qanday damlanadi" videosi. Tugmalar sayt dizaynida bo'lsin, katta va o'zbekcha. Ostida esa bosqichlar ro'yxati: "Sabzi" ni bossangiz — video o'sha joyga o'tsin.

Ikkalasi uchun brauzer pleyerning "boshqaruv paneli"ni JavaScript'ga ochib beradi. Tayyor tugmalar ham aslida shu panelni ishlatadi. Biz faqat o'z tugmalarimizni ulaymiz.

2. Media elementi JavaScript'da

2.1 Bitta interfeys — ikki teg

<audio> va <video> JavaScript'da deyarli bir xil. Interfeys (interface) — obyektda qanday xususiyat va metodlar borligining ro'yxati. Televizor pulti kabi: televizor qaysi bo'lmasin, tugmalari bir xil. Ikkala teg ham HTMLMediaElement interfeysidan meros oladi (Meros darsidagi extends g'oyasi). Video faqat o'ziga xos bir nechta xususiyat qo'shadi: videoWidth, videoHeight, poster. Shuning uchun bu darsdagi hamma narsa ikkala teg uchun ham ishlaydi.

Elementni sahifadan olish mumkin yoki JavaScript'da yaratish:

js
const video = document.querySelector("video");
const bell = new Audio("qongiroq.mp3"); // sahifaga qo'shilmaydi

new Audio(...) — ko'rinmas <audio> elementi. U sahifaga qo'shilmasa ham tovush chaladi. Bildirishnoma ovozlari uchun eng qulay yo'l.

2.2 Natija oynasida media qayerdan?

Haqiqiy saytda src="qongiroq.mp3" yozasiz. Natija oynasida esa tashqi fayl yo'q. Shuning uchun misollarda tovushni o'zimiz yasaymiz. WAV — eng sodda tovush fayli formati: unda tovush siqilmagan, tebranishlar qanday bo'lsa, shunday yozilgan. makeWav yordamchisi notalar ro'yxatidan WAV fayl baytlarini yozib, Blob qaytaradi. Keyin URL.createObjectURL(blob) — va bu src sifatida oddiy fayl kabi ishlaydi.

Yordamchining ichini tushunish shart emas: unda fayl sarlavhasi va har bir tovush tebranishi binar ma'lumot sifatida yoziladi. Uni tayyor "qora quti" deb oling. Muhimi — undan keyingi kod: u .mp3 fayl bilan ham aynan shunday ishlaydi.

3. play() va pause()

3.1 play() — Promise qaytaradi

Eng sodda pleyer — ikki tugma. Ammo bitta muhim tafsilot bor: play() darhol ijro etmaydi. U Promise qaytaradi. Promise ijro haqiqatan boshlanganda bajariladi, boshlanmasa — rad etiladi.

Nega rad etilishi mumkin? Uch sabab:

  • brauzer avtomatik ijroni taqiqladi (bu haqda "Autoplay siyosati" bo'limida);
  • fayl buzuq yoki brauzer formatni tushunmaydi;
  • ijro boshlanmasdan pause() chaqirildi.

Shuning uchun play() ni async / await darsidagidek chaqiramiz:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<p>Oshxona ekrani: buyurtma tayyor bo'lganda qo'ng'iroq.</p>
<button id="ring" type="button">Qo'ng'iroq chalish</button>
<button id="stop" type="button">To'xtatish</button>
<script>
  // Yordamchi: notalardan WAV fayl yasaydi (ichi — qora quti)
  function makeWav(notes, rate = 8000) {
    const total = notes.reduce((sum, n) => sum + n.sec, 0);
    const count = Math.floor(rate * total);
    const view = new DataView(new ArrayBuffer(44 + count * 2));
    const put = (at, text) => [...text].forEach((ch, i) =>
      view.setUint8(at + i, ch.charCodeAt(0)));
    put(0, "RIFF"); view.setUint32(4, 36 + count * 2, true);
    put(8, "WAVEfmt "); view.setUint32(16, 16, true);
    view.setUint16(20, 1, true); view.setUint16(22, 1, true);
    view.setUint32(24, rate, true);
    view.setUint32(28, rate * 2, true);
    view.setUint16(32, 2, true); view.setUint16(34, 16, true);
    put(36, "data"); view.setUint32(40, count * 2, true);
    let i = 0;
    for (const { hz, sec } of notes) {
      const end = Math.min(count, i + Math.floor(rate * sec));
      for (let k = 0; i < end; i++, k++) {
        const fade = Math.min(1, (end - i) / 400);
        const v = Math.sin(2 * Math.PI * hz * k / rate) * 6000 * fade;
        view.setInt16(44 + i * 2, v, true);
      }
    }
    return new Blob([view.buffer], { type: "audio/wav" });
  }

  const bell = new Audio();
  bell.src = URL.createObjectURL(makeWav([
    { hz: 880, sec: 0.25 }, { hz: 660, sec: 0.4 },
  ]));

  async function ring() {
    try {
      bell.currentTime = 0; // har safar boshidan
      await bell.play();
      console.log("Chalinyapti");
    } catch (error) {
      console.log(`Chalib bo'lmadi: ${error.name}`);
    }
  }

  bell.addEventListener("ended", () => console.log("Tugadi"));
  document.getElementById("ring").addEventListener("click", ring);
  document.getElementById("stop").addEventListener("click", () => {
    bell.pause();
    console.log(`To'xtadi: ${bell.paused}`);
  });
</script>

«Qo'ng'iroq chalish» ni bosing — ikki notali "ding-dong" eshitiladi. Biz natija oynasida bosib sinadik. Konsolda avval Chalinyapti, 0,65 soniyadan keyin Tugadi chiqdi.

Qatorma-qator:

  • bell.currentTime = 0 — qo'ng'iroqni har safar boshidan chalish uchun. Busiz, ikkinchi bosishda tovush tugagan joyidan "davom etadi" — ya'ni hech narsa eshitilmaydi.
  • await bell.play() — ijro haqiqatan boshlangach keyingi qatorga o'tiladi.
  • catch — rad etilish. error.name xato turini aytadi: NotAllowedError, NotSupportedError yoki AbortError.
  • pause() — Promise qaytarmaydi, darhol to'xtatadi. paused xususiyati esa holatni aytadi: to'xtab turgan bo'lsa — true.

stop() degan metod yo'q. To'xtatib, boshiga qaytarish uchun ikki qator: pause() va currentTime = 0.

3.2 Uchta rad etilish — haqiqiy matnlari

Biz Chrome 154 da uchala holatni sinab, xabarlarni oldik.

Foydalanuvchi sahifa bilan hali o'zaro ta'sir qilmagan (hech narsa bosmagan) paytda play():

text
NotAllowedError: play() failed because the user didn't interact with the document first.

Tarjimasi: "play() bajarilmadi, chunki foydalanuvchi hujjat bilan hali o'zaro ta'sir qilmadi". Bu — autoplay siyosati.

Brauzer tushunmaydigan fayl (biz WAV o'rniga oddiy matn berdik):

text
NotSupportedError: Failed to load because no supported source was found.

Tarjimasi: "Yuklab bo'lmadi: qo'llanadigan manba topilmadi". Format noto'g'ri yoki fayl buzuq.

Hali yuklanmagan elementda play() dan darhol keyin pause():

text
AbortError: The play() request was interrupted by a call to pause().

Tarjimasi: "play() so'rovi pause() chaqiruvi bilan to'xtatildi". Bu xavfli emas, lekin ushlanmasa konsolda Uncaught (in promise) bo'lib qizil chiqadi. Masalan, foydalanuvchi videoni tez-tez bosib-to'xtatganda. Yechim bir xil: play() ning Promise'ini doim ushlang.

Tekshirib ko'ring: Sardor shunday yozdi: bell.play(); console.log("Chalinyapti");. Brauzer ijroni taqiqlasa, konsolda nima chiqadi?

Javob

Avval Chalinyapti — chunki play() kutilmadi, keyingi qator darhol bajarildi. Keyin qizil Uncaught (in promise) NotAllowedError: ... — rad etilgan Promise hech kim tomonidan ushlanmadi (Ushlanmagan xatolar). Ya'ni kod "chalinyapti" deb yolg'on gapirdi. To'g'risi: await bell.play() va try/catch.

4. Vaqt: currentTime va duration

4.1 Ikkala xususiyat

  • currentTime — joriy ijro joyi, soniyada (kasr bilan: 12.47). O'qish — "hozir qayerdamiz?". Yozish — "shu joyga o't" (seek). video.currentTime = 60 — birinchi daqiqaga sakrash.
  • duration — uzunlik, soniyada. Faqat o'qiladi.

Tuzoq: duration darhol ma'lum emas. Brauzer faylning boshini (metama'lumotni — uzunlik, o'lcham) o'qimaguncha u NaN. Biz sinadik: src berilgan zahoti duration — NaN, readyState — 0. Shuning uchun uzunlikni loadedmetadata hodisasidan keyin o'qing:

js
video.addEventListener("loadedmetadata", () => {
  totalOut.textContent = formatTime(video.duration);
});

Jonli efir (stream) uchun duration — Infinity: uning oxiri yo'q.

4.2 timeupdate — progress chizig'i uchun

Ijro paytida currentTime o'zgarib turadi, lekin u o'zgarganda har safar hodisa kelmaydi. Buning o'rniga timeupdate hodisasi davriy keladi. Spetsifikatsiya bo'yicha — har 15–250 ms da. Biz Chrome 154 da o'lchadik: 3 soniyalik ijro davomida hodisalar orasi taxminan 260 ms bo'ldi, ya'ni soniyasiga 4 marta.

Progress chizig'i uchun bu yetarli. Lekin soniyasiga 4 marta — silliq animatsiya emas. Silliq chiziq kerak bo'lsa, ijro paytida requestAnimationFrame siklida currentTime ni o'qing.

Tekshirib ko'ring: Sardor sahifa yuklanishi bilan video.src = "osh.mp4" yozdi, keyingi qatorda esa timeOut.textContent = video.duration. Ekranda nima chiqadi?

Javob

NaN. Brauzer faylning boshini hali o'qimagan — uzunlik noma'lum. To'g'ri yo'l: loadedmetadata hodisasini kutib, duration ni o'shanda o'qish.

5. Ovoz, tezlik va takrorlash

Xususiyat Qiymat Ma'nosi
volume 0 … 1 Ovoz balandligi: 0.5 — yarmi
muted true/false Ovozni o'chirish (volume ni eslab qoladi)
playbackRate 0.5, 1, 1.5, 2 Tezlik: 2 — ikki baravar tez
loop true/false Tugagach boshidan

Bir nechta tafsilot:

  • volume 0 dan 1 gacha — foiz emas. volume = 50 yozsangiz, Chrome xato tashlaydi (biz 1.5 bilan sinadik):
text
IndexSizeError: Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided 1.5-bob is outside the range [0, 1].

Tarjimasi: "volume xususiyatini o'rnatib bo'lmadi: berilgan qiymat 1.5-bob [0, 1] oralig'idan tashqarida". Foizdan o'tkazish: slider.value / 100.

  • muted va volume — alohida. Ovozni o'chirish tugmasi muted ni almashtiradi. Shunda yoqilganda avvalgi balandlik qaytadi — volume ni 0 qilib, keyin "eski qiymat qancha edi?" deb eslab yurish shart emas.
  • playbackRate o'zgarganda brauzer tovush ohangini saqlaydi (preservesPitch = true): ovoz tezlashadi, lekin "multfilm ovozi"ga aylanmaydi. Ma'ruza va retsept videolarida 1.5 tezlik — mashhur funksiya.
  • iPhone'larda volume ni JavaScript'dan o'zgartirib bo'lmaydi — ovozni foydalanuvchi faqat telefon tugmalari bilan boshqaradi. muted esa ishlaydi. Shuning uchun ovoz surgichini iPhone'da yashirish mumkin.

Tekshirib ko'ring: Ovoz surgichi 40% da turibdi. audio.volume ga qaysi son yoziladi? Javob: .

Javob

0.4 — ya'ni 40 / 100. volume foiz emas, 0 dan 1 gacha bo'lgan ulush. 40 yozilsa, Chrome IndexSizeError tashlaydi.

6. Media hodisalari

6.1 Yuklanish tartibi

Media elementi o'z hayotini hodisalar bilan aytib boradi. Avval yuklanish bosqichlari, keyin ijro hodisalari. Quyidagi oyna asosiy hodisalarni tinglaydi va konsolga yozadi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="play" type="button">Ijro</button>
<button id="seek" type="button">0.5 s ga o'tish</button>
<button id="fast" type="button">Tezlik 2×</button>
<script>
  // Yordamchi: notalardan WAV fayl yasaydi (ichi — qora quti)
  function makeWav(notes, rate = 8000) {
    const total = notes.reduce((sum, n) => sum + n.sec, 0);
    const count = Math.floor(rate * total);
    const view = new DataView(new ArrayBuffer(44 + count * 2));
    const put = (at, text) => [...text].forEach((ch, i) =>
      view.setUint8(at + i, ch.charCodeAt(0)));
    put(0, "RIFF"); view.setUint32(4, 36 + count * 2, true);
    put(8, "WAVEfmt "); view.setUint32(16, 16, true);
    view.setUint16(20, 1, true); view.setUint16(22, 1, true);
    view.setUint32(24, rate, true);
    view.setUint32(28, rate * 2, true);
    view.setUint16(32, 2, true); view.setUint16(34, 16, true);
    put(36, "data"); view.setUint32(40, count * 2, true);
    let i = 0;
    for (const { hz, sec } of notes) {
      const end = Math.min(count, i + Math.floor(rate * sec));
      for (let k = 0; i < end; i++, k++) {
        const fade = Math.min(1, (end - i) / 400);
        const v = Math.sin(2 * Math.PI * hz * k / rate) * 6000 * fade;
        view.setInt16(44 + i * 2, v, true);
      }
    }
    return new Blob([view.buffer], { type: "audio/wav" });
  }

  const audio = new Audio();
  const events = [
    "loadstart", "durationchange", "loadedmetadata", "loadeddata",
    "canplay", "canplaythrough", "play", "playing", "pause",
    "seeking", "seeked", "ratechange", "ended",
  ];
  for (const name of events) {
    audio.addEventListener(name, () => {
      console.log(`${name} — ${audio.currentTime.toFixed(2)} s`);
    });
  }
  audio.src = URL.createObjectURL(makeWav([{ hz: 523, sec: 1 }]));

  document.getElementById("play").addEventListener("click",
    () => audio.play().catch((e) => console.log(e.name)));
  document.getElementById("seek").addEventListener("click", () => {
    audio.currentTime = 0.5;
  });
  document.getElementById("fast").addEventListener("click", () => {
    audio.playbackRate = 2;
  });
</script>

Konsolda:

text
loadstart — 0.00 s
durationchange — 0.00 s
loadedmetadata — 0.00 s
loadeddata — 0.00 s
canplay — 0.00 s
canplaythrough — 0.00 s

Hech narsa bosilmagan — bu faqat yuklanish. Har bosqichning ma'nosi:

Hodisa Nima bo'ldi
loadstart Yuklash boshlandi
loadedmetadata Uzunlik va o'lcham ma'lum — duration endi son
loadeddata Birinchi kadr (yoki tovush bo'lagi) bor
canplay Ijroni boshlasa bo'ladi
canplaythrough Oxirigacha to'xtamasdan o'ynash mumkin (taxmin)

Butun yo'l bitta chizmada. Nimaga qarang: play va playing — ikki alohida bosqich. Internet sekin bo'lsa, ular orasida waiting keladi.

flowchart TD
  A["loadstart"] --> B["loadedmetadata<br/>duration ma'lum"]
  B --> C["canplay"]
  C --> D["play<br/>ijro so'raldi"]
  D --> E["playing<br/>tovush chiqyapti"]
  D -.-> W["waiting<br/>ma'lumot kutilmoqda"]
  W -.-> E
  E --> P["pause"]
  E --> F["ended"]

Endi «Ijro» ni bosing: play, keyin playing. Ular farq qiladi: play — "ijro so'raldi", playing — "tovush haqiqatan chiqyapti". Internet sekin bo'lsa, ular orasida waiting hodisasi keladi — ma'lumot kutilmoqda. Yuklanish belgisini (spinner) shu ikkisi orasida ko'rsatish to'g'ri. Oxirida pause va ended keladi: tugagach element o'zi to'xtaydi.

«0.5 s ga o'tish» — seeking va seeked: o'tish boshlandi va tugadi. O'tishdan keyin canplay va playing yana keladi — brauzer yangi joydan ijroga tayyorlanadi. «Tezlik 2×» — ratechange. Biz bu tugmalarni natija oynasida bosib, hodisalarni tekshirdik.

6.2 readyState va error

Hodisani o'tkazib yuborsangiz (masalan, tinglovchini kech qo'shsangiz), holatni readyState dan o'qing: 0 — hech narsa yo'q, 1 — metama'lumot bor, 4 — yetarli ma'lumot bor. if (video.readyState >= 1) — "uzunlik allaqachon ma'lum".

Fayl ochilmasa, error hodisasi keladi, media.error.code esa sababni aytadi. Biz buzuq faylda sinadik: code — 4 (MEDIA_ERR_SRC_NOT_SUPPORTED, "manba qo'llanmaydi"). Qolganlari: 1 — foydalanuvchi to'xtatdi, 2 — tarmoq xatosi, 3 — faylni ochishda (decode) xato.

Maslahat: Bir martalik kutish uchun once opsiyasi qulay: video.addEventListener("canplay", start, { once: true }). Video tayyor bo'lgan zahoti bir marta ishlaydi va tinglovchi o'zi o'chadi.

7. Autoplay siyosati

7.1 Qoida

video va audio darsida aytgan edik: ovozli videoni brauzer o'zi boshlatmaydi. Endi buning JavaScript tomoni. play() ni kim chaqirishidan qat'i nazar, brauzer bitta savol beradi: "foydalanuvchi bu sahifa bilan o'zaro ta'sir qildimi?" O'zaro ta'sir — bosish, klaviatura tugmasi, ekranga tegish. Sahifani aylantirish yoki sichqonchani olib o'tish hisoblanmaydi.

Ta'sir bo'lmagan bo'lsa, ovozli play() — NotAllowedError. Ta'sir bo'lgan bo'lsa — ruxsat. Shuning uchun play() ni tugma bosilganda chaqirish doim ishlaydi.

Ovozsiz (muted) video uchun cheklov yumshoqroq: Chrome uni odatda o'zi boshlashga ruxsat beradi — fon videolari shunday ishlaydi. Lekin buni ham kafolat deb bilmang. Biz sinadik: natija oynasida (boshqa saytdan joylangan oyna) ovozsiz audio ham hech narsa bosilmasdan NotAllowedError oldi. Chrome qo'shimcha ravishda saytga "ishonch" darajasini ham hisoblaydi: siz ko'p video ko'radigan saytlarda ruxsat kengroq. Demak, natijani oldindan bilib bo'lmaydi — doim catch yozing.

Yana bir sabab bor. Telefon tejash rejimida yoki foydalanuvchi brauzer sozlamasida autoplay'ni butunlay o'chirgan bo'lishi mumkin. To'g'ri kod buni "xato" emas, oddiy holat deb qabul qiladi.

7.2 To'g'ri UX: rad etilsa — tugma

Amaliy naqsh: ijroga urinib ko'ring, bo'lmasa — foydalanuvchiga tugma ko'rsating:

js
async function tryAutoplay(video) {
  try {
    await video.play();
  } catch (error) {
    if (error.name === "NotAllowedError") {
      playButton.hidden = false; // "Ijro etish" tugmasi
    } else {
      throw error; // boshqa xato — yashirmaymiz
    }
  }
}

Oshxona ekrani uchun ham shunday: sahifa ochilganda xodim bir marta «Ovozni yoqish» tugmasini bosadi. Shundan keyin qo'ng'iroqlar avtomatik chalinadi — o'zaro ta'sir allaqachon bo'lgan. Bu tugma keraksiz "byurokratiya" emas: u reklama videolari to'satdan baqirmasligi uchun qilingan himoyaning narxi.

Tekshirib ko'ring: Sahifa ochilishi bilan setTimeout(() => bell.play(), 5000) qo'yildi. Foydalanuvchi shu 5 soniya ichida sahifadagi tugmani bosdi. Qo'ng'iroq chalinadimi?

Javob

Ha. Brauzer play() chaqirilgan paytda "sahifa bilan o'zaro ta'sir bo'lganmi?" deb so'raydi. 5 soniya ichida bosish bo'ldi — ruxsat bor. Hech kim bosmaganida esa NotAllowedError bo'lardi. Ruxsat sahifa yangilanguncha saqlanadi.

8. O'z pleyerimiz: retsept videosi

8.1 Video natija oynasida

Endi hammasini bitta pleyerga yig'amiz. Video kerak — tashqi fayl esa yo'q. Shuning uchun «Video yasash» tugmasi canvas'dagi animatsiyani 4 soniyalik video qilib yozib oladi. Buni makeVideo yordamchisi qiladi. U Canvas amaliyotda darsidagi chizish va keyingi darsdagi MediaRecorder dan foydalanadi. Hozir uni qora quti deb oling.

Pleyerda:

  • o'z tugmalarimiz: ijro/pauza, ovozsiz, tezlik, to'liq ekran;
  • progress surgichi va vaqt (0:01 / 0:04);
  • bosqichlar ro'yxati — bosilganda o'sha soniyaga o'tadi;
  • o'zbekcha subtitr — <track> bilan.
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .player { max-width: 320px; background: #1b1b1b; }
  video { display: block; width: 100%; }
  .bar { display: flex; gap: 0.25rem; align-items: center;
    flex-wrap: wrap; padding: 0.25rem; color: #ffffff; }
  .bar input { flex: 1; }
  button { font: inherit; padding: 0.3rem 0.6rem; }
</style>
<p><button id="make" type="button">Video yasash</button>
  <span id="status"></span></p>
<div class="player" id="player" hidden>
  <video id="video" playsinline>
    <track id="subs" kind="subtitles" srclang="uz"
      label="O'zbekcha" default>
  </video>
  <div class="bar">
    <button id="toggle" type="button">Ijro</button>
    <input id="progress" type="range" min="0" max="4" step="0.1"
      value="0" aria-label="Video joyi">
    <span id="time">0:00 / 0:00</span>
  </div>
  <div class="bar">
    <button id="mute" type="button"
      aria-pressed="false">Ovozsiz</button>
    <button id="speed" type="button">1×</button>
    <button id="cc" type="button" aria-pressed="true">Subtitr</button>
    <button id="full" type="button">To'liq ekran</button>
  </div>
</div>
<ol id="steps">
  <li><button type="button" data-time="0">Piyoz</button></li>
  <li><button type="button" data-time="1">Go'sht</button></li>
  <li><button type="button" data-time="2">Sabzi</button></li>
  <li><button type="button" data-time="3">Guruch</button></li>
</ol>
<script>
  // Yordamchi: canvas animatsiyasidan video yasaydi (qora quti)
  function makeVideo(seconds) {
    const canvas = document.createElement("canvas");
    canvas.width = 320;
    canvas.height = 180;
    const ctx = canvas.getContext("2d");
    const names = ["Piyoz", "Go'sht", "Sabzi", "Guruch"];
    const recorder = new MediaRecorder(canvas.captureStream(30));
    const chunks = [];
    recorder.ondataavailable = (event) => chunks.push(event.data);
    const start = performance.now();
    function draw(now) {
      const t = (now - start) / 1000;
      const name = names[Math.min(names.length - 1, Math.floor(t))];
      ctx.fillStyle = "#2e7d32";
      ctx.fillRect(0, 0, 320, 180);
      ctx.fillStyle = "#e0a458";
      ctx.fillRect(0, 172, (320 * t) / seconds, 8);
      ctx.fillStyle = "#ffffff";
      ctx.font = "700 32px system-ui, sans-serif";
      ctx.textAlign = "center";
      ctx.fillText(name, 160, 80);
      if (recorder.state === "recording") requestAnimationFrame(draw);
    }
    return new Promise((resolve) => {
      recorder.onstop = () =>
        resolve(new Blob(chunks, { type: recorder.mimeType }));
      recorder.start();
      requestAnimationFrame(draw);
      setTimeout(() => recorder.stop(), seconds * 1000);
    });
  }

  const vtt = `WEBVTT

00:00.000 --> 00:01.000
Piyozni halqa qilib to'g'rang

00:01.000 --> 00:02.000
Go'shtni qizartiring

00:02.000 --> 00:03.000
Sabzini somon qilib to'g'rang

00:03.000 --> 00:04.000
Guruchni tekis yoying`;

  const video = document.getElementById("video");
  const toggle = document.getElementById("toggle");
  const progress = document.getElementById("progress");
  const timeOut = document.getElementById("time");
  const subs = document.getElementById("subs");
  subs.src = "data:text/vtt," + encodeURIComponent(vtt);

  function formatTime(sec) {
    const s = Math.floor(sec);
    return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
  }

  function showTime() {
    progress.value = video.currentTime;
    timeOut.textContent = `${formatTime(video.currentTime)} / `
      + formatTime(Math.ceil(video.duration));
  }

  async function togglePlay() {
    if (video.paused) {
      try {
        await video.play();
      } catch (error) {
        console.log(`Ijro bo'lmadi: ${error.name}`);
      }
    } else {
      video.pause();
    }
  }

  video.addEventListener("loadedmetadata", () => {
    progress.max = video.duration;
    showTime();
    console.log(`Video tayyor: ${video.videoWidth} × `
      + `${video.videoHeight}`);
  });
  video.addEventListener("timeupdate", showTime);
  video.addEventListener("play", () => {
    toggle.textContent = "Pauza";
  });
  video.addEventListener("pause", () => {
    toggle.textContent = "Ijro";
  });
  video.addEventListener("ended", () => {
    console.log("Retsept tugadi");
  });

  let lastCue = null;
  subs.track.addEventListener("cuechange", () => {
    const cues = subs.track.activeCues; // hozir faol yozuvlar
    const cue = cues[cues.length - 1];
    if (cue && cue !== lastCue) console.log(`Subtitr: ${cue.text}`);
    lastCue = cue;
  });

  toggle.addEventListener("click", togglePlay);
  progress.addEventListener("input", () => {
    video.currentTime = Number(progress.value);
  });

  const muteButton = document.getElementById("mute");
  muteButton.addEventListener("click", () => {
    video.muted = !video.muted;
    muteButton.setAttribute("aria-pressed", String(video.muted));
  });

  const speeds = [1, 1.5, 2, 0.5];
  const speedButton = document.getElementById("speed");
  speedButton.addEventListener("click", () => {
    const next = speeds[(speeds.indexOf(video.playbackRate) + 1)
      % speeds.length];
    video.playbackRate = next;
    speedButton.textContent = `${next}×`;
  });

  const ccButton = document.getElementById("cc");
  ccButton.addEventListener("click", () => {
    const on = subs.track.mode !== "showing";
    subs.track.mode = on ? "showing" : "hidden";
    ccButton.setAttribute("aria-pressed", String(on));
  });

  document.getElementById("full").addEventListener("click", () => {
    document.getElementById("player").requestFullscreen()
      .catch((error) => console.log(error.name));
  });

  const steps = document.getElementById("steps");
  steps.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-time]");
    if (!button) return;
    video.currentTime = Number(button.dataset.time);
    togglePlay();
  });

  const makeButton = document.getElementById("make");
  makeButton.addEventListener("click", async () => {
    const status = document.getElementById("status");
    status.textContent = "Video yozilmoqda… (4 soniya)";
    const blob = await makeVideo(4);
    video.src = URL.createObjectURL(blob);
    document.getElementById("player").hidden = false;
    status.textContent = "";
  });
</script>

«Video yasash» ni bosing va 4 soniya kuting. Keyin «Ijro»: subtitrlar video ustida chiqadi, konsolga esa har bosqich yoziladi. Biz natija oynasida sinadik: konsolda Video tayyor: 320 × 180, ijro paytida to'rtta Subtitr: … qatori va oxirida Retsept tugadi. «Sabzi» bosqichi bosilganda video 2-soniyadan boshlandi va Subtitr: Sabzini somon qilib to'g'rang chiqdi.

8.2 Pleyerni tahlil qilamiz

Bitta haqiqat manbai. Tugma matni («Ijro» / «Pauza») tugma bosilganda emas, play va pause hodisalarida o'zgaradi. Nega? Video boshqa yo'l bilan ham to'xtashi mumkin: tugadi, bosqich tugmasi bosildi, telefon qo'ng'irog'i keldi. Holatning manbai — video elementining o'zi. Biz faqat uning hodisalarini tinglaymiz. Bu state → render g'oyasi: UI holatni ko'rsatadi, uni alohida eslab yurmaydi.

Surgich. input hodisasida currentTime ni yozamiz — o'quvchi surgichni sudrasa, video o'sha joyga o'tadi. timeupdate da esa aksincha: video yurganda surgich siljiydi. progress.max = video.duration — loadedmetadata dan keyin, chunki undan oldin uzunlik NaN.

Tezlik. Tugma tezliklar ro'yxati bo'ylab aylanadi: indexOf joriy tezlikni topadi, % speeds.length esa oxiridan boshiga qaytaradi.

Subtitrlar. <track> elementining track xususiyati — TextTrack obyekti. Uning mode i uch holatli:

  • "showing" — brauzer subtitrni video ustida ko'rsatadi;
  • "hidden" — ko'rsatmaydi, lekin cuechange hodisalari keladi;
  • "disabled" — butunlay o'chiq, fayl yuklanmaydi ham.

activeCues — ayni paytdagi subtitr bo'laklari (cue — "bitta subtitr yozuvi"). Chegarada (aynan 2-soniyada) ikki yozuv birga faol bo'lishi mumkin — biz Chrome'da shuni ko'rdik. Shuning uchun oxirgisini olamiz va bir yozuvni ikki marta chiqarmaymiz (lastCue). "hidden" rejim ham foydali: subtitrni video ustida emas, sahifaning o'zida, masalan katta shrift bilan ko'rsatish mumkin. Barcha treklar ro'yxati — video.textTracks. WebVTT formatining o'zi — Subtitrlar darsida.

To'liq ekran. Videoning o'zini emas, butun .player ni to'liq ekranga chiqaramiz — shunda bizning tugmalarimiz ham ko'rinadi. requestFullscreen() Promise qaytaradi — navigator va qurilma API'lari darsidagi kabi.

Bobur va Malika. Hamma boshqaruv — haqiqiy <button>: Tab va Enter bilan ishlaydi, ekran o'quvchi nomini o'qiydi. Ikki holatli tugmalarda aria-pressed bor — Malika "Ovozsiz, bosilgan" deb eshitadi. Surgichning yozuvi — aria-label. Subtitrlar esa eshitish qobiliyati past mehmonlar uchun: video ovoz bilan nimani aytsa, matnda ham bo'lsin.

Diqqat: Brauzerning standart klaviatura tugmalari (bo'sh joy — pauza, strelkalar — surish) faqat controls bilan ishlaydi. O'z pleyeringizda ularni o'zingiz yozasiz (3-mashq). Tugmalarsiz pleyer Bobur uchun ishlamaydi.

9. Ko'p uchraydigan xatolar

9.1 play() ni ushlamaslik

video.play(); — Promise'siz. Rad etilsa konsolda Uncaught (in promise), foydalanuvchi esa nima bo'lganini bilmaydi. Tuzatish: await + try/catch yoki .catch(...), rad etilganda tugma ko'rsatish.

9.2 duration ni erta o'qish

js
video.src = "osh.mp4";
console.log(video.duration); // NaN

Tuzatish: loadedmetadata hodisasidan keyin o'qing yoki readyState >= 1 ni tekshiring.

9.3 volume ni foizda berish

audio.volume = 80 — IndexSizeError. Tuzatish: 0 dan 1 gacha: 80 / 100.

9.4 Ikkinchi marta chalinmaydigan qo'ng'iroq

Qisqa tovush birinchi marta chalinadi, ikkinchi marta — jim. Sababi: ijro oxiriga yetgan, currentTime — oxirida. Tuzatish: play() dan oldin currentTime = 0. Bir nechta qo'ng'iroq bir vaqtda kerak bo'lsa (ketma-ket ikki buyurtma) — har biriga bell.cloneNode() yoki keyingi darsdagi Web Audio.

9.5 Tugma holatini qo'lda yuritish

"Bosdim — demak, o'ynayapti" deb tugma matnini bosishda o'zgartirish. Video tugasa yoki play() rad etilsa, tugma yolg'on gapiradi. Tuzatish: matnni play/pause hodisalarida o'zgartiring.

10. Mashqlar

1-mashq (oson): Yangi buyurtma ovozi

Oshxona ekranida «Yangi buyurtma» tugmasi bor. Har bosilganda qisqa ovoz boshidan chalinsin va konsolga Buyurtma #N chiqsin (N — 1, 2, 3, …). Ovoz uchun yuqoridagi makeWav ni bitta nota bilan ishlating: [{ hz: 988, sec: 0.2 }]. Ijro rad etilsa, konsolga Ovoz o'chiq: <xato nomi>.

Ishora: «Qo'ng'iroq chalish» misolidagi ring funksiyasi naqshi.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.4rem 0.8rem; }
</style>
<button id="order" type="button">Yangi buyurtma</button>
<script>
  // Yordamchi: notalardan WAV fayl yasaydi (ichi — qora quti)
  function makeWav(notes, rate = 8000) {
    const total = notes.reduce((sum, n) => sum + n.sec, 0);
    const count = Math.floor(rate * total);
    const view = new DataView(new ArrayBuffer(44 + count * 2));
    const put = (at, text) => [...text].forEach((ch, i) =>
      view.setUint8(at + i, ch.charCodeAt(0)));
    put(0, "RIFF"); view.setUint32(4, 36 + count * 2, true);
    put(8, "WAVEfmt "); view.setUint32(16, 16, true);
    view.setUint16(20, 1, true); view.setUint16(22, 1, true);
    view.setUint32(24, rate, true);
    view.setUint32(28, rate * 2, true);
    view.setUint16(32, 2, true); view.setUint16(34, 16, true);
    put(36, "data"); view.setUint32(40, count * 2, true);
    let i = 0;
    for (const { hz, sec } of notes) {
      const end = Math.min(count, i + Math.floor(rate * sec));
      for (let k = 0; i < end; i++, k++) {
        const fade = Math.min(1, (end - i) / 400);
        const v = Math.sin(2 * Math.PI * hz * k / rate) * 6000 * fade;
        view.setInt16(44 + i * 2, v, true);
      }
    }
    return new Blob([view.buffer], { type: "audio/wav" });
  }

  const sound = new Audio();
  sound.src = URL.createObjectURL(makeWav([{ hz: 988, sec: 0.2 }]));
  let orderNumber = 0;

  document.getElementById("order").addEventListener("click",
    async () => {
      orderNumber++;
      console.log(`Buyurtma #${orderNumber}`);
      try {
        sound.currentTime = 0;
        await sound.play();
      } catch (error) {
        console.log(`Ovoz o'chiq: ${error.name}`);
      }
    });
</script>

Uch marta bosing — konsolda Buyurtma #1, #2, #3, har safar ovoz bilan. Buyurtma raqami ovozdan oldin chiqariladi: ovoz bo'lmasa ham buyurtma yo'qolmasligi kerak. Ovoz — qo'shimcha xizmat, asosiy ish emas.

2-mashq (o'rta): Ma'ruza tezligi

Audio pleyerda to'rtta tezlik tugmasi bo'lsin: 0.5×, 1×, 1.5×, 2×. Tanlangan tugmada aria-pressed="true", qolganlarida "false". Tezlik o'zgarganda (ratechange hodisasida — tugma bosilganda emas!) konsolga Tezlik: 1.5×. Tanlangan tezlik localStorage da saqlansin va sahifa qayta ochilganda tiklansin.

Ishora: tugmalarni data-rate atributi bilan yozing va event delegation bilan bitta tinglovchi qo'ying. localStorage — localStorage va sessionStorage darsidagi setItem/getItem.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button { font: inherit; padding: 0.3rem 0.7rem; }
  [aria-pressed="true"] { background: #2e7d32; color: #ffffff; }
</style>
<audio id="lecture" controls></audio>
<div id="rates">
  <button type="button" data-rate="0.5">0.5×</button>
  <button type="button" data-rate="1">1×</button>
  <button type="button" data-rate="1.5">1.5×</button>
  <button type="button" data-rate="2">2×</button>
</div>
<script>
  // Yordamchi: notalardan WAV fayl yasaydi (ichi — qora quti)
  function makeWav(notes, rate = 8000) {
    const total = notes.reduce((sum, n) => sum + n.sec, 0);
    const count = Math.floor(rate * total);
    const view = new DataView(new ArrayBuffer(44 + count * 2));
    const put = (at, text) => [...text].forEach((ch, i) =>
      view.setUint8(at + i, ch.charCodeAt(0)));
    put(0, "RIFF"); view.setUint32(4, 36 + count * 2, true);
    put(8, "WAVEfmt "); view.setUint32(16, 16, true);
    view.setUint16(20, 1, true); view.setUint16(22, 1, true);
    view.setUint32(24, rate, true);
    view.setUint32(28, rate * 2, true);
    view.setUint16(32, 2, true); view.setUint16(34, 16, true);
    put(36, "data"); view.setUint32(40, count * 2, true);
    let i = 0;
    for (const { hz, sec } of notes) {
      const end = Math.min(count, i + Math.floor(rate * sec));
      for (let k = 0; i < end; i++, k++) {
        const fade = Math.min(1, (end - i) / 400);
        const v = Math.sin(2 * Math.PI * hz * k / rate) * 6000 * fade;
        view.setInt16(44 + i * 2, v, true);
      }
    }
    return new Blob([view.buffer], { type: "audio/wav" });
  }

  const KEY = "bahor:tezlik";
  const audio = document.getElementById("lecture");
  const rates = document.getElementById("rates");
  audio.src = URL.createObjectURL(makeWav([
    { hz: 392, sec: 0.5 }, { hz: 440, sec: 0.5 }, { hz: 494, sec: 1 },
  ]));

  function showRate() {
    for (const button of rates.querySelectorAll("button")) {
      const rate = Number(button.dataset.rate);
      const active = rate === audio.playbackRate;
      button.setAttribute("aria-pressed", String(active));
    }
  }

  audio.addEventListener("ratechange", () => {
    showRate();
    localStorage.setItem(KEY, String(audio.playbackRate));
    console.log(`Tezlik: ${audio.playbackRate}×`);
  });

  rates.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-rate]");
    if (button) audio.playbackRate = Number(button.dataset.rate);
  });

  const saved = Number(localStorage.getItem(KEY));
  audio.playbackRate = saved > 0 ? saved : 1;
  showRate();
</script>

Birinchi ochilishda localStorage bo'sh — Number(null) 0 beradi, shuning uchun saved > 0 tekshiruvi 1 ni tanlaydi. Tezlik 1 ga teng bo'lgani uchun hech narsa o'zgarmaydi va ratechange kelmaydi. «1.5×» ni bosing — Tezlik: 1.5×. Keyin «Qayta ishga tushirish»: natija oynasining xotirasi saqlanadi, va yuklanishda konsolga yana Tezlik: 1.5× chiqadi — tezlik tiklandi. Haqiqiy saytda bu brauzerning o'z localStorage i.

3-mashq (qiyin): Klaviatura bilan boshqarish

Retsept pleyeri uchun Bobur so'ragan tezkor tugmalarni qo'shing (pleyer konteynerida, tabindex="0" bilan):

  • bo'sh joy yoki k — ijro/pauza;
  • ← / → — 1 soniya orqaga/oldinga (0 dan kam va duration dan ko'p bo'lmasin);
  • m — ovozsiz.

Bo'sh joy va strelkalar sahifani aylantirmasin. Fokus tugmada bo'lsa (masalan, «Ijro» tugmasi), bo'sh joy ikki marta ishlamasin.

Ishora: event.key ning bo'sh joy uchun qiymati — " ". Klaviatura hodisalari darsidagi key. Tugma ustida bo'sh joy o'zi click beradi.

Yechim
js
const player = document.getElementById("player");
player.tabIndex = 0;

player.addEventListener("keydown", (event) => {
  // Tugma ustida bo'sh joy o'zi "click" qiladi — ikki marta emas
  if (event.target.closest("button") && event.key === " ") return;

  const key = event.key.toLowerCase();
  if (key === " " || key === "k") {
    togglePlay();
  } else if (key === "arrowleft" || key === "arrowright") {
    const step = key === "arrowleft" ? -1 : 1;
    video.currentTime = Math.min(video.duration,
      Math.max(0, video.currentTime + step));
  } else if (key === "m") {
    video.muted = !video.muted;
  } else {
    return; // boshqa tugmalarga tegmaymiz
  }
  event.preventDefault(); // sahifa aylanmasin
});

Tinglovchi pleyer konteynerida — ichidagi tugmalardan ko'tarilgan (bubble) hodisalar ham shu yerga keladi. Shuning uchun birinchi qator bor: fokus tugmada bo'lsa va bo'sh joy bosilsa, brauzer click ni o'zi beradi — biz yana togglePlay qilsak, video boshlanib darhol to'xtardi. Math.min/Math.max vaqtni chegaralar ichida ushlaydi. Boshqa tugmalarda return — Tab kabi kerakli tugmalarni buzmaslik uchun preventDefault faqat o'zimiz ishlatgan tugmalarda.

4-mashq: Amaliy tajriba — vazifa bajarilganda "ding"

Kanon kodga tegmasdan tajriba qiling: kurs/mashqlar/13/17-ding/index.html. Ro'yxatda uchta vazifa va har birining katakchasi. Katakcha belgilanganda (bajarildi) qisqa "ding" chalinsin. Lekin sahifada «Ovozlar» katakchasi bo'lsin va u standartda o'chiq — foydalanuvchi o'zi yoqmaguncha sayt jim. Tanlov localStorage da "vazifalar:ovoz" kalitida saqlansin. Live Server bilan oching.

Yechim
html
<label><input type="checkbox" id="ovoz"> Ovozlar</label>
<ul id="royxat">
  <li><label><input type="checkbox"> Non olish</label></li>
  <li><label><input type="checkbox"> Hisobot yozish</label></li>
  <li><label><input type="checkbox"> Kassa hisobi</label></li>
</ul>
<script>
  const SOUND_KEY = "vazifalar:ovoz";
  const ding = new Audio("ding.mp3"); // papkaga qisqa ovoz qo'ying
  const soundToggle = document.getElementById("ovoz");
  soundToggle.checked = localStorage.getItem(SOUND_KEY) === "ha";

  soundToggle.addEventListener("change", () => {
    const value = soundToggle.checked ? "ha" : "yoq";
    localStorage.setItem(SOUND_KEY, value);
  });

  document.getElementById("royxat").addEventListener("change",
    async (event) => {
      if (!event.target.checked || !soundToggle.checked) return;
      try {
        ding.currentTime = 0;
        await ding.play();
      } catch (error) {
        console.warn("Ovoz chalinmadi:", error.name);
      }
    });
</script>

Bu yerda haqiqiy fayl — ding.mp3 (istalgan qisqa ovoz; o'zingiz yozib olishingiz ham mumkin — keyingi darsda). Ovoz faqat ikki shart bajarilganda chalinadi: vazifa bajarildi holatiga o'tdi va foydalanuvchi ovozni o'zi yoqqan. Katakcha bosilgani — o'zaro ta'sir, shuning uchun autoplay siyosati bu yerda to'sqinlik qilmaydi. console.warn — xato emas, ogohlantirish: ovoz chalinmasa ham ilova ishlayveradi.

bash
git add 13/17-ding/index.html
git commit -m "13/17: vazifa bajarilganda ixtiyoriy ovoz"

11. Real ishda

  • Video platformalar. YouTube va kurs saytlarining pleyerlari — aynan shu API ustida: o'z tugmalari, tezlik, subtitr, "qayerda to'xtagan bo'lsangiz" eslab qolish. Katta videolar bo'laklab, sifatni internetga moslab yuklanadi (HLS, DASH formatlari). Buni hls.js kabi kutubxonalar qiladi — media elementiga baytlarni bo'lak-bo'lak beradi.
  • Bildirishnoma ovozlari. Telegram Web, CRM va POS tizimlari yangi xabar yoki buyurtmada qisqa ovoz chaladi — new Audio() + play() + catch. Va hamma joyda — ovozni o'chirish sozlamasi.
  • Media Session API. Telefonning qulf ekranida va quloqchin tugmalarida "keyingi trek" ishlashi uchun navigator.mediaSession ishlatiladi. Musiqa va podkast saytlari uchun foydali — MDN'da ko'ring.
  • Intervyu. "play() nega Promise qaytaradi?", "Autoplay qachon bloklanadi?", "loadedmetadata va canplay farqi?" — media bilan ishlaydigan jamoalarda so'raladi.

Xulosa

  • <audio> va <video> — bitta HTMLMediaElement interfeysi; new Audio() sahifaga qo'shilmasdan chaladi.
  • play() Promise qaytaradi: NotAllowedError (autoplay), NotSupportedError (format), AbortError (pauza) — doim ushlang.
  • currentTime o'qiladi va yoziladi (seek); duration — loadedmetadata dan keyin; timeupdate taxminan 4 marta soniyasiga.
  • volume 0–1, muted alohida, playbackRate ohangni saqlaydi.
  • UI holatini hodisalardan oling (play, pause, ended); autoplay rad etilsa — tugma ko'rsating.
  • Subtitrlar — <track>, textTracks, mode (showing/hidden/disabled), cuechange va activeCues.

Keyingi dars: Web Audio, kamera va mikrofon — tovushni fayldan emas, koddan yaratamiz (AudioContext, ossillyator), kamera va mikrofondan oqim olamiz va uni MediaRecorder bilan yozib olamiz.

Manbalar

  • HTML Living Standard: "Media elements" (play(), hodisalar, readyState) — html.spec.whatwg.org
  • MDN: HTMLMediaElement, "Autoplay guide for media and Web Audio APIs", TextTrack — developer.mozilla.org
  • Chrome for Developers: "Autoplay policy in Chrome" — developer.chrome.com
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript audio va video: HTMLMediaElement bilan pleyerni boshqarish — IlmHamroh