WCS предоставляет возможность захвата медиапотока из файла MP4, расположенного на локальном диске сервера (Video on Demand, VOD). Полученный поток можно воспроизвести, ретранслировать, управлять им, как любым потоком на WCS-сервере. Прежде всего, данная возможность предназначена для воспроизведения записанных ранее трансляций в браузере или мобильном приложении клиента.
Описание
Для захвата VOD из файла в качестве имени потока при вызове функции session.createStream() должна быть указана ссылка на файл в виде:
vod://sample.mp4
где sample.mp4 - имя файла, который должен находиться в каталоге /usr/local/FlashphonerWebCallServer/media. Начиная со сборки 5.2.687, каталог для размещения файлов, может быть указан при помощи настройки в файле flashphoner.properties
media_dir=/usr/local/FlashphonerWebCallServer/media
В случае, если файл с таким именем отсутствует, сервер вернет сообщение StreamStatusEvent FAILED, в поле "info" которого будет указан диагноз "File not found".
Поток, созданный таким образом, предназначен для трансляции одному пользователю (персональный VOD). На этот поток нельзя подписаться, его нельзя транскодировать, добавить в микшер или проиграть по HLS
В случае, если необходимо организовать полноценную онлайн-трансляцию, следует указать ссылку на файл в виде:
vod-live://sample.mp4
К такому потоку могут подключиться одновременно несколько пользователей в реальном времени, он может быть транскодирован, добавлен в микшер или воспроизведен по HLS.
Поддерживаемые форматы и кодеки
- Контейнер: MP4
- Видео: H.264
- Аудио: AAC
Схема работы
- Браузер соединяется с сервером по протоколу Websocket и отправляет команду publish.
- Браузер захватывает микрофон и камеру и отправляет WebRTC поток H.264 + AAC на сервер с параметром record: true.
- WCS-сервер записывает поток в файл.
- Браузер останавливает публикацию.
- Второй браузер устанавливает соединение по Websocket, создает поток с указанием имени файла и отправляет команду play.
- Второй браузер получает WebRTC поток и воспроизводит этот поток на странице.
Краткое руководство по тестированию
1. Для теста используем веб-приложение Player для воспроизведения файла.
2. Загрузите файл в каталог /usr/local/FlashphonerWebCallServer/media/
3. Откройте веб-приложение Player, укажите в поле Stream имя файла:
4. Нажмите Start. Начнется воспроизведение файла:
5. Нажмите Stop для остановки воспроизведения.
6. Удалите файл из каталог /usr/local/FlashphonerWebCallServer/media/
7. Нажмите Start. Отобразится статус FAILED и сообщение "File not found":
Последовательность выполнения операций
Ниже описана последовательность вызовов при использовании:
примера Stream Recording для публикации потока и записи файла
примера Player для воспроизведения VOD-потока
1. Установка соединения с сервером для публикации и записи потока.
Flashphoner.createSession(); code
Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function(session){
...
});
2. Получение от сервера события, подтверждающего успешное соединение.
ConnectionStatusEvent ESTABLISHED code
Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function(session){
setStatus(session.status());
//session connected, start playback
publishStream(session);
}).on(SESSION_STATUS.DISCONNECTED, function(){
...
}).on(SESSION_STATUS.FAILED, function(){
...
});
3. Публикация потока с указанием признака записи:
stream.publish(); code
session.createStream({
name: streamName,
display: localVideo,
record: true,
receiveVideo: false,
receiveAudio: false
...
}).publish();
4. Получение от сервера события, подтверждающего успешную публикацию потока.
StreamStatusEvent, статус PUBLISHING code
session.createStream({
name: streamName,
display: localVideo,
record: true,
receiveVideo: false,
receiveAudio: false
}).on(STREAM_STATUS.PUBLISHING, function(stream) {
setStatus(stream.status());
onStarted(stream);
}).on(STREAM_STATUS.UNPUBLISHED, function(stream) {
...
}).on(STREAM_STATUS.FAILED, function(stream) {
...
}).publish();
5. Отправка аудио-видео потока по WebRTC
6. Остановка публикации потока.
stream.stop(); code
function onStarted(stream) {
$("#publishBtn").text("Stop").off('click').click(function(){
$(this).prop('disabled', true);
stream.stop();
}).prop('disabled', false);
}
7. Получение от сервера события, подтверждающего остановку публикации потока.
StreamStatusEvent, статус UNPUBLISHED code
session.createStream({
name: streamName,
display: localVideo,
record: true,
receiveVideo: false,
receiveAudio: false
}).on(STREAM_STATUS.PUBLISHING, function(stream) {
...
}).on(STREAM_STATUS.UNPUBLISHED, function(stream) {
setStatus(stream.status());
showDownloadLink(stream.getRecordInfo());
onStopped();
}).on(STREAM_STATUS.FAILED, function(stream) {
...
}).publish();
8. Установка соединения с сервером для воспроизведения потока.
Flashphoner.createSession(); code
Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function(session){
...
});
9. Получение от сервера события, подтверждающего успешное соединение.
ConnectionStatusEvent ESTABLISHED code
Flashphoner.createSession({urlServer: url}).on(SESSION_STATUS.ESTABLISHED, function(session){
setStatus(session.status());
//session connected, start playback
playStream(session);
}).on(SESSION_STATUS.DISCONNECTED, function(){
...
}).on(SESSION_STATUS.FAILED, function(){
...
});
10. Воспроизведение потока.
stream.play(); code
if (Flashphoner.getMediaProviders()[0] === "MSE" && mseCutByIFrameOnly) {
options.mediaConnectionConstraints = {
cutByIFrameOnly: mseCutByIFrameOnly
}
}
if (resolution_for_wsplayer) {
options.playWidth = resolution_for_wsplayer.playWidth;
options.playHeight = resolution_for_wsplayer.playHeight;
} else if (resolution) {
options.playWidth = resolution.split("x")[0];
options.playHeight = resolution.split("x")[1];
}
stream = session.createStream(options).on(STREAM_STATUS.PENDING, function(stream) {
...
});
stream.play();
11. Получение от сервера события, подтверждающего успешное воспроизведение потока.
StreamStatusEvent, статус PLAYING code
stream = session.createStream(options).on(STREAM_STATUS.PENDING, function(stream) {
...
}).on(STREAM_STATUS.PLAYING, function(stream) {
$("#preloader").show();
setStatus(stream.status());
onStarted(stream);
}).on(STREAM_STATUS.STOPPED, function() {
...
}).on(STREAM_STATUS.FAILED, function(stream) {
...
}).on(STREAM_STATUS.NOT_ENOUGH_BANDWIDTH, function(stream){
...
});
stream.play();
12. Прием аудио-видео потока по Websocket и воспроизведение по WebRTC
13. Остановка воспроизведения потока.
stream.stop(); code
function onStarted(stream) {
$("#playBtn").text("Stop").off('click').click(function(){
$(this).prop('disabled', true);
stream.stop();
}).prop('disabled', false);
...
}
14. Получение от сервера события, подтверждающего остановку воспроизведения потока.
StreamStatusEvent, статус STOPPED code
stream = session.createStream(options).on(STREAM_STATUS.PENDING, function(stream) {
...
}).on(STREAM_STATUS.PLAYING, function(stream) {
...
}).on(STREAM_STATUS.STOPPED, function() {
setStatus(STREAM_STATUS.STOPPED);
onStopped();
}).on(STREAM_STATUS.FAILED, function(stream) {
...
}).on(STREAM_STATUS.NOT_ENOUGH_BANDWIDTH, function(stream){
...
});
stream.play();
Циклический захват потока из файла
Для трансляций vod-live поддерживается циклический захват потока, после окончания файла захват начинается сначала. Эта возможность включается настройкой в файле flashphoner.properties
vod_live_loop=true
Захват файла, размещенного на AWS или другом S3 хранилище
Поток может быть захвачен из файла, размещенного на AWS в хранилище S3. В отличие от VOD захвата файла с локального диска, файл, размещенный на внешнем хранилище, загружается и воспроизводится последовательно.
Для захвата VOD из файла на AWS в качестве имени потока при вызове функции session.createStream() должна быть указана ссылка на файл в виде:
vod://s3/bucket/sample.mp4
где
- bucket - имя корзины S3
- sample.mp4 - имя файла
В сборке 5.2.939 добавлена возможность указать полный URL файла в S3 хранилище, это позволяет захватывать файлы из других S3 хранилищ (Digital Ocean, Selectel и т.д.)
Пример для Digital Ocean Spaces
vod://s3/https://ams3.digitaloceanspaces.com/myspace/folder/file.mp4
Пример для Selectel
vod://s3/https://s3.selcdn.ru/mystorage/file.mp4
Схема работы
1. Браузер запрашивает захват потока из файла на AWS
2. WCS сервер направляет запрос AWS
3. Файл загружается на WCS сервер
4. WebRTC поток из файла передается в браузер для воспроизведения
Настройка
Доступ к хранилищу S3
AWS
Для загрузки файлов из AWS необходимо указать в файле настроек flashphoner.properties данные для доступа к хранилищу S3
aws_s3_credentials=zone;login;hash
Здесь
- zone - регион, где размещено хранилище
- login - идентификатор ключа доступа (Access Key ID)
- hash - секретный ключ доступа (Secret Accesss Key)
Пример настройки доступа:
aws_s3_credentials=eu-central-1;AA22BB33CC44DE;DhlAkpZ4adclHhbLwhTNL4hvWTo80Njo
Digital Ocean Spaces
Для загрузки файлов из DO Spaces необходимо указать настройку
aws_s3_credentials=ams3;access_key;secret
Здесь
- ams3 - поддомен digitaloceanspaces.com
- access_key - ключ доступа к хранилищу
- secret - секретный код доступа к хранилищу
Selectel
Для загрузки файлов из Selectel S3 необходимо указать настройку
aws_s3_credentials=ru-1a;login;password
Здесь
- ru-1a - регион хранилища
- login - имя пользователя
- password - пароль
Захват VOD из файла во время загрузки
Чтобы захватывать поток из файла во время его загрузки, необходимо указать следующую настройку
vod_mp4_container_new=true
При низкой пропускной способности канала между WCS и хранилищем S3, либо при недостаточной его стабильности, может быть включена буферизация файла при загрузке. Размер буфера задается в миллисекундах настройкой
vod_mp4_container_new_buffer_ms=10000
В данном случае, размер буфера составит 10 секунд.
Требования к формату файлов
Заголовок (moov) должен всегда располагаться перед данными (mdat). Примерная структура файла должна быть такой:
Atom ftyp @ 0 of size: 32, ends @ 32 Atom moov @ 32 of size: 357961, ends @ 357993 ... Atom free @ 357993 of size: 8, ends @ 358001 Atom mdat @ 358001 of size: 212741950, ends @ 213099951
Проверить структуру файла можно при помощи утилиты AtomicParsley
AtomicParsley file.mp4 -T 1
Если структура файла не соответствует требованиям, файл не будет воспроизводиться. При необходимости, структуру файла можно исправить при помощи ffmpeg без перекодирования
ffmpeg -i bad.mp4 -acodec copy -vcodec copy -movflags +faststart good.mp4
Требования к именам файлов
Официальная документация по AWS S3 не рекомендует использование пробелов наряду с другими специальными символами, но и не запрещает их. Если пробелы в именах файлов все же используются, их необходимо заменять на '%20', например
vod://s3/bucket/sample%20with%20spaces.mp4
Управление VOD при помощи REST API
REST-запрос должен быть HTTP/HTTPS POST запросом в таком виде:
- HTTP: http://test.flashphoner.com:8081/rest-api/vod/startup
- HTTPS: https://test.flashphoner.com:8444/rest-api/vod/startup
Здесь:
- test.flashphoner.com - адрес WCS-сервера
- 8081 - стандартный REST / HTTP порт WCS-сервера
- 8444 - стандартный HTTPS порт
- rest-api - обязательная часть URL
- /vod/startup - используемый REST-метод
REST-методы и статусы ответа
REST-метод | Пример тела REST-запроса | Пример тела REST-ответа | Статусы ответа | Описание |
|---|---|---|---|---|
/vod/startup | {
"uri":"vod-live://sample.mp4",
"localStreamName": "test"
}
| 409 - Conflict 500 - Internal error | Захватить поток из указанного файла | |
| /vod/find | {
"localStreamName": "test"
}
| [
{
"localMediaSessionId": "29ec3236-1093-42bb-88d6-d4ac37af3ac0",
"localStreamName": "test",
"uri": "vod-live://sample.mp4",
"status": "PROCESSED_LOCAL",
"hasAudio": true,
"hasVideo": true,
"record": false,
"loop": false
}
]
| 200 – потоки найдены 404 – потоки не найдены | Найти VOD-потоки по указанному критерию |
/vod/find_all | [
{
"localMediaSessionId": "29ec3236-1093-42bb-88d6-d4ac37af3ac0",
"localStreamName": "test",
"uri": "vod-live://sample.mp4",
"status": "PROCESSED_LOCAL",
"hasAudio": true,
"hasVideo": true,
"record": false,
"loop": false
}
]
| 200 – потоки найдены 404 – потоки не найдены | Найти все VOD-потоки | |
/vod/terminate | {
"uri":"vod://sample.mp4",
"localStreamName": "test"
}
| 200 - поток завершен 404 - поток не найден | Завершить VOD-поток |
Параметры
Имя параметра | Описание | Пример |
|---|---|---|
uri | Имя файла для захвата потока | vod://sample.mp4 |
| localStreamName | Имя создаваемого потока | test |
status | Текущий статус потока | PROCESSED_LOCAL |
| localMediaSessionId | Идентификатор медиасессии | 29ec3236-1093-42bb-88d6-d4ac37af3ac0 |
| hasAudio | В потоке есть аудио | true |
| hasVideo | В потоке есть видео | true |
| record | Поток записывается | false |
| loop | Файл захватывается циклически | false |
Циклический захват потока из файла по запросу
В сборке 5.2.1528 добавлена возможность указать, должен ли файл захватываться циклически, при создании VOD live трансляции по REST API
{
"uri":"vod-live://sample.mp4",
"localStreamName": "test",
"loop": true
}
По умолчанию, если параметр loop не указан, применяется настройка vod_live_loop . Если параметр указан, то, в зависимости от его значения
true- файл будет захватываться циклическиfalse- файл будет проигран однократно, после чего VOD live трансляция остановится
Значение параметра loop имеет приоритет над значением настройки vod_live_loop.
Ограничения
Запрос /rest-api/vod/startup может применяться только для создания VOD live трансляций. При этом, запросы find, find_all и terminate могут быть применены как к VOD, так и к VOD live трансляциям.
Настройка продолжительности публикации VOD потока после отключения подписчиков
По умолчанию, VOD поток остается опубликованным на сервере в течение 30 секунд после отключения последнего подписчика, при условии, что продолжительность файла превышает этот интервал. Данное время может быть изменено при помощи настройки
vod_stream_timeout=60000
В этом случае, VOD поток останется опубликованным в течение 60 секунд.
Известные проблемы
1. AAC фреймы типа 0 не поддерживаются декодером на базе ffmpeg и будут игнорироваться при воспроизведении захваченного потока
Симптомы: предупреждения в клиентском логе:
10:13:06,815 WARN AAC - AudioProcessor-c6c22de8-a129-43b2-bf67-1f433a814ba9 Dropping AAC frame that starts with 0, 119056e500
Решение: переключиться на использование FDK AAC декодера
use_fdk_aac=true
2. Файлы, содержащие B-фреймы, могут проигрываться неплавно, с фризами или артефактами
Симптомы: периодические фризы, артефакты при проигрывании файла через VOD, предупреждения в клиентском логе
09:32:31,238 WARN 4BitstreamNormalizer - RTMP-pool-10-thread-5 It is B-frame!
Решение: перекодировать файл таким образом, чтобы исключить B-фреймы, например
ffmpeg -i bad.mp4 -preset ultrafast -acodec copy -vcodec h264 -g 24 -bf 0 good.mp4
3. При захвате VOD из продолжительного файла или при одновременном захвате нескольких VOD потоков процесс сервера может завершиться с Out of memory
Симптомы: процесс сервера завершается; "Map failed" в серверном логе и в error*.log
19:30:53,277 ERROR DefaultMp4SampleList - Thread-34 java.io.IOException: Map failed
at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:940)
at com.googlecode.mp4parser.FileDataSourceImpl.map(FileDataSourceImpl.java:62)
at com.googlecode.mp4parser.BasicContainer.getByteBuffer(BasicContainer.java:223)
at com.googlecode.mp4parser.authoring.samples.DefaultMp4SampleList$SampleImpl.asByteBuffer(DefaultMp4SampleList.java:204)
at com.flashphoner.media.F.A.A.A$1.A(Unknown Source)
at com.flashphoner.media.M.B.C.D(Unknown Source)
at com.flashphoner.server.C.A.B.A(Unknown Source)
at com.flashphoner.server.C.A.B.C(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Caused by: java.lang.OutOfMemoryError: Map failed
at sun.nio.ch.FileChannelImpl.map0(Native Method)
at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:937)
... 8 more
Event: 1743.157 Thread 0x00007fc480375000 Exception <a 'java/lang/OutOfMemoryError': Map failed> (0x00000000a1d750b0) thrown at [/HUDSON/workspace/8-2-build-linux-amd64/jdk8u161/10277/hotspot/src/share/vm/prims/jni.cpp, line 735]
Решение:
1. Увеличить максимальное число областей виртуальной памяти
sysctl -w vm.max_map_count=262144
и объем виртуальной памяти, выделяемой процессу сервера, заменив в файле /usr/local/FlashphonerWebCallServer/bin/webcallserver строку
ulimit -n 20000
на строки
ulimit -n 20000 ulimit -v 1000000000
2. Начиная со сборки 5.2.57, установить настройку
vod_mp4_container_isoparser_heap_datasource=true





