Один из проектов нашей компании — это сервис online-видео, аналогичный youtube. Для вещания и реализации возможностей стриминга используется замечательный веб-сервер nginx с модулем ngx_http_flv_module.
Все было хорошо, пока количество просмотров не достигло уровня, когда не только сетевые каналы серверов оказались перегружены, но и перестала справляться с запросами на чтение дисковая подсистема серверов.
Проблемы с дисковой подсистемой решили просто установкой 10-ого рейда. Но с сетью надо было что-то делать и появилась идея загружать файлы для просмотра не одним потоком, а дискретно, кусками одинакового размера. Благо во flash’e было реализовано все, что нужно. Согласно документации в объекте NetStream есть метод appendBytes() с нужным функционалом. Вот выдержка из официальной документации:
“Передает объект ByteArray в экземпляр NetStream для воспроизведения. Вызовите этот метод для объекта NetStream в режиме создания данных. Чтобы перевести объект NetStream в режим создания данных, вызовите метод NetStream.play(null) для экземпляра NetStream, созданного для объекта NetConnection, который подключен к null. Метод appendBytes() нельзя вызывать для объекта NetStream, который не находится в режиме создания данных, в противном случае выдается исключение.”
Казалось бы, берем класс Loader, прописываем заголовок Range, но нас тут поджидает мина от разработчиков Adobe. В стандартных классах Action Script, которые работают с протоколом HTTP, недоступно использование заголовка Range. Опять же, из официальных источников становится известно, что заголовок Range заблокирован в коде классов, работающих в протоколом HTTP, из соображений безопасности еще в версии плеера 9. Но радовало одно: что блокировка не нативная, а в самих классах, из чего следовало, что можно написать свой HTTP-клиент, используя базовый класс сокета Soсket. Благо в закромах google нашелся уже готовый класс HTTPClientLib. Приведенный ниже код загружает первый мегабайт видеофайла и воспроизводит его:
private var ns:NetStream;
private var video:Video;
private var meta:Object;
private var client:HttpClient;
private var filesize:Number = 0;
private var loadedBytes:Number = 0;
private var data:ByteArray = new ByteArray();
private var datadelta:Number = 1024*1024;
//Переменная содержит ссылку на видео-файл
private var file:String = %ссылка на видео файл%;
private function init():void{
//Инициируем объект, который получит метаданные загруженного видео
var nsClient: Object = {};
//Собственно функция обработчик события на получение метаданных
nsClient.onMetaData = metaDataHandler;
//Объект соединения с сервером; в нашем варианте он создается с ссылкой на объект null так как мы не работаем с медиа-сервером
var nc:NetConnection = new NetConnection();
nc.connect(null);
//Собственно объект, который будет управлять получением данных и воспроизведением самого видео
ns = new NetStream(nc);
ns.client = nsClient;
//Событие на обработки изменения статуса
ns.addEventListener(NetStatusEvent.NET_STATUS,netStatusHandler);
//Обработчик ошибок сети
ns.addEventListener(IOErrorEvent.IO_ERROR,nsIOErrorHandler);
//Визуальный компонент который будет показывать видео
video = new Video();
video.attachNetStream(ns);
// Сглаживание картинки. Необязательный параметр, но на низком качестве видео пригодится
video.smoothing = true;
uic.addChild(video);
//Наш объект, который будет грузить данные вместо NetStream
client = new HttpClient();
//Старт загрузки первого мегабайта видео
loadData();
//Включаем режим создания данных объекта NetStream
ns.play(null);
}
private function loadData():void{
//Объект с ссылкой на видео
var uri:URI = new URI(file);
//Объект, в котором можно объявить заголовки
var request:HttpRequest = new Get();
//maxdata показывает конечную границу диапазона загрузки
//loadedBytes - это начальная граница диапазона
var maxdata:Number = loadedBytes+datadelta;
//Формируем заголовок с учетом того, что размер файла может быть не кратен нашей дельте загрузки
if (maxdata>=filesize and filesize>0){
request.addHeader('Range','bytes='+loadedBytes+'-');
} else {
request.addHeader('Range','bytes='+loadedBytes+'-'+maxdata);
}
//Обработчик события на получение данных, по мере загрузки сохраняем в буфер data
//В данном случае загружаемые данные лучше сохранять в
//временный буфер потому что при прямой отправке в объект NetStream воспроизведение
//начинает прыгать по временной шкале
client.listener.onData = function(e:HttpDataEvent):void {
var bytes:ByteArray = new ByteArray();
bytes = e.bytes;
bytes.position = 0;
data.writeBytes(bytes);
};
//Обработчик события на конец загрузки файла
client.listener.onComplete = function(e:HttpResponseEvent ):void{
//Увеличиваем нижнюю границу на длину загруженных данных
loadedBytes+=data.length;
Получаем размер файла из заголовка ответа
filesize = Number(e.response.header.getValue('Content-Length'))/1024;
//Добавляем загруженные данные в воспроизведение
ns.appendBytes(data);
//Очищаем буфер
data.clear();
//Тригер состояния загрузки. О нем будет сказано ниже
inLoaded = false;
};
//Отправляем запрос на данные
client.request(uri,request);
}
Один важный момент для работы кода: объект NetStram должен быть в режиме создания данных, который включается так NetStream.play(null). Это должно быть сделано до того как поступят первые видеоданные.
Дальше по мере надобности следует подгружать оставшиеся части файла такими же кусками по 1 мегабайту. Размер в 1 мб (равен примерно 15 секундам видео) был получен экспериментально в ходе большого количества тестов и для нашей системы. Подгрузкой управляет таймер, который выполняет следующий код по событию.
if ((!inLoaded) && (ns.bufferLength <= Math.ceil(loadtime+timeDelta)) && (loadedBytes < filesize)){
inLoaded = true;
loadData();
}
Загрузку будем запускать, при соблюдении следующих условий:
inLoaded = false — индикатор состояния, потока true — грузятся данные, false — нет;
ns.bufferLength <= Math.ceil(loadtime+timeDelta) — в буфере воспроизведения осталось времени меньше или равно времени предыдущей загрузки(loadtime) плюс дельта для запаса (timeDelta);
loadedBytes < filesize — не достигнут конец воспроизводимого файла;
Важный момент в режиме создания данных не генерируются события NetStream.Play.Start, NetStream.Play.Stop. Поэтому надо следить за объемом загруженных данных или проигранным временем видео-ролика.
Переменная timeDelta позволяет подстроиться к качеству канала пользователя: в случае опустошения буфера воспроизведения раньше, чем закончится загрузка следующего фрагмента данных, дельта увеличивается.
Немного о перемотке
В режиме создания данных некоторые методы класса NetStream меняют свой обычный функционал. Это относится и к методу seek() — он осуществляет поиск ключевого кадра (так называемого I-кадра), расположенного ближе всего к указанной точке. В режиме создания данных после вызова метода seek класс начинает игнорировать все передаваемые методом appendBytes() данные, до вызова метода appendBytesAction(). Аргумент метода может иметь следующие значения NetStreamAppendBytesAction.RESET_BEGIN или NetStreamAppendBytesAction.RESET_SEEK, первый подразумевает наличие в свежих данных нового заголовка flv файла и обнуляет счетчики воспроизведения в классе. Вот пример кода перематывающего видео:
//Ищем в массиве навигационных точек, полученном из мета-данных видео-файла, смещения в байтах от начала файла по времени на которое кликнули на прогрессбаре воспроизведения
for (var i:Number=0;i<positions.length;i++){
if ((positions[i]<=seekpositions)&&(positions[i+1]>=seektpositions)){
//Смещаем наш счетчик загруженных данных на новое значение и новая загрузка начнется именно с этого места
loadedBytes = positions[i];
break;
}
}
//Метод seek сбрасывает и игнорирует уже имеющиеся данные
ns.seek(seektime);
//Cообщаем классу NetStream том что мы просто переходим на новое место файла, а не начинаем проигрывать новый
ns.appendBytesAction(NetStreamAppendBytesAction.RESET_SEEK);
//Старт загрузки новых данных
loadData();
times — массив смещения по времени навигационных точек;
positions — массив смещения по байтам навигационных точек;
Пара замечаний
Одной особенностью библиотеки HTTPClientLib является то, что файл с политикой безопасности сокетов (crossdomain.xml) при кросс доменной работе запрашивается с 843 порта сервера, в отличии от стандартных объектов, которые могут подцепить его и с 80-го. Поэтому в конфигурации сервера nginx была добавлена следующая запись:
server {
listen 843;
server_name localhost;
location / {
rewrite ^(.*)$ /crossdomain.xml;
}
error_page 400 /crossdomain.xml;
location = /crossdomain.xml {
root /home/www-root;
default_type text/x-cross-domain-policy;
}
}
Итоги
Применение плеера, который загружает видео по данной методике, позволило значительно сократить объем “лишнего — паразитного” трафика, и снизить нагрузку на потоковые сервера. В среднем на 200 000 просмотров трафик снизился на 30%. Однако, вышеизложенная методика имеет и “негативную” сторону, а именно: наличие у пользователя минимум 10.1 версии Flash Player.
Автор: Mu57Di3