Один из проектов нашей компании — это сервис 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. Приведенный ниже код загружает первый мегабайт видеофайла и воспроизводит его:
Один важный момент для работы кода: объект NetStram должен быть в режиме создания данных, который включается так NetStream.play(null). Это должно быть сделано до того как поступят первые видеоданные.
Дальше по мере надобности следует подгружать оставшиеся части файла такими же кусками по 1 мегабайту. Размер в 1 мб (равен примерно 15 секундам видео) был получен экспериментально в ходе большого количества тестов и для нашей системы. Подгрузкой управляет таймер, который выполняет следующий код по событию.
Загрузку будем запускать, при соблюдении следующих условий:
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 файла и обнуляет счетчики воспроизведения в классе. Вот пример кода перематывающего видео:
times — массив смещения по времени навигационных точек;
positions — массив смещения по байтам навигационных точек;
Одной особенностью библиотеки HTTPClientLib является то, что файл с политикой безопасности сокетов (crossdomain.xml) при кросс доменной работе запрашивается с 843 порта сервера, в отличии от стандартных объектов, которые могут подцепить его и с 80-го. Поэтому в конфигурации сервера nginx была добавлена следующая запись:
Применение плеера, который загружает видео по данной методике, позволило значительно сократить объем “лишнего — паразитного” трафика, и снизить нагрузку на потоковые сервера. В среднем на 200 000 просмотров трафик снизился на 30%. Однако, вышеизложенная методика имеет и “негативную” сторону, а именно: наличие у пользователя минимум 10.1 версии Flash Player.
Полный пример кода
Все было хорошо, пока количество просмотров не достигло уровня, когда не только сетевые каналы серверов оказались перегружены, но и перестала справляться с запросами на чтение дисковая подсистема серверов.
Проблемы с дисковой подсистемой решили просто установкой 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.
Полный пример кода
