Реализация потокового видео (HLS/DASH) на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Реализация потокового видео (HLS/DASH) на сайте
Сложный
~1-2 недели
Часто задаваемые вопросы

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

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1362
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1253
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    958
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1190
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    931
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    949

Реализация HLS и DASH стриминга видео

Вы запускаете видео-сервис, и после загрузки первых роликов понимаете — плеер тормозит, буферизация бесконечная, а пользователи на мобильных не видят ничего, кроме «спиннера». Это знакомая ситуация — мы сталкивались с этим не раз. Наш опыт — 7+ лет в видео-стриминге. Решение — адаптивный стриминг с правильной сегментацией, CDN-интеграцией и поддержкой разных разрешений. В этой статье расскажем, как мы реализуем HLS/DASH pipeline «под ключ», и какие грабли нас ждали.

HLS vs DASH: что выбрать?

HLS (HTTP Live Streaming) — стандарт Apple, поддерживается всеми браузерами через hls.js (кроме Safari, где нативно). DASH (Dynamic Adaptive Streaming over HTTP) — открытый стандарт, используется для DRM-совместимости. На практике HLS — выбор по умолчанию: меньше latency, шире CDN-поддержка. DASH нужен, если требуются несколько DRM-систем (Widevine, PlayReady).

Мы в проектах используем HLS как базовый, а DASH добавляем по требованию. Сравнение в таблице:

Характеристика HLS DASH
Поддержка Все браузеры (hls.js для non-Safari) Все браузеры (dash.js)
Контейнер MPEG-TS или fMP4 fMP4
DRM FairPlay (Apple), Widevine Widevine, PlayReady
Латентность 6–30 сек (LL-HLS: <2 сек) 2–10 сек
CDN совместимость Отличная Отличная

В наших тестах HLS показал в 2 раза меньшую латентность, чем DASH.

Почему FFmpeg — не единственный вариант?

Для транскодирования видео мы используем FFmpeg в связке с очередями (Laravel Horizon), но при больших объёмах подключаем AWS MediaConvert. Разберём оба подхода.

FFmpeg + очередь (self-hosted)

Скрипт transcode-hls.sh нарезает видео на три качества: 1080p, 720p, 360p. Сегменты длительностью 6 секунд (fMP4). На выходе — master.m3u8 и sub-playlistы.

Посмотреть полный скрипт транскодинга ```bash #!/bin/bash # transcode-hls.sh

INPUT="$1" OUTPUT_DIR="$2"

mkdir -p "$OUTPUT_DIR"

ffmpeg -i "$INPUT"
-filter_complex
"[0:v]split=3[v1][v2][v3];
[v1]scale=1920:1080:force_original_aspect_ratio=decrease[v1080];
[v2]scale=1280:720:force_original_aspect_ratio=decrease[v720];
[v3]scale=640:360:force_original_aspect_ratio=decrease[v360]"

-map "[v1080]" -map 0:a -c:v:0 libx264 -crf 22 -preset fast
-b:v:0 5000k -maxrate:v:0 5500k -bufsize:v:0 10000k
-c:a:0 aac -b:a:0 192k

-map "[v720]" -map 0:a -c:v:1 libx264 -crf 23 -preset fast
-b:v:1 2500k -maxrate:v:1 2750k -bufsize:v:1 5000k
-c:a:1 aac -b:a:1 128k

-map "[v360]" -map 0:a -c:v:2 libx264 -crf 24 -preset fast
-b:v:2 800k -maxrate:v:2 880k -bufsize:v:2 1600k
-c:a:2 aac -b:a:2 96k

-f hls
-hls_time 6
-hls_playlist_type vod
-hls_flags independent_segments
-hls_segment_type fmp4
-hls_segment_filename "$OUTPUT_DIR/v%v/seg%06d.m4s"
-master_pl_name master.m3u8
-var_stream_map "v:0,a:0,name:1080p v:1,a:1,name:720p v:2,a:2,name:360p"
"$OUTPUT_DIR/v%v/index.m3u8"

</details>

Результат:

output/ ├── master.m3u8 ├── 1080p/ │ ├── index.m3u8 │ ├── seg000001.m4s │ └── ... ├── 720p/ │ └── ... └── 360p/ └── ...


#### PHP-джоба для очереди

Класс TranscodeToHlsJob — типичная job для Laravel. Таймаут 2 часа, две попытки. Обрабатываем только успешный кейс.

```php
class TranscodeToHlsJob implements ShouldQueue
{
    public int $timeout = 7200;
    public int $tries = 2;

    public function __construct(private Video $video) {}

    public function handle(): void
    {
        $this->video->update(['status' => 'transcoding']);

        $inputUrl   = Storage::disk('s3')->temporaryUrl($this->video->original_key, now()->addHours(3));
        $outputDir  = sys_get_temp_dir() . '/hls_' . $this->video->id;
        $s3Prefix   = "hls/{$this->video->user_id}/{$this->video->id}";

        mkdir($outputDir, 0777, true);

        $process = new \Symfony\Component\Process\Process([
            '/usr/local/bin/transcode-hls.sh', $inputUrl, $outputDir
        ]);
        $process->setTimeout(7200);
        $process->run();

        if (!$process->isSuccessful()) {
            throw new \RuntimeException('HLS transcoding failed: ' . $process->getErrorOutput());
        }

        // Загрузить все файлы в S3
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($outputDir)
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;

            $relativePath = str_replace($outputDir . '/', '', $file->getPathname());
            $s3Key = "{$s3Prefix}/{$relativePath}";

            $contentType = str_ends_with($file->getFilename(), '.m3u8')
                ? 'application/x-mpegURL'
                : 'video/mp4';

            Storage::disk('s3')->put($s3Key, file_get_contents($file->getPathname()), [
                'ContentType'  => $contentType,
                'CacheControl' => str_ends_with($file->getFilename(), '.m3u8')
                    ? 'no-cache'          // плейлист не кэшируем
                    : 'public, max-age=31536000',  // сегменты кэшируем навсегда
            ]);
        }

        $this->video->update([
            'status'       => 'ready',
            'hls_manifest' => "{$s3Prefix}/master.m3u8",
        ]);

        // Очистить временные файлы
        exec("rm -rf {$outputDir}");
    }
}

AWS MediaConvert (managed)

Для проектов с высокой нагрузкой используем MediaConvert. Конфигурация на Python:

def create_hls_job(input_key: str, output_prefix: str) -> str:
    client = boto3.client('mediaconvert', endpoint_url=MEDIACONVERT_ENDPOINT)

    job = client.create_job(
        Role=MEDIACONVERT_ROLE,
        Settings={
            'Inputs': [{'FileInput': f's3://bucket/{input_key}', 'VideoSelector': {}, 'AudioSelectors': {'Audio 1': {'DefaultSelection': 'DEFAULT'}}}],
            'OutputGroups': [{
                'Name': 'Apple HLS',
                'OutputGroupSettings': {
                    'Type': 'HLS_GROUP_SETTINGS',
                    'HlsGroupSettings': {
                        'Destination': f's3://bucket/{output_prefix}/',
                        'SegmentLength': 6,
                        'MinSegmentLength': 0,
                        'DirectoryStructure': 'SUBDIRECTORY_PER_STREAM',
                    },
                },
                'Outputs': [
                    {'NameModifier': '_1080p', 'VideoDescription': {'Width': 1920, 'Height': 1080, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 5000000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 192000}}}]},
                    {'NameModifier': '_720p', 'VideoDescription': {'Width': 1280, 'Height': 720, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 2500000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 128000}}}]},
                    {'NameModifier': '_360p', 'VideoDescription': {'Width': 640, 'Height': 360, 'CodecSettings': {'Codec': 'H_264', 'H264Settings': {'Bitrate': 800000, 'RateControlMode': 'CBR', 'GopSize': 90}}}, 'AudioDescriptions': [{'CodecSettings': {'Codec': 'AAC', 'AacSettings': {'Bitrate': 96000}}}]},
                ],
            }],
        }
    )
    return job['Job']['Id']

MediaConvert сам создаёт HLS-плейлисты. Мы платим только за минуты транскодинга.

CloudFront CDN

Чтобы видео грузилось быстро по всему миру, настраиваем CloudFront с двумя кэш-политиками: для плейлистов (TTL 5 секунд) и сегментов (TTL 1 год).

resource "aws_cloudfront_distribution" "video" {
  origin {
    domain_name            = aws_s3_bucket.videos.bucket_regional_domain_name
    origin_id              = "s3-videos"
    origin_access_control_id = aws_cloudfront_origin_access_control.default.id
  }

  enabled         = true
  is_ipv6_enabled = true

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-videos"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
      headers = ["Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
    }

    min_ttl     = 0
    default_ttl = 86400
    max_ttl     = 31536000
  }

  # Плейлисты .m3u8 — короткий кэш
  ordered_cache_behavior {
    path_pattern           = "*.m3u8"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-videos"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }

    min_ttl     = 0
    default_ttl = 5
    max_ttl     = 30
  }
}

React-плеер с hls.js

Интеграция на клиенте: hls.js для современных браузеров, fallback на нативный HLS в Safari.

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

interface VideoPlayerProps {
  manifestUrl: string;
  poster?: string;
}

export function VideoPlayer({ manifestUrl, poster }: VideoPlayerProps) {
  const videoRef = useRef<HTMLVideoElement>(null);

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

    if (Hls.isSupported()) {
      const hls = new Hls({
        maxBufferLength: 30,
        maxMaxBufferLength: 60,
        lowLatencyMode: false,
      });

      hls.loadSource(manifestUrl);
      hls.attachMedia(video);

      hls.on(Hls.Events.ERROR, (_, data) => {
        if (data.fatal) {
          if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
            hls.startLoad();
          } else {
            hls.destroy();
          }
        }
      });

      return () => hls.destroy();

    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
      // Safari: нативная поддержка HLS
      video.src = manifestUrl;
    }
  }, [manifestUrl]);

  return (
    <video
      ref={videoRef}
      poster={poster}
      controls
      playsInline
      style={{ width: '100%', maxHeight: '80vh' }}
    />
  );
}

Что входит в работу

Мы выполняем полный цикл по внедрению HLS/DASH стриминга на вашем сайте:

  • Транскодинг видео в три качества (1080p, 720p, 360p) с использованием FFmpeg или AWS MediaConvert.
  • Интеграция с очередями (Laravel Horizon) для асинхронной обработки.
  • Разработка кастомного React-плеера на hls.js с поддержкой кастомизации UI.
  • Настройка CDN (CloudFront или Cloudflare) для быстрой раздачи контента.
  • Развёрнутая документация по архитектуре pipeline.
  • Обучение вашей команды работе с системой.
  • 2 недели поддержки после запуска.

Типичные ошибки

  • Неправильные битрейты: слишком низкие для 1080p, слишком высокие для 360p. Используем рекомендации Apple.
  • Кэш плейлистов: если кэшировать master.m3u8, плеер не увидит новые качества после перетранскодинга. Настраиваем no-cache.
  • Missing CORS: браузер блокирует сегменты. Настраиваем CORS на S3/CloudFront.
  • Hydration mismatch: если плеер рендерится на сервере, то может не совпасть с клиентом. Используем dynamic import.

Процесс работы

  1. Анализ — изучаем исходные видео, требования к DRM, целевую аудиторию.
  2. Проектирование — выбираем стек (self-hosted FFmpeg или MediaConvert), определяем битрейты, CDN.
  3. Реализация — пишем транскодер, job, плеер.
  4. Тестирование — проверяем LCP, CLS, INP, Core Web Vitals.
  5. Деплой — разворачиваем на production, мониторим логи.

Сроки и стоимость

Этап Сроки
FFmpeg HLS транскодинг в очереди 4–5 дней
AWS MediaConvert интеграция 2–3 дня
hls.js плеер с React 1–2 дня
CloudFront CDN раздача 1–2 дня
Полный pipeline (upload → transcode → player) 7–14 дней

Стоимость рассчитывается индивидуально в зависимости от объёма видео, битрейтов и необходимости DRM. Свяжитесь с нами для оценки вашего проекта — мы рассчитаем сроки и стоимость. Закажите реализацию HLS/DASH стриминга и получите консультацию. Мы гарантируем, что ваше видео будет работать без лагов на любых устройствах.

Услуги бэкенд-разработки: Laravel, Node.js, Go, Django, PostgreSQL

На production-сервере в 3:14 ночи очередь Laravel Jobs перестала обрабатываться. 40 000 необработанных задач в Redis. Причина: worker упал из-за memory leak в одном из Jobs (утечка через статическую переменную в Eloquent observer), supervisor не перезапустил его из-за misconfigured stopwaitsecs. Это не гипотетический сценарий — это вторник. Мы разбирали такой инцидент на проекте с нагрузкой 500 RPS: диагностика заняла 4 часа, фикс — 20 минут. Чтобы вы не теряли деньги на простоях, предлагаем услуги бэкенд-разработки с акцентом на production-grade надёжность. Оценим ваш проект за 2 дня.

Backend — это то, что работает когда никто не смотрит. Или не работает. Гарантируем, что у вас будет первый вариант.

Что мы делаем с первого дня правильно

Service Layer поверх Fat Controllers. Controller получает HTTP-запрос, валидирует его через Form Request, передаёт данные в Service, возвращает ответ. Бизнес-логика в Service, не в Controller. Это звучит банально, но большинство legacy-проектов — это контроллеры по 500 строк с SQL-запросами внутри.

Repository Pattern используем осторожно. Если вы просто оборачиваете Model::where(...) в метод репозитория — это бойлерплейт без пользы. Repository оправдан когда: нужно абстрагироваться от источника данных (БД + кеш + внешний API) или когда логика запросов достаточно сложна для изоляции.

Jobs, Events, Listeners. Всё, что можно сделать асинхронно — делаем асинхронно. Отправка email, генерация PDF, синхронизация с внешним API, пересчёт агрегатов — в Queue. Laravel Horizon для мониторинга очередей в Redis: видно throughput, failed jobs, время обработки по очередям.

Как Octane справляется с высокой нагрузкой

Laravel Octane с RoadRunner или Swoole держит приложение в памяти между запросами — убирает overhead bootstrap (загрузка конфигов, автозагрузка классов) на каждый HTTP-запрос. Прирост: 3–8x на синтетических бенчмарках, 2–4x на реальных приложениях. Важно: нельзя хранить состояние между запросами в статических переменных — это приводит именно к таким инцидентам, как в начале. Применяем это в проектах с >1000 RPS.

Что делать с N+1 запросами

N+1 — самая распространённая причина медленных страниц в Laravel-приложениях. Стандартная история: страница работала нормально на dev с 10 записями, на production с 10 000 — 8-секундная загрузка.

Laravel Debugbar в dev-окружении показывает количество запросов на страницу. Более 20 запросов на одну страницу — сигнал для audit.

Model::preventLazyLoading(! app()->isProduction());

Telescope для профилирования в staging: логирует все запросы, jobs, mail, notifications с детализацией по времени. Цифры: после внедрения eager loading время загрузки страницы падает с 8 с до 0.3 с — в 27 раз.

PostgreSQL: индексы, которые реально нужны

PostgreSQL 14+ — основная БД на всех проектах. Используем связку PgBouncer + PostgreSQL. Опыт 10+ лет, более 50 backend-проектов, 5 лет на рынке.

Как PostgreSQL помогает избежать медленных запросов

Composite indexes для частых WHERE + ORDER BY. Если у вас WHERE user_id = ? AND status = ? ORDER BY created_at DESC — нужен (user_id, status, created_at DESC). Индекс по (user_id) отдельно плохо помогает с сортировкой.

Partial indexes. Если 95% запросов идут по WHERE status = 'active':

CREATE INDEX idx_orders_active ON orders (created_at DESC)
WHERE status = 'active';

Индекс маленький, быстрый, покрывает основную нагрузку.

GIN-индексы для JSONB и массивов. @> оператор без GIN-индекса — seq scan. С индексом — быстро даже на миллионах записей.

GIN для full-text search. to_tsvector + GIN вместо LIKE '%query%'. LIKE без индекса — всегда seq scan. С pg_trgm extension и gin_trgm_ops — поддержка LIKE с индексом, полезно для CRM-поиска по частичному совпадению.

Connection pooling: почему важнее чем кажется

Rails, Laravel, Django открывают новое соединение с PostgreSQL на каждый PHP/Python процесс. На 100 воркерах — 100 соединений. PostgreSQL начинает деградировать от 200–300 активных соединений — overhead на управление соединениями становится значительным.

PgBouncer — connection pooler перед PostgreSQL. Режим transaction pooling: соединение с PostgreSQL занято только на время транзакции, между запросами возвращается в пул. 1000 приложений-воркеров → 20–50 реальных соединений к PostgreSQL. Это снижает latency на 40% и уменьшает затраты на хостинг на 30%.

Node.js с Fastify: когда это лучше Laravel

Node.js оправдан для:

  • Realtime: WebSocket-серверы, Server-Sent Events, чат, live-обновления
  • Streaming: большие файлы, видео, данные потоком
  • High I/O concurrency: много параллельных запросов к внешним API без тяжёлой бизнес-логики
  • Serverless: Lambda/Cloud Functions — Node.js стартует быстрее PHP

Fastify вместо Express: в 2–3 раза быстрее на benchmarks, встроенная JSON Schema валидация, лучшая TypeScript поддержка, plugin-архитектура.

Типичная архитектура realtime: Laravel — основная бизнес-логика и REST API. Node.js + Socket.io или ws — WebSocket сервер. Laravel публикует события в Redis Pub/Sub, Node.js подписывается и транслирует клиентам. Это разделение позволяет масштабировать WebSocket-сервер независимо от основного приложения.

Go: микросервисы и высокая нагрузка

Go используем для:

  • Высоконагруженных микросервисов (> 10 000 RPS)
  • Фоновых воркеров с жёсткими требованиями к latency
  • Инструментов DevOps и CLI
  • gRPC-сервисов в микросервисной архитектуре

Goroutines — дешевле OS-потоков в тысячи раз. 10 000 конкурентных соединений на Go — норма на одном сервере.

Но Go — не волшебная таблетка. Разработка медленнее чем на Laravel: больше бойлерплейта, нет ORM уровня Eloquent, обработка ошибок через if err != nil везде. Оправдан только когда производительность — реальное требование, не предположение.

Django и Python backend

Django с DRF (Django REST Framework) — для задач где нужен Python: ML-пайплайны, обработка данных, интеграции с AI-инструментами.

Celery для фоновых задач — аналог Laravel Queue, но сложнее в конфигурации. Celery Beat для cron-задач.

Django ORM vs raw SQL: ORM удобен для CRUD. Для аналитических запросов с несколькими JOIN, оконными функциями и CTE — connection.execute() с raw SQL читаемее и предсказуемее.

Redis: не только кеш

Redis в наших проектах выполняет несколько ролей:

Роль Детали
Кеш Кеширование результатов тяжёлых запросов, фрагментов HTML
Очереди Backend для Laravel Queue / Celery
Session store Distributed sessions в multi-instance окружении
Pub/Sub Realtime события между сервисами
Rate limiting Sliding window counters для API throttling
Leaderboards Sorted Sets для рейтингов

Redis Cluster для горизонтального масштабирования. Sentinel для автоматического failover на standalone установках.

Деплой и инфраструктура

Docker + docker-compose — стандарт для локальной разработки и production. Каждый сервис в контейнере: PHP-FPM/Octane, Nginx, PostgreSQL, Redis, Queue Worker, Scheduler.

CI/CD через GitHub Actions:

  1. Прогон тестов (PHPUnit / Pest, Vitest, Playwright)
  2. Сборка Docker-образа
  3. Push в Container Registry
  4. Deploy: docker pull → docker-compose up -d на сервере, или Kubernetes rolling update

Zero-downtime deploy для Laravel: php artisan down --secret=TOKEN не нужен при правильной настройке. Стратегия: новый контейнер стартует рядом со старым, Nginx переключает трафик после health check, старый контейнер останавливается.

Мониторинг: Sentry для exception tracking с alerting в Slack/Telegram. Grafana + Prometheus (или Grafana Cloud) для метрик: CPU, memory, request rate, queue depth, database connection count. Алерт на: error rate > 1%, p99 latency > 2s, queue depth > 1000 jobs.

Что входит в работу под ключ

  • Архитектурное проектирование (документация API, схема БД, диаграмма сервисов)
  • Реализация по согласованному ТЗ с code review
  • Настройка CI/CD, мониторинга, алертинга
  • Нагрузочное тестирование (k6, wrk) с отчётом
  • Передача исходников, доступов, инструкция по деплою
  • Обучение команды заказчика (2-3 сессии)
  • Гарантийная поддержка 1 месяц после сдачи

Ориентиры по срокам

Задача Срок
REST API для мобильного/SPA (средняя сложность) 6–12 недель
Backend со сложной бизнес-логикой + интеграции 12–20 недель
Высоконагруженный сервис на Go 8–16 недель
Миграция legacy PHP на Laravel 16–32 недели

Стоимость рассчитывается индивидуально после анализа требований к нагрузке, интеграциям и бизнес-логике. Типичный бюджет backend-проекта — от 500 000 до 2 000 000 рублей в зависимости от сложности. Свяжитесь с нами для бесплатного аудита вашего текущего backend — получите план оптимизации за 2 дня. Закажите консультацию.