Иногда возникает необходимость воспроизведения в браузере видео файлов, но использовать специальный видео хостинг (вроде YouTube) не всегда есть возможность (например, если файлы загружены пользователями).
Самый простой способ добавить видео на страницу, это использовать тег <video>
:
<video src="/path/to/video.mp4">
В браузере уже есть стандартный встроенный видео плеер, который проиграет переданный в параметре src
файл.
У этого способа есть ряд проблем:
<video>
есть возможность указать несколько источников с разными форматами, т.е. можно подготовить несколько файлов, чтобы получить наибольшую поддержку в браузерах, но не все форматы одинаково хорошо кодируются -
у некоторых форматов может не быть аппаратной поддержки, поэтому они кодироваться будут очень неэффективно.
Вероятно сейчас (май 2021 года) наиболее оптимальным будет выбор H.264 для видео и AAC для аудио.
Поэтому нужно либо ограничить возможность загрузки видео только указанными форматами, либо предварительно перекодировать файлы не сервере.По таблице совместимости на caniuse можно заметить, что для аудио лучше подходит MP3 - у него немного более широкая поддержка в браузерах. Но на практике использовать MP3 вместе с DASH (об этом пойдет речь ниже) может быть проблематично - например Video.js в некоторых случаях поддерживает воспроизведение только AAC.
Все перечисленные проблемы решает потоковое воспроизведение видео, если на стороне сервера файлы подготавливаются при помощи FFmpeg, а на стороне браузера используется плеер Video.js.
FFmpeg - это набор программ и библиотек для работы с аудио и видео, поддерживающий большое количество форматов. Здесь не будет описываться его установка, она зависит от используемой операционной системы, нужную информацию можно получить на официальном сайте.
Пример встраивания видео на страницу при помощи тега video
Пример встраивания видео на страницу при помощи Video.js
Для потокового воспроизведения (streaming, стриминга) видео в браузере в разное время использовались разные решения. Раньше было популярно использование протоколов RTSP/RTMP. Встроенной их поддержки в браузерах не было (ее и сейчас нет), но существовали плееры на основе Flash. Сейчас использование Flash невозможно, поэтому RTSP/RTMP используются в основном для передачи видео между серверами, а в браузерах нужно использовать другие решения.
Специально для веба были разработаны протоколы передачи видео, использующие в качестве транспорта HTTP - стандартный протокол для всех браузеров. Здесь можно выделить HLS, который разработан и поддерживается Apple, и DASH, который разработан и поддерживается международной организацией MPEG. Оба протокола зарегистрированы как стандарты:
Еще есть возможность передачи видео через WebRTC, но это требует использования специального сервера для передачи данных (вроде Kurento).
Далее будет рассматриваться использование протокола DASH - на практике он показался более гибким и удобным.
Формат DASH поддерживает использование видео с адаптивным битрейтом, т.е. выбор во время воспроизведения видео, наиболее подходящего скорости соединения или возможностям устройства пользователя. Также поддерживается выбор из нескольких аудио-потоков.
В общем случае, видео и аудио-потоки разбиваются на отдельные файлы - сегменты фиксированной длины. При воспроизведении эти файлы просто скачиваются браузером, поэтому для размещения такого видео достаточно обычного HTTP-сервера.
FFmpeg поддерживает конвертирование видео в формат DASH, для этого можно воспользоваться примерно такой командой:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ffmpeg -hide_banner -y -i input.mp4 \
-r 25 -c:v libx264 -pix_fmt yuv420p -preset veryfast -profile:v main \
-keyint_min 250 -g 250 -sc_threshold 0 \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-map v:0 -filter:v:0 "scale=-2:360" -b:v:0 800k -maxrate:0 856k -bufsize:0 1200k \
-map v:0 -filter:v:1 "scale=-2:432" -b:v:1 1400k -maxrate:1 1498k -bufsize:1 2100k \
-map v:0 -filter:v:2 "scale=-2:540" -b:v:2 2000k -maxrate:2 2140k -bufsize:2 3500k \
-map v:0 -filter:v:3 "scale=-2:720" -b:v:3 2800k -maxrate:3 2996k -bufsize:3 4200k \
-map v:0 -filter:v:4 "scale=-2:1080" -b:v:4 5000k -maxrate:4 5350k -bufsize:4 7500k \
-map 0:a \
-init_seg_name "init-\$RepresentationID\$.\$ext\$" \
-media_seg_name "chunk-\$RepresentationID\$-\$Number%05d\$.\$ext\$" \
-dash_segment_type mp4 \
-use_template 1 \
-use_timeline 0 \
-seg_duration 10 \
-adaptation_sets "id=0,streams=v id=1,streams=a" \
-f dash \
dash.mpd
Параметры здесь указаны следующие:
-hide_banner
- не показывать информацию о сборке FFmpeg-y
- перезаписать существующие файлы-i input.mp4
- путь к исходному файлу для перекодирования-r 25
- частота кадров в секунду в перекодированном файле-c:v libx264
- выбор кодека H.264 для кодирования видео-pix_fmt yuv420p
- способ кодирования цвета (рекомендуется указать явно)-preset veryfast
- настройки качества кодирования для кодека H.264, чем выше качество, тем ниже скорость кодирования.
Можно выбрать из ряда ultrafast,superfast,veryfast,faster,fast,medium,slow,slower,veryslow
.
Обычно для веба скорость обработки важнее, поэтому рекомендуется выбирать более быстрые настройки.-profile:v main
- настройка профиля для кодека H.264, main
- безопасный вариант.
Параметры кодека H.264 более подробно описаны в документации FFmpeg.-keyint_min 250 -g 250 -sc_threshold 0
- настройка создания ключевых кадров.
Рекомендуется выбирать keyint_min
и g
близкими к длине сегмента, т.к. FFmpeg делит видео на сегменты как раз по ключевым кадрам.
Так достигается наиболее точное разделение - сегменты получаются максимально близкими по длине.
Если эти параметры не указать или выбрать другое значение, FFmpeg будет определять длину сегмента как ожидаемая продолжительность (seg_duration) плюс расстояние до следующего ключевого кадра.
С учетом частоты кадров значение будет равно (частота_кадров х длина_сегмента), т.е. 25 х 10 = 250.
-sc_threshold 0
отключает встроенное в H.264 определение смены сцен и автоматическую расстановку ключевых кадров в них.-c:a aac
- выбор кодека AAC для кодирования аудио-b:a 128k
- битрейт аудио, обычно достаточно значения в пределах 100k-200k-ac 2
- количество каналов аудио (1 - моно, 2 - стерео)-ar 48000
- частота дискретизации аудио, обычно выбирают 44100 или 48000-map v:0 -filter:v:0 "scale=-2:360" -b:v:0 800k -maxrate:0 856k -bufsize:0 1200k
- параметры кодирования видео под определенный битрейт.
Количество поддерживаемых настроек качества зависит от качества исходного видео и аппаратных возможностей по перекодированию - для каждой настройки видео нужно будет перекодировать полностью, поэтому не стоит добавлять очень много настроек.
Выбор битрейта вопрос больше творческий и зависит от исходного видео, для примера можно воспользоваться таблицей.
-map v:0
- использовать видео №0 из исходного файла-filter:v:0 "scale=-2:360"
- изменить размер изображения до 360 пикселей по высоте, а ширину подобрать автоматически по соотношению сторон.
-2 означает, что высота кадра должна быть кратна 2, это требование кодека H.264 - размеры должны быть обязательно четными числами.-b:v:0 800k
- целевой битрейт видео-maxrate:0 856k
- максимальный допустимый битрейт видео-bufsize:0 1200k
- размер буфера кодировщика-map 0:a
- использовать аудио №0 из исходного файла-init_seg_name "init-\$RepresentationID\$.\$ext\$"
- шаблон имени файлов для сегментов инициализации DASH (файлы DASH будут описаны ниже).
Здесь используются подстановки \$RepresentationID\$
- номер аудио или видео-потока, \$ext\$
- расширение файла, в зависимости от выбранного контейнера.-media_seg_name "chunk-\$RepresentationID\$-\$Number%05d\$.\$ext\$"
- шаблон имени файла для сегментов с данными.
Здесь также используются подстановки, в дополнение к предыдущим, \$Number%05d\$
- номер сегмента, число из пяти цифр с ведущими нулями.-dash_segment_type mp4
- формат контейнера для сегментов, может быть mp4
или webm
-use_template 1
- если указать 1, то использовать шаблон имени файла для описания всех сегментов в итоговом файле.
Если указать 0, то вместо шаблона имени будет перечисление имен всех сегментов.-use_timeline 0
- если указать 1, то в дополнение к ожидаемой длительности сегмента перечислить фактическое время начала и длительность для каждого сегмента.
Используется для более точной навигации по файлу.-seg_duration 10
- ожидаемая продолжительность одного сегмента (в секундах)-adaptation_sets "id=0,streams=v id=1,streams=a"
- группировка потоков в итоговом файле (будет рассмотрено подробнее в описании файла mpd, в данном случае группировка отдельно всех видео-потоков и отдельно аудио-поток)-f dash
- использовать формат DASHdash.mpd
- имя файла mpdВ результате выполнения команды будет создано некоторое количество файлов (о них будет подробнее написано ниже). Для воспроизведения результата можно воспользоваться таким html-кодом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="https://vjs.zencdn.net/7.10.2/video-js.css" rel="stylesheet" />
<title>DASH</title>
</head>
<body>
<video id="my-video" class="video-js" controls preload="auto" width="640" data-setup="{}">
<source src="dash.mpd" type="application/dash+xml" />
<p class="vjs-no-js">NO SIGNAL</p>
</video>
<script src="https://vjs.zencdn.net/7.10.2/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-contrib-quality-levels@2.0.9/dist/videojs-contrib-quality-levels.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-hls-quality-selector@1.1.4/dist/videojs-hls-quality-selector.min.js"></script>
<script>
const player = videojs('my-video');
player.hlsQualitySelector();
</script>
</body>
</html>
Здесь используется плеер Video.js, который поддерживает формат DASH, с компонентом, позволяющим вручную выбирать качество видео. По умолчанию качество видео будет подстраиваться автоматически.
Таким образом решены все перечисленные ранее проблемы:
Недостатком такого подхода можно назвать необходимость перекодировать видео под каждый битрейт. Если файл большой, то это может потребовать большого количества времени и аппаратных ресурсов. Чтобы разобраться, возможны ли другие подходы, рассмотрим файлы, которые создал FFmpeg.
Пример встраивания видео в формате DASH на страницу
Здесь будет описываться только некоторая часть формата DASH для VOD (Video-on-Demand), для LIVE-трансляций файлы и описания могут быть другими.
Формат DASH определяет три типа файлов:
Файл описания dash.mpd
, созданный FFmpeg с параметрами, описанными выше, будет выглядеть так:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="utf-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:dash:schema:mpd:2011"
xmlns:xlink="http://www.w3.org/1999/xlink"
xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd"
profiles="urn:mpeg:dash:profile:isoff-live:2011"
type="static"
mediaPresentationDuration="PT53.3S"
maxSegmentDuration="PT10.0S"
minBufferTime="PT20.0S">
<ProgramInformation>
<Title>sample</Title>
</ProgramInformation>
<ServiceDescription id="0">
</ServiceDescription>
<Period id="0" start="PT0.0S">
<AdaptationSet id="0" contentType="video" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true"
frameRate="25/1" maxWidth="1920" maxHeight="1080" par="16:9" lang="und">
<Representation id="0" mimeType="video/mp4" codecs="avc1.4d401e" bandwidth="800000" width="640" height="360" sar="1:1">
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
<Representation id="1" mimeType="video/mp4" codecs="avc1.4d401e" bandwidth="1400000" width="768" height="432" sar="1:1">
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
<Representation id="2" mimeType="video/mp4" codecs="avc1.4d401f" bandwidth="2000000" width="960" height="540" sar="1:1">
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
<Representation id="3" mimeType="video/mp4" codecs="avc1.4d401f" bandwidth="2800000" width="1280" height="720" sar="1:1">
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
<Representation id="4" mimeType="video/mp4" codecs="avc1.4d4028" bandwidth="5000000" width="1920" height="1080" sar="1:1">
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" startWithSAP="1" segmentAlignment="true" bitstreamSwitching="true" lang="rus">
<Representation id="5" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="128000" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
<SegmentTemplate timescale="1000000" duration="10000000"
initialization="init-$RepresentationID$.m4s"
media="chunk-$RepresentationID$-$Number%05d$.m4s"
startNumber="1">
</SegmentTemplate>
</Representation>
</AdaptationSet>
</Period>
</MPD>
Это файл XML, в котором представлены основные параметры видео и аудио - общая продолжительность, количество потоков, форматы и имена файлов сегментов. В файле можно выделить такие сведения:
mediaPresentationDuration="PT53.3S"
- общая продолжительность видеоmaxSegmentDuration="PT10.0S"
- ожидаемая длина сегмента (см. параметр seg_duration
выше)<AdaptationSet>
- группировка потоков аудио и видео.
В данном случае, в одной группе находятся потоки, полученные из перекодирования одного и того же исходного потока - например здесь сгруппированы видео с разным битрейтом.
Аудио-поток находится в отдельной группе и, если в исходном файле было бы несколько потоков, то в mpd каждый находился бы в своей группе.
Также в AdaptationSet описывается тип потока и некоторые общие сведения (например соотношение сторон и частота кадров для видео).<Representation>
- описывает один поток.
Здесь описывается тип потока, кодек, битрейт, для видео - размеры изображения, для аудио - частота дискретизации.<SegmentTemplate>
- описывает, как хранятся файлы сегментов потока:
id
- номер потока,
initialization
- шаблон имени сегмента инициализации (см. параметр init_seg_name
),
media
- шаблон имени файла с данными (см. параметр media_seg_name
).Файлы init-0.m4s
… init-5.m4s
- это сегменты инициализации - они для каждого потока хранят информацию о кодеке (данных аудио и видео в них нет).
Сегменты инициализации скачиваются перед началом воспроизведения потока.
Файлы вида chunk-0-00001.m4s
- это сегменты с данными, они хранят данные для каждого потока видео или аудио.
Формат этих файлов определяется выбранным типом контейнера (см. параметр dash_segment_type
).
В каждом файле хранится фрагмент видео или аудио длиной около seg_duration
(в данном примере 10 секунд).
Эти файлы по отдельности могут не воспроизводиться обычными плеерами, т.к. они могут не содержать сведения о кодеке (которые находятся в сегменте инициализации).
Проанализировав файлы и их содержимое можно предположить, что вместо многократного полного перекодирования исходного файла можно попробовать создавать нужные файлы по запросу. Т.е. содержимое файла mpd и сегментов инициализации известно заранее, и их можно быстро сгенерировать. А сегменты с данными можно попробовать получать перекодировав только часть исходного файла (длиной в один сегмент). Этот подход будет описан в следующих разделах.
Проще всего сгенерировать файл mpd и сегменты инициализации. Они не требуют полного кодирования исходного файла - достаточно перекодировать небольшой фрагмент (например секунду). Команда для кодирования будет во многом аналогична исходной:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ffmpeg -hide_banner -y \
-ss 0 -to 1 \
-i input.mp4 \
-r 25 -c:v libx264 -pix_fmt yuv420p -preset veryfast -profile:v main \
-keyint_min 250 -g 250 -sc_threshold 0 \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-map v:0 -filter:v:0 "scale=-2:360" -b:v:0 800k -maxrate:0 856k -bufsize:0 1200k \
-map v:0 -filter:v:1 "scale=-2:432" -b:v:1 1400k -maxrate:1 1498k -bufsize:1 2100k \
-map v:0 -filter:v:2 "scale=-2:540" -b:v:2 2000k -maxrate:2 2140k -bufsize:2 3500k \
-map v:0 -filter:v:3 "scale=-2:720" -b:v:3 2800k -maxrate:3 2996k -bufsize:3 4200k \
-map v:0 -filter:v:4 "scale=-2:1080" -b:v:4 5000k -maxrate:4 5350k -bufsize:4 7500k \
-map 0:a \
-init_seg_name "init-\$RepresentationID\$.\$ext\$" \
-media_seg_name "chunk-\$RepresentationID\$-\$Number%05d\$.\$ext\$" \
-dash_segment_type mp4 \
-use_template 1 \
-use_timeline 0 \
-seg_duration 10 \
-adaptation_sets "id=0,streams=v id=1,streams=a" \
-f dash \
dash.mpd
Добавились параметры -ss 0 -to 1
- они говорят, что нужно взять отрезок видео с 0 до 1 секунды (можно и просто указать -to 1
, в этом случае начало будет неявно установлено на 0).
Для этих параметров важно, располагаются они до -i
или после.
Если до, то поиск идет по исходному файлу (относительно быстро), а если после, то поиск идет по перекодированному файлу.
Т.е. если нужно взять фрагмент через 30 секунд от начала, то в первом случае FFmpeg отступит 30 секунд в исходном файле и начнет кодирование, а во втором случае первые 30 секунд будут тоже перекодированы.
Получившийся файл dash.mpd
будет аналогичен приведенному выше за одним исключением - mediaPresentationDuration
(т.е. общая длительность видео) будет равно 1 секунде.
Это значение нужно будет заменить на действительную продолжительность видео, которую можно получить при помощи программы ffprobe
- ее применение будет рассмотрено в отдельном разделе.
Также будут созданы сегменты инициализации, их изменение не требуется. Сегменты данных содержат только одну секунду перекодированных потоков, поэтому их можно удалить.
Сегменты с данными аудио и видео-потоков нужно создавать по запросу, выделяя и кодируя нужный фрагмент исходного файла.
Можно предположить, что для этого достаточно просто отмерять от начала файла по 10 секунд на сегмент.
Но на практике возникнет ряд проблем.
Из-за особенностей кодировщика mp4 в FFmpeg и особенностей обработки сегментов mp4 в Video.js, использовать контейнер mp4 для сегментов не получится. Video.js может использовать контейнер MPEG-TS, хотя и с ограничениями на формат видео и аудио - выбор ограничен H.264 для видео и AAC для аудио - поэтому для кодирования аудио нельзя использовать mp3.
Возможность использовать MPEG-TS для сегментов данных - это особенность Video.js, другие плееры могут не поддерживать воспроизведение такого видео.
Следующие две проблемы связаны с кодированием аудио.
Если попробовать разделить аудио-поток на фрагменты ровно по 10 секунд и затем склеить их обратно без полного перекодирования (так же будет происходить склейка сегментов при воспроизведении), то можно будет заметить, что длина получившегося файла будет немного больше и на месте склеек появятся фрагменты длиной в несколько миллисекунд, не содержащие звука (см. изображение ниже). При воспроизведении эти фрагменты будут давать явно слышимые искажения.
На рисунке пример склейки аудио фрагментов длинной в одну секунду, отмечены места появившихся искажений. Повторить эту склейку можно при помощи таких команд (а результат открыть, например, в audacity):
1
2
3
4
5
ffmpeg -hide_banner -i -ss 0 -to 1 -i input.mp4 -vn -c:a aac audio-1.aac
ffmpeg -hide_banner -i -ss 1 -to 2 -i input.mp4 -vn -c:a aac audio-2.aac
ffmpeg -hide_banner -i -ss 2 -to 3 -i input.mp4 -vn -c:a aac audio-3.aac
echo "file 'audio-1.aac'\nfile 'audio-2.aac'\nfile 'audio-3.aac'" > join.txt
ffmpeg -hide_banner -i -f concat -i join.txt -c:a copy audio.aac
Первая проблема. Кодек AAC делит аудио-данные на фреймы, каждый фрейм содержит 1024 значения. При частоте дискретизации 48000 Гц, получается, что один фрейм содержит 1024 / 48000 = 0.02133333 секунд аудио. Количество фреймов в файле должно быть обязательно целым и, если данных в конце потока не хватает на полный фрейм, то он будет дополнен нулевыми значениями (т.е. отсутствием звука, тишиной). Получается, что в худшем случае неровное разделение на фреймы может давать примерно 0.021 лишних секунд к общей длине аудио-потока на каждую склейку, и это будет одним из источников искажений звука.
Решить эту проблему можно подбирая длину сегмента, кратную длине фрейма, т.е.
Вторая проблема. Кодек AAC всегда добавляет в начало файла один пустой фрейм. Т.е. при частоте дискретизации 48000 Гц каждый сегмент будет начинаться с примерно 0.021 секунд тишины. При склейке аудио без перекодирования эти фреймы сохраняются и тоже являются источником искажений звука. Средствами FFmpeg отключить создание пустого фрейма нельзя, вырезать ровно один фрейм из аудио тоже не получается.
Решить проблему можно, если сначала перенести начало сегмента на AudioOverhead секунд раньше, а затем вырезать фрагмент начиная с AudioOverhead секунды без перекодирования (т.е. указав -c:a copy
).
Если не было перекодирования, то пустой фрейм не создается и сегмент будет начинаться сразу со звука.
Значение AudioOverhead нужно подобрать кратным длине фрейма AAC, чтобы не получить неполные фреймы.
Пустой фрейм в начале файла - особенность не только AAC, но и других кодеков, кодирующих с потерями, например MP3.
Так как теперь длина сегмента не ровно 10 секунд, то при разделении видео на сегменты может оказаться, что раздел пройдет по кадру (т.е. в сегменте будет не целое число кадров). В худшем случае может получиться, что при склейке один кадр либо пропадет, либо продублируется. Решить проблему можно, подбирая длину сегмента, кратную частоте кадров, т.е.
Длина видео и аудио сегментов не совпадает, поэтому итоговая длина сегмента должна быть кратна и длительности аудио фрейма, и длительности кадра видео. Для частоты кадров видео, равной 25, и частоте дискретизации аудио, равной 48000, длина фрейма будет равна 0.32 секунды. Вывод этого значения будет рассмотрен в отдельном разделе.
Теперь можно определить время начала и окончания сегментов:
<SegmentTemplate>
файла mpd)Команда для кодирования одного сегмента видео будет выглядеть примерно так:
1
2
3
4
5
6
7
8
9
10
11
12
ffmpeg -hide_banner -y \
-ss VideoSegmentStart \
-to VideoSegmentEnd \
-i input.mp4 \
-copyts -start_at_zero \
-an \
-r 25 -c:v libx264 -pix_fmt yuv420p -preset veryfast -profile:v main \
-keyint_min 250 -g 250 -sc_threshold 0 \
-map v:0 -filter:v:0 "scale=-2:360" -b:v:0 800k -maxrate:0 856k -bufsize:0 1200k \
-f mpegts \
-muxdelay 0 -muxpreload 0 \
chunk-video.m4s
Здесь добавились новые параметры:
-copyts -start_at_zero
- у сегментов есть время начала и нужно, чтобы каждый следующий сегмент начинался там, где заканчивается предыдущий, но при перекодировании время сбрасывается и все сегменты начинаются с нуля.
Эти параметры оставляют исходное время начала (это немного упрощенное объяснение, но задачу параметры решают именно такую)-an
- исключить аудио-поток-f mpegts
- использовать формат MPEG-TS-muxdelay 0 -muxpreload 0
- по умолчанию FFmpeg для формата MPEG-TS добавляет небольшую задержку в начало файла (т.е. он начинается не с нуля), эти параметры задержку отключают.Выбор параметров кодирования видео зависит от номера потока, для которого нужно сгенерировать сегмент.
Для кодирования аудио нужно две команды. Первая перекодирует фрагмент аудио-потока исходного файла в AAC:
1
2
3
4
5
6
7
8
9
10
11
ffmpeg -hide_banner -y \
-ss AudioSegmentStart \
-to AudioSegmentEnd \
-i input.mp4 \
-copyts -start_at_zero \
-vn \
-map a:0 \
-c:a aac -b:a 128k -ac 2 -ar 48000 \
-f mpegts \
-muxdelay 0 -muxpreload 0 \
chunk-audio.tmp.m4s
Вторая уберет пустой первый фрейм:
1
2
3
4
5
6
7
8
ffmpeg -hide_banner -y \
-ss AudioSegmentTrimStart \
-i chunk-audio.tmp.m4s \
-c:a copy -vn \
-copyts \
-f mpegts \
-muxdelay 0 -muxpreload 0 \
chunk-audio.m4s
-vn
- исключить видео-поток-c:a copy
- копировать аудио-поток без перекодированияЕсли использовать такие команды для кодирования сегментов по запросу и плеер Video.js для воспроизведения, то результат будет аналогичен варианту с полным кодированием исходного файла. Такая логика работы с сегментами реализована в проекте gifr.
Комментарий по производительности. Если перекодированные сегменты данных сохранять и при повторном запросе вместо перекодирования отдавать ранее сохраненный сегмент, то для воспроизведения всего файла потребуется столько же вычислений, сколько и для полного перекодирования. Отличие только в том, что вычисления будут выполняться не все сразу, а будут распределены по времени. При полном перекодировании нагрузка будет более предсказуемой - пока обработка файла не завершится, он не будет доступен для воспроизведения. При перекодировании по запросу долгого ожидания доступности файла не будет, можно начинать воспроизведение, как только будет готов первый сегмент, но в таком случае сложно предсказать, успеет ли завершиться перекодирование следующего сегмента за время воспроизведения текущего.
Формат DASH позволяет включать в видео несколько аудио-потоков, а Video.js позволяет во время воспроизведения их переключать.
Для этого каждый поток должен располагаться в отдельной секции <AdaptationSet>
.
Иначе, если все потоки собрать в одну секцию, то плеер это воспримет как варианты одного потока и возможности переключения не будет.
Кодируются дополнительные аудио-потоки так же, номер потока нужно выбирать в параметре -map a:0
.
Формат DASH поддерживает субтитры к видео, но FFmpeg эту функциональность при кодировании DASH не поддерживает.
Плеер Video.js самостоятельно поддерживает отображение субтитров, для этого их нужно перекодировать в формат WebVTT и передать в параметре конфигурации tracks
.
Перекодировать можно с помощью FFmpeg:
1
2
3
4
ffmpeg -hide_banner -y \
-i input.mp4 \
-map s:0 -c:s webvtt \
subtitles.webvtt
-map s:0
- выбрать первые субтитры в исходном файле-c:s webvtt
- перекодировать в формат WebVTTВ параметрах Video.js субтитры добавляются следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
{
...
tracks: [
{
src: '/ссылка/на/файл/субтитров.webvtt',
kind: 'subtitles',
srclang: 'код языка субтитров',
label: 'отображаемое в интерфейсе название'
}
],
...
}
Для получения разнообразной информации о видео файле: длительности, о количестве, видах и свойствах потоков, можно использовать программу ffprobe
, она обычно устанавливается вместе с FFmpeg.
Получить информацию о потоках в формате, удобном для программного разбора, можно такой командой:
1
2
3
4
ffprobe -loglevel error \
-show_entries streams \
-of default=noprint_wrappers=1:nokey=0 \
input.mp4
Будет выведена примерно такая информация:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
index=0
codec_name=h264
codec_long_name=H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
profile=Main
codec_type=video
codec_tag_string=avc1
codec_tag=0x31637661
width=1920
height=1080
coded_width=1920
coded_height=1080
closed_captions=0
has_b_frames=2
sample_aspect_ratio=1:1
display_aspect_ratio=16:9
pix_fmt=yuv420p
level=40
color_range=tv
color_space=bt709
color_transfer=bt709
color_primaries=bt709
chroma_location=left
field_order=unknown
refs=1
is_avc=true
nal_length_size=4
id=N/A
r_frame_rate=24000/1001
avg_frame_rate=38340000/1599097
time_base=1/90000
start_pts=0
start_time=0.000000
duration_ts=4797291
duration=53.303233
bit_rate=1692919
max_bit_rate=N/A
bits_per_raw_sample=8
nb_frames=1278
nb_read_frames=N/A
nb_read_packets=N/A
...
Информация о каждом потоке начинается со строки index=
.
Тип потока определяется по строке codec_type=
.
Также нужно обратить внимание, что частота кадров определяется в виде дроби.
r_frame_rate
- это частота кадров из метаданных потока, а avg_frame_rate
- это частота, полученная делением количества кадров на длительность потока.
Отдельно нужно рассмотреть определение длительности видео. То, что выводится в информации о потоках - это длительность, записанная в метаданных потока, она иногда может отсутствовать или быть неправильной. Для определения правильной длительности файла можно использовать команду:
1
2
3
4
ffprobe -loglevel error \
-show_entries format=duration \
-of default=noprint_wrappers=1:nokey=0 \
input.mp4
Результатом будет длительность в секундах:
duration=53.376000
Нужно определить такое минимальное количество секунд, в которое помещается целое количество кадров видео и фреймов аудио (AAC). Видео рассматривается с частотой кадров 25, аудио с частотой дискретизации 48000.
Длительность одного кадра видео будет равна:
\[VideoFrameDuration = \frac{1}{25} = \frac{1}{5^2}\]Рассмотрим длительность фрейма аудио:
\[AudioFrameDuration = \frac{1024}{48000} = \frac{2^{10}}{2^7 \cdot 3 \cdot 5^3} = \frac{2^3}{3 \cdot 5^3} = \frac{2^3}{3 \cdot 5} \cdot \frac{1}{5^2} = \frac{2^3}{3 \cdot 5} \cdot VideoFrameDuration\]Чтобы получить целое число, нужно умножить AudioFrameDuration на 15, получится:
\[15 \cdot AudioFrameDuration = 15 \cdot \frac{2^3}{3 \cdot 5} \cdot VideoFrameDuration = 8 \cdot VideoFrameDuration\]Т.е. длительность пятнадцати фреймов аудио равна длительности восьми кадров видео. В итоге минимальная длительность фрейма для аудио и видео равна:
\[8 \cdot VideoFrameDuration = 8 \cdot \frac{1}{25} = 0.32\]Примеры