Red5: Практика работы с потоковым мультимедиа. Часть 10

Эта статья завершит серию материалов, посвященных задачам создания мультимедиа-приложений, использующих возможности flash и java. Прошлая статья рассказывала о том, как мультимедиа-сервер red5 умеет “отдавать” клиенту поток видеоинформации. Так, мы создали простенький видеопроигрыватель, который умел загружать и показывать видео и как обычный flv-файл, и как мультимедиа-поток, формируемый red5-сервером. Сегодня пришло время рассмотреть вторую сторону этой задачи: захват видеопотока с веб-камеры и отправка его на red5-сервер.

Благодаря тому, что flash как продукт ориентируется на создание интерактивных мультимедиа-приложений, то задача захвата видео сводится буквально к паре строк кода, оперирующих с высокоуровневым объектом Camera. Самым первым шагом я покажу, как можно присоединить камеру (и соответственно поступающий с нее видеопоток) к mxml-компоненту VideoDisplay. Про VideoDisplay я рассказывал в прошлой статье, когда показал, как VideoDisplay с помощью свойства source умеет загружать с red5-сервера видеофайл. Далее я показываю заготовку простого mxml-приложения, состоящего из компонента VideoDisplay и кнопки, по нажатию на которую я присоединю к VideoDisplay веб-камеру (естественно, что сама веб-камера должна быть уже подключена к вашему компьютеру). Сначала я приведу пример mxml-разметки, создающей заготовку пользовательского интерфейса:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script><![CDATA[
]]></mx:Script>
<mx:VBox paddingTop="10" paddingLeft="10">
<mx:Button click="grab(event)" label="grab camera"/>
<mx:VideoDisplay id="videoPublisher" width="320" height="240" />
</mx:VBox>
</mx:Application>
Теперь рассмотрим функцию “grab”, которая вызывается по нажатию на кнопку “grab camera”:
public function grap(event:MouseEvent):void {
var cam:Camera = = Camera.getCamera();
videoPublisher.attachCamera(cam); }
}

<img1>Итак, сразу после того как вы нажмете на кнопку “подключиться к веб-камере”, должно появиться диалоговое окно, в котором flash player спрашивает у нас, т.е. у пользователя, хочет ли тот разрешить доступ к веб-камере для текущего приложения (см. рис. 1). Если мы ответим утвердительно, то внизу формы приложения появится окошко с видеоизображением, поступающим в текущий момент с веб-камеры (см. рис. 2). К слову, flash player требует, чтобы размер вашего приложения был не менее чем 215 на 138 пикселей, что необходимо для того, чтобы отобразить диалоговое окошко с запросом “разрешить ли доступ к веб-камере”. Если размер окна будет менее требуемой величины, то с камерой вы работать не сможете.

<img2>Вызов метода Camera.getCamera() возвращает нам ссылку на веб-камеру, подключенную к машине клиента, а в том случае, если камеры нет, метод getCamera вернет значение null. Так что имеет смысл дополнить код предыдущего примера проверкой объекта “cam” на null. После получения ссылки на веб-камеру мы можем “опросить” ее и узнать некоторые полезные характеристики. Так, мы можем узнать название камеры, разрешение (размер записываемой области) и частоту кадров в секунду, с которой камера может работать:

public function grap(event:MouseEvent):void {
var cam : Camera = Camera.getCamera();
if (cam == null){
Alert.show("Веб-камера не доступна");
return; }
Alert.show("информация о камере: name: "+cam.name+", resolution: "+ cam.width+" * "+ cam.height+", fps: "+ cam.fps);
videoPublisher.attachCamera(cam);
}

Приведенный выше пример кода не идеален и демонстрирует одну распространенную ошибку работы с веб-камерой. Мы должны учитывать тот момент, что клиент может нажать кнопку отмены в появляющемся перед ним диалоге “разрешить использовать веб-камеру”. Поэтому нельзя сразу после получения ссылки на камеру “присоединять” ее к объекту VideoDisplay. Необходимо зарегистрировать специальную функцию, слушающую событие “пользователь разрешил или запретил работу с камерой”.

В следующем примере я создал функцию onCameraStatus и привязал его к событию “изменение статуса камеры”. Внутри этой функции я анализирую свойство muted и либо прячу объект VideoDisplay, либо показываю его и присоединяю к нему веб-камеру (если значение muted равно true, то доступ к веб-камере запрещен). Для удобства я сам явно вызвал функцию onCameraStatus в самый первый раз после получения ссылки на камеру. Также для удобства я решил спрятать объект VideoDisplay так, чтобы при открытии веб-странички его не было изначально видно. Для этого я добавил к объявлению компонента VideoDisplay свойство visible и установил его значение в false. А теперь пример обновленного кода:

var cam:Camera = null;
public function grap(event:MouseEvent):void {
cam = Camera.getCamera();
if (cam == null) {
Alert.show("Веб-камера не доступна");
return; }
cam.addEventListener(flash.events.StatusEvent.STATUS, onCameraStatus);
onCameraStatus (null);
}
private function onCameraStatus (e: flash.events.StatusEvent):void{
if (! cam.muted){
videoPublisher.visible = true;
videoPublisher.attachCamera(cam); }
else
videoPublisher.visible = false;
}

Для того чтобы проверить работоспособность примера, попробуйте после нажатия на кнопку “grab camera” вызвать по правой кнопке мыши контекстное меню на flash-ролике и выбрать пункт “Параметры”. Затем попробуйте переключать “галочку” с пункта “разрешено” на “запрещено” и обратно. Вы увидите, что VideoDisplay, показывающий изображение с камеры, будет динамически прятаться и показываться в зависимости от вашего выбора. Одна из самых полезных функций, которая только есть при работе с веб-камерой, – это механизм обнаружения активности. Что это такое, можно объяснить на простом примере. Предположим, что вы хотите использовать веб-камеру как часть системы контроля за безопасностью помещений, то есть хотите записывать на жесткий диск как видеофайл все, что происходит в помещении. В этом случае имеет смысл не записывать изображение постоянно, а только тогда, когда в кадре происходят какие-то изменения. Это позволит экономить как место на диске, так и (в случае передачи данных по Интернету) трафик. Вся эта “магия” работает благодаря тому, что после включения камеры она постоянно отслеживает так называемый activityLevel. В любой момент времени вы можете узнать значение этого самого activityLevel, просто обратившись к одноименному свойству внутри объекта Camera. Так вот, когда в течение некоторого времени activityLevel был меньше, чем установленное вами пороговое значение, то flash player известит вас об этом, вызвав метод-обработчик события ActivityEvent.ACTIVITY. Аналогично, вас известят, вызвав этот же метод, когда камера зарегистрирует какое- то движение, превысившее пороговое значение activityLevel. Важно, что переход камеры в “неактивное” состояние вовсе не означает того, что она не будет записывать видео или не будет показывать его на присоединенном компоненте VideoDisplay. Просто вас будут извещать о наступлении таких событий, а как вы будете использовать эту информацию – только ваше дело:

var cam:Camera = null;
public function grap(event:MouseEvent):void {
cam = Camera.getCamera();
if (cam == null) {
Alert.show("Веб-камера недоступна");
return;
}
cam.setMotionLevel(5, 2000);
cam.addEventListener(ActivityEvent.ACTIVITY, activityHandler);
}

private function activityHandler(e:ActivityEvent):void {
if (e.activating) {
labActivity.text = "Activated at "+ new Date ().toString() + “, activity level is”+ cam.activityLevel;
} else {
labActivity.text = "Deactivated at "+ new Date ().toString();
} }

Пример построен вокруг вызова функции setMotionLevel, первым параметром которой является значение переменной activityLevel. ActivityLevel принимает значения в отрезке от 0 до 100 (со значением по умолчанию равным 50). Второй параметр функции setMotionLevel – это количество миллисекунд, которые будет отсчитывать flash player перед тем как известить наше приложение о том, что уровень активности упал ниже предельного. Естественным шагом после того, как мы научились определять доступность у конкретного клиента веб-камеры, определять свойства камеры и присоединять ее к VideoDisplay, будет отправка видеопотока с камеры на сервер для последующего его сохранения в виде flv-файла. Для простейшей реализации подобной функции не требуется никаких дополнительных настроек на стороне red5-сервера. Я решил воспользоваться все тем же старым примером red5-приложения на java, с которым мы работали в прошлых статьях, но очистил все содержимое класса HelloApplication. А также я вернул к своему оригинальному виду конфигурационный файл red5-web.xml, то есть удалил объявление бина “streamFilenameGenerator” (см. прошлую статью для пояснения, для чего был нужен этот самый “streamFilenameGenerator”). Также я поменял внешний вид приложения, поместив в самом верху формы две кнопки: кнопку записи видео и кнопку просмотра, а внизу формы были размещены два элемента VideoDisplay и UIControl. Первый из них будет использоваться для просмотра захватываемого видео с камеры перед его отправкой на сервер, а второй компонент будет служить для одновременного с этим просмотра видео, загружаемого с сервера. То есть после того как приложение будет запущено, с ним может работать любое количество клиентов, из которых один должен иметь веб-камеру и публиковать свой видеопоток, а все остальные клиенты будут способны просматривать публикуемую видеоинформацию. Поскольку нужно показывать только один из видеоэкранов, в зависимости от того, какой входящий или исходящий видеопоток он записывает, то я разместил компоненты videoPlayer и videoPublisher внутри специального mxml-компонента ViewStack. В будущем я смогу внутри actionscript-кода переключать видимый видеоэкран с videoPlayer на videoPublisher и обратно (изменяя свойство selectedChild):

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" >
<mx:Script><![CDATA[
]]></mx:Script>
<mx:VBox paddingTop="10" paddingLeft="10">
<mx:HBox>
<mx:Button click="grab(event)" label="grab camera"/>
<mx:Button click="play(event)" label="show video stream"/>
</mx:HBox>
<mx:ViewStack id="pages">
<mx:Panel id="panePublisher">
<mx:VideoDisplay id="videoPublisher" width="320" height="240" />
</mx:Panel>
<mx:Panel id="panePlayer">
<mx:UIComponent id="videoPlayer" width="320" height="240" />
</mx:Panel>
</mx:ViewStack>
</mx:VBox>
</mx:Application>

Теперь мне нужно привести пример кода, размещенного внутри секции “mx:Script” и обрабатывающего нажатия на кнопки grab и play:

import mx.controls.Alert;

private var ncPublish:NetConnection;
private var nsPublish:NetStream;
private var ncPlay:NetConnection;
private var nsPlay:NetStream;
private var cam:Camera = null;

private function grab(event:MouseEvent):void {
cam = Camera.getCamera();
if (cam == null) {
Alert.show("Веб-камера недоступна");
return;
}
if (cam.muted) {
Alert.show("Доступ к веб-камере запрещен");
return;
}
pages.selectedChild = panePublisher;
videoPublisher.attachCamera(cam);
ncPublish = new NetConnection();
ncPublish.objectEncoding = ObjectEncoding.AMF0;
ncPublish.addEventListener(NetStatusEvent.NET_STATUS, netStatusPublish);
ncPublish.connect('rtmp://localhost/warmodule/');
}

private function netStatusPublish(event:NetStatusEvent):void {
if (event.info.code == 'NetConnection.Connect.Success') {
nsPublish = new NetStream(ncPublish);
nsPublish.attachCamera(cam);
nsPublish.publish("my-home-video", "append");
nsPublish.pause()
} }

private function play(event:MouseEvent):void {
pages.selectedChild = panePlayer;
ncPlay = new NetConnection();
ncPlay.objectEncoding = ObjectEncoding.AMF0;
ncPlay.addEventListener(NetStatusEvent.NET_STATUS, netStatusPlay);
ncPlay.connect('rtmp://localhost/warmodule/');
}

private function netStatusPlay(event:NetStatusEvent):void {
if (event.info.code == 'NetConnection.Connect.Success') {
nsPlay = new NetStream(ncPlay);
var v:Video = new Video();
v.width=320;
v.height=240;
v.attachNetStream(nsPlay);
videoPlayer.addChild(v);
nsPlay.play("my-home-video");
}
}

Вначале кода я объявил пять переменных: cam – для ссылки на веб-камеру и по две пары переменных, служащих для связи с red5-сервером. Так, пара ncPublish и nsPublish представляет собой объект “подключение” к red5-серверу и объект “поток”, по которому информация будет публиковаться на сервер от клиента с веб-камерой. Вторая пара переменных, ncPlay и nsPlay, представляет собой “подключение” и “поток”, по которым видеоинформация будет загружаться с red5-сервера. Поскольку недопустима одновременная работа flash-приложения и в режиме публикации, и в режиме просмотра видеопотока, то можно было обойтись всего двумя переменными, NetConnection и NetStream (также можно было бы обойтись всего одним общим компонентом VideoDisplay), но я решил их разделить для большей наглядности. Далее рассмотрим, что происходит, когда пользователь нажимает на кнопку “начать захват видео” и вызывается функция grab. Внутри этой функции я выполняю ряд проверок: на предмет физического наличия веб-камеры и наличия разрешения на работу с ней. Если какое-то из этих условий не выполняется, то я завершаю работу с соответствующим сообщением об ошибке. Если же все было хорошо, то я создаю и настраиваю объект NetConnection и привязываю к нему функцию netStatusPublish, которая вызывается, когда соединение будет установлено. Если и здесь не возникло проблем, то я создаю объект NetStream, непосредственно служащий для публикации видеопотока, привязываю к нему веб-камеру (attachCamera) и вызываю метод publish, в качестве параметров для которого выступают, во-первых, имя flv-файла, в который red5 будет сохранять поступающее видеоизображение, а вторым параметром указывается режим работы с файлом.

<img3>Так, возможны три кодовых значения: record – когда видео сохраняется в файл, но если файл уже существует, то он будет затерт. В режиме append ранее существовавший файл не будет утерян, а будет дополнен новым видеорядом. Режим live служит для публикации видеопотока без сохранения его в видеофайл. В любом случае, новый файл будет размещен в подкаталоге streams в корне каталога с вашим веб-приложением (см. рис. 3). Если вы хотите, чтобы записываемый видеофайл размещался в каком-то другом месте, то нужно использовать точно такую же методику со специальным классом “подсказчиком”, что я демонстрировал в прошлой статье, когда показывал, как можно хранить в произвольном каталоге те видеофильмы, что клиент может загружать для просмотра. Тогда мы создали свой класс, реализующий интерфейс IStreamFilenameGenerator, и зарегистрировали его в файле red5- web.xml.

<img4>Теперь рассмотрим, что происходит, когда другой клиент хочет подключиться к публикуемому видеоряду и нажимает на кнопку “просмотр видеопотока”, тем самым вызывая функцию play. Здесь нет ничего незнакомого нам после прошлой статьи: нет никакой разницы в методике подключения к статическому или динамически формируемому видеофайлу. Так, внутри метода play я создал объект NetConnection, присоединил его к red5-серверу и указал, какой метод нужно вызвать, когда соединение будет фактически установлено (netStatusPlay). Внутри функции netStatusPlay я создаю объект NetStream, присоединяю его к видеопрогрывателю Video и запускаю просмотр видео, вызвав метод play с таким же именем видеопотока, которое я использовал ранее для публикации файла. В результате я получу картинку, показанную на рис. 4, где показано, как приложение, запущенное в браузере Opera, играет роль источника видеопотока и передает его на сервер, где видео сохраняется в виде статического flv-файла. А также этот же видеопоток идет на вход остальным двум flash-клиентам (в браузерах Chrome и Internet Explorer).

Сегодняшняя статья завершает серию материалов, посвященных работе с red5 и flash. Хотя первоначально я планировал сделать совсем небольшую серию из двух-трех статей, посвященных только работе с видео (фактически это материал сегодняшней и прошлой статей), но так получилось, что я начал более обстоятельный рассказ и затронул множество других сложных и важных тем, объединенных общей целью. Целью создания приложения, активно использующего современные технологии; приложения, демонстрирующего различные методики взаимодействия между flash и java-стороной с помощью методики обратных вызовов и SharedObject. Также были показаны методики сохранения состояния приложения (SharedObject) на жесткий диск в виде файлов или базы данных. Естественно, что некоторые темы остались «за кадром», но если у вас возникнут вопросы, то вы всегда можете задать их мне по электронной почте.

black-zorro@tut.by black-zorro.com


Компьютерная газета. Статья была опубликована в номере 06 за 2010 год в рубрике программирование

©1997-2024 Компьютерная газета