Реалізація відеоплеєра (Video.js/Plyr) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація відеоплеєра (Video.js/Plyr) на сайті
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація відеоплеєра (Video.js/Plyr) на сайті

Вбудований тег <video> — мінімально робоче рішення, яке ламається на першому ж нестандартному вимогу: кастомний дизайн, субтитри, якість, аналітика перегляду, реклама, DRM. Для продакшену потрібен плеєр з екосистемою.

Video.js vs Plyr: коли що

Plyr — легкий (~28 KB gzip), красивий з коробки, підтримує HTML5 video/audio, YouTube, Vimeo. Достатньо для 80% завдань. API простий і зрозумілий.

Video.js — важкий (~150 KB core + плагіни), промишленний стандарт. Підтримує HLS, DASH, DRM (Widevine, PlayReady, FairPlay через плагіни), рекламу (IMA SDK), аналітику. Використовується на великих медіаплатформах.

Вибір: якщо потрібен просто красивий плеєр для mp4/YouTube — Plyr. Адаптивні потоки (HLS/DASH), live-трансляції або монетизація — Video.js.

Plyr: базова інтеграція

<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css">

<video id="player" playsinline controls>
  <source src="/video/movie.mp4" type="video/mp4">
  <source src="/video/movie.webm" type="video/webm">
  <track kind="captions" label="Російська" srclang="ru" src="/captions/ru.vtt" default>
</video>

<script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
<script>
const player = new Plyr('#player', {
  controls: ['play-large', 'play', 'progress', 'current-time', 'mute',
             'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
  settings: ['captions', 'quality', 'speed', 'loop'],
  speed: { selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2] },
  youtube: { noCookie: true, rel: 0, showinfo: 0 },
  ratio: '16:9',
  keyboard: { focused: true, global: false },
  tooltips: { controls: true, seek: true },
  captions: { active: true, language: 'ru', update: true },
})

// Аналітика перегляду
player.on('timeupdate', () => {
  const pct = Math.floor((player.currentTime / player.duration) * 100)
  if ([25, 50, 75, 90].includes(pct)) {
    analytics.track('video_progress', { percent: pct, src: player.source })
  }
})
</script>

Plyr з React

import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
import { useEffect, useRef } from 'react'

interface VideoPlayerProps {
  src: string
  poster?: string
  captions?: { src: string; label: string; language: string }[]
}

export function VideoPlayer({ src, poster, captions = [] }: VideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null)
  const playerRef = useRef<Plyr>()

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

    playerRef.current = new Plyr(videoRef.current, {
      ratio: '16:9',
      controls: ['play-large', 'play', 'progress', 'current-time',
                 'mute', 'volume', 'settings', 'fullscreen'],
    })

    return () => {
      playerRef.current?.destroy()
    }
  }, [])

  return (
    <video ref={videoRef} poster={poster} crossOrigin="anonymous">
      <source src={src} type="video/mp4" />
      {captions.map(cap => (
        <track key={cap.language} kind="captions"
          label={cap.label} srcLang={cap.language} src={cap.src} />
      ))}
    </video>
  )
}

Video.js: HLS-стриміінг

<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet">
<video id="my-video" class="video-js vjs-big-play-centered" controls preload="auto"
  poster="/poster.jpg" data-setup='{}' style="width:100%;height:auto">
</video>

<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
<script>
const player = videojs('my-video', {
  fluid: true,
  responsive: true,
  sources: [{
    src: 'https://stream.example.com/hls/video.m3u8',
    type: 'application/x-mpegURL',
  }],
  html5: {
    vhs: {
      overrideNative: true,      // використовувати VHS замість рідного HLS
      enableLowInitialPlaylist: true,
      smoothQualityChange: true,
      limitRenditionByPlayerDimensions: true,
    },
  },
  controlBar: {
    children: [
      'playToggle',
      'volumePanel',
      'currentTimeDisplay',
      'timeDivider',
      'durationDisplay',
      'progressControl',
      'liveDisplay',
      'remainingTimeDisplay',
      'customControlSpacer',
      'playbackRateMenuButton',
      'chaptersButton',
      'descriptionsButton',
      'subsCapsButton',
      'qualitySelector',    // вимагає плагін videojs-contrib-quality-levels
      'fullscreenToggle',
    ],
  },
})

// Логування помилок
player.on('error', () => {
  const error = player.error()
  console.error('Video error:', error.code, error.message)
  Sentry.captureException(new Error(`Video error ${error.code}: ${error.message}`))
})
</script>

Переключення якості в Video.js

npm install @videojs/http-streaming videojs-contrib-quality-levels videojs-hls-quality-selector
import videojs from 'video.js'
import 'videojs-contrib-quality-levels'
import 'videojs-hls-quality-selector'

const player = videojs('player')
player.hlsQualitySelector({ displayCurrentQuality: true })

// Програмне переключення
const qualityLevels = player.qualityLevels()
qualityLevels.on('addqualitylevel', (event) => {
  const level = event.qualityLevel
  console.log(`Quality: ${level.height}p, bitrate: ${level.bitrate}`)
})

Ледачо-завантаження плеєра

Ініціалізувати Video.js на 10 плеєрах одночасно — вбивство продуктивності сторінки. Використовуємо IntersectionObserver:

const observers = new Map<Element, IntersectionObserver>()

function lazyInitPlayer(container: HTMLElement) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const video = entry.target.querySelector('video')!
        videojs(video, { fluid: true })
        observer.disconnect()
        observers.delete(container)
      }
    })
  }, { threshold: 0.1, rootMargin: '200px' })

  observer.observe(container)
  observers.set(container, observer)
}

document.querySelectorAll('.video-lazy').forEach(el => lazyInitPlayer(el as HTMLElement))

Кастомний постер з попереднім переглядом при наведенні

// Генеруємо спрайт попередньо перегляду (storyboard)
// На сервері: ffmpeg -i input.mp4 -vf "fps=1/10,scale=160:-1,tile=10x10" sprites.jpg
// Клієнтський код:
function setupThumbnailPreview(player: any) {
  const progressBar = player.controlBar.progressControl.seekBar.el()
  const thumb = document.createElement('div')
  thumb.className = 'vjs-thumbnail'
  progressBar.appendChild(thumb)

  progressBar.addEventListener('mousemove', (e: MouseEvent) => {
    const rect = progressBar.getBoundingClientRect()
    const pct = (e.clientX - rect.left) / rect.width
    const time = pct * player.duration()
    const frame = Math.floor(time / 10)  // 1 кадр кожні 10 сек

    const col = frame % 10
    const row = Math.floor(frame / 10)

    thumb.style.backgroundImage = 'url(/sprites.jpg)'
    thumb.style.backgroundPosition = `-${col * 160}px -${row * 90}px`
    thumb.style.left = `${e.clientX - rect.left - 80}px`
    thumb.style.display = 'block'
  })
}

Вбудовування YouTube через Plyr

const player = new Plyr('#youtube-player', {
  debug: false,
})
// HTML: <div id="youtube-player" data-plyr-provider="youtube" data-plyr-embed-id="dQw4w9WgXcQ"></div>

// Отримати поточний час з YouTube iframe
player.on('timeupdate', ({ detail: { plyr } }) => {
  console.log(plyr.currentTime)
})

Продуктивність та Core Web Vitals

Відеоплеєр — один із головних винуватців поганого LCP/CLS. Рецепти:

<!-- Зарезервуємо місце під плеєр через aspect-ratio, запобігаємо CLS -->
<div style="aspect-ratio: 16/9; background: #000;">
  <video ...></video>
</div>

<!-- Зображення постера для швидкого LCP -->
<video poster="/poster-small.webp" preload="none">
// Не завантажуємо JS плеєра до взаємодії
const loadPlayer = async () => {
  const { default: Plyr } = await import('plyr')
  await import('plyr/dist/plyr.css')
  return new Plyr('#player')
}

document.getElementById('play-btn')?.addEventListener('click', async () => {
  const player = await loadPlayer()
  player.play()
}, { once: true })

Терміни

Plyr з базовою конфігурацією та стилізацією під бренд — 1 день. Video.js з HLS, переключенням якості, субтитрами та аналітикою — 3–4 дні. DRM-інтеграція (Widevine + сервер ліцензій) — окремий проект на 1–2 тижні.