Мультимедиа-программирование вместе с Red 5 server. Часть 10

February 18, 2010

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

Благодаря тому, что flash как продукт ориентируется на создание интерактивных мультимедиа-приложений, то задача захвата видео сводится, буквально, к паре строк кода, оперирующих с высокоуровневым объектом Camera. Самым первым шагом я покажу то, как можно присоединить камеру (и соответственно поступающий с нее видео-поток) к mxml компоненту VideoDisplay. Про VideoDisplay я рассказывал в прошлой статье, когда показал то, как VideoDisplay с помощью свойства source умеет загружать с red5-сервера видео-файл. Далее я показываю заготовку простого mxml-приложения, состоящего из компонента VideoDisplay и кнопки, по нажатию на которую я присоединю к VideoDisplay веб-камеру (естественно, что сама веб-камера должна быть уже подключена к вашему компьютеру). Сначала я приведу пример mxml-разметки, создающей заготовку пользовательского интерфейса:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  3.   <mx:Script><![CDATA[
  4.  
  5.   ]]></mx:Script>
  6.   <mx:VBox paddingTop="10" paddingLeft="10">
  7.     <mx:Button click="grab(event)" label="grab camera"/>
  8.     <mx:VideoDisplay id="videoPublisher" width="320" height="240" />
  9.   </mx:VBox>
  10. </mx:Application>
Теперь рассмотрим функцию “grab”, которая вызывается по нажатию на кнопку “grab camera”:
  1. public function grap(event:MouseEvent):void {
  2.    var cam:Camera = = Camera.getCamera();
  3.    videoPublisher.attachCamera(cam); }
  4. }
Итак, сразу после того как вы нажмете на кнопку “подключиться к веб-камере”, то должно появиться диалоговое окно, в котором flashplayer спрашивает у нас, т.е. у пользователя, хочет ли разрешить доступ к веб-камере для текущего приложения (см. рис. 1).



Если мы ответим утвердительно, то внизу формы приложения появится окошко с видеоизображением, поступающим в текущий момент с веб-камеры (см. рис. 2).



К слову flashplayer требует, чтобы размер вашего приложения был не менее чем 215 на 138 пикселей, что необходимо для того, чтобы отобразить диалоговое окошко с запросом “разрешить ли доступ к веб-камере”. Если размер окна будет менее требуемой величины, то с камерой вы работать не сможете.

Вызов метода Camera.getCamera() возвращает нам ссылку на веб-камеру, подключенную к машине клиента, а в том случае, если камеры нет, то метод getCamera вернет значение null. Так что имеет смысл дополнить код предыдущего примера проверкой объекта “cam” на null. После получения ссылки на веб-камеру, мы можем “опросить” ее и узнать некоторые полезные характеристики. Так мы можем узнать название камеры, разрешение (размер записываемой области) и частоту кадров в секунду, с которой камера может работать:
  1. public function grap(event:MouseEvent):void {
  2.   var cam : Camera = Camera.getCamera();
  3.   if (cam == null){
  4.      Alert.show("Веб-камера не доступна");
  5.      return; 
  6.   }
  7.   Alert.show("информация о камере: name: "+cam.name+", resolution: "+ cam.width+" * "+ cam.height+", fps: "+ cam.fps);
  8.   videoPublisher.attachCamera(cam);
  9. }
Приведенный выше пример кода не идеален и демонстрирует одну распространенную ошибку работы с веб-камерой. Мы должны учитывать тот момент, что клиент может нажать кнопку отмены в появляющемся перед ним диалоге “разрешить использовать веб-камеру”. Поэтому нельзя сразу после получения ссылки на камеру “присоединять” ее к объекту VideoDisplay. Необходимо зарегистрировать специальную функцию, слушающую событие “пользователь разрешил или запретил работу с камерой”. В следующем примере я создал функцию onCameraStatus и привязал его к событию “изменение статуса камеры”. Внутри этой функции я анализирую свойство muted и либо прячу объект VideoDisplay, либо показываю его и присоединяю к нему веб-камеру (если значение muted равно true, то доступ к веб-камере запрещен). Для удобства я сам явно вызвал функцию onCameraStatus в самый первый раз после получения ссылки на камеру. Также для удобства я решил спрятать объект VideoDisplay так, чтобы при открытии веб-странички его не было изначально видно. Для этого я добавил к объявлению компонента VideoDisplay свойство visible и установил его значение в false. А теперь пример обновленного кода:
  1. var cam:Camera = null;
  2. public function grap(event:MouseEvent):void {
  3.   cam = Camera.getCamera();
  4.      if (cam == null) {
  5.        Alert.show("Веб-камера не доступна");
  6.        return;   
  7.      }
  8.   cam.addEventListener(flash.events.StatusEvent.STATUS, onCameraStatus);
  9.   onCameraStatus (null);
  10. }
  11.  
  12. private function onCameraStatus (e: flash.events.StatusEvent):void{
  13.   if (! cam.muted){
  14.      videoPublisher.visible = true;
  15.      videoPublisher.attachCamera(cam); 
  16.   }
  17.   else
  18.     videoPublisher.visible = false;
  19. }
Для того, чтобы проверить работоспособность примера попробуйте после нажатия на кнопку “grab camera” вызвать по правой кнопке мыши контекстное меню на flash-ролике и выбрать пункт “Параметры”. Затем, попробуйте переключать “галочку” с пункта “разрешено” на “запрещено” и обратно. Вы увидите, что VideoDisplay, показывающий изображение с камеры, будет динамически прятаться и показываться в зависимости от вашего выбора.

Одной из самых полезных функций, которая только есть при работе с веб-камерой – это механизм обнаружения активности. Что это такое можно объяснить на простом примере. Предположим, что вы хотите использовать веб-камеру как часть системы контроля за безопасностью помещений, т.е. хотите записывать на жесткий диск как видео-файл все, что происходит в помещении. В этом случае имеет смысл не записывать изображение постоянно, а только тогда когда в кадре происходят какие-то изменения. Это позволит экономить как место на диске, так и (в случае передачи данных по интернет) экономить трафик. Вся эта “магия” работает благодаря тому, что после включения камеры, она постоянно отслеживает, так называемый, “activityLevel”. В любой момент времени вы можете узнать значение этого самого activityLevel, просто обратившись к одноименному свойству внутри объекта Camera. Так вот, когда в течении некоторого времени activityLevel был меньше чем установленное вами пороговое значение, то flashplayer известит вам об этом вызвав метод-обработчик события ActivityEvent.ACTIVITY. Аналогично, вас известят, вызвав этот же метод, когда камера зарегистрирует какое-то движение, превысившее пороговое значение activityLevel. Важно, что переход камеры в “неактивное” состояние вовсе не означает того, что она не будет записывать видео или не будет показывать его на присоединенном компоненте VideoDisplay. Просто вас будут извещать о наступлении таких событий, а как вы будете использовать эту информацию – только ваше дело:
  1. var cam:Camera = null;
  2. public function grap(event:MouseEvent):void {
  3.    cam = Camera.getCamera();
  4.    if (cam == null) {
  5.       Alert.show("Веб-камера не доступна");
  6.       return;
  7.    }
  8.    cam.setMotionLevel(5, 2000);
  9.    cam.addEventListener(ActivityEvent.ACTIVITY, activityHandler);
  10. }
  11.  
  12. private function activityHandler(e:ActivityEvent):void {
  13.   if (e.activating) {
  14.      labActivity.text = "Activated at "+ new Date ().toString() + “, activity level is”+ cam.activityLevel;
  15.   } 
  16.   else {
  17.      labActivity.text = "Deactivated at "+ new Date ().toString(); 
  18.   } 
  19. }
Пример построен вокруг вызова функции setMotionLevel, первым параметром которой является значение переменной activityLevel. ActivityLevel принимает значения в отрезке от 0 до 100 (со значением по умолчанию равным 50). Второй параметр функции setMotionLevel – это количество миллисекунд, которые будет отсчитывать flashplayer перед тем как известить наше приложение о том, что уровень активности упал ниже предельного.

Естественным шагом после того как мы научились определять доступность у конкретного клиента веб-камеры, определять свойства камеры и присоединять ее к VideoDisplay, будет отправка видео-потока с камеры на сервер для последующего его сохранения в виде flv-файла. Для простейшей реализации подобной функции не требуется никаких дополнительных настроек на стороне red5-сервера. Я решил воспользоваться все тем же старым примером red5-приложения на java, с которым мы работали в прошлых статьях, но очистил все содержимое класса HelloApplication. А также я вернул к своему оригинальному виду конфигурационный файл red5-web.xml, т.е. удалил объявление бина “streamFilenameGenerator” (см. прошлую статью для пояснения для чего был нужен этот самый “streamFilenameGenerator”). Также, я поменял внешний вид приложения, поместив в самом верху формы две кнопки: кнопку записи видео и кнопку просмотра, а внизу формы были размещены два элемента VideoDisplay и UIControl. Первый из них будет использоваться для просмотра захватываемого видео с камеры перед ее отправкой на сервер, а второй компонент будет служить для одновременного с этим просмотра видео, загружаемого с сервера. Т.е. после того как приложение будет запущено, с ним может работать любое количество клиентов, из которых один должен иметь веб-камеру и публиковать свой видео-поток, а все остальные клиенты будут способны просматривать публикуемую видеоинформацию. Поскольку, только один из видеоэкранов нужно показывать в зависимости от того, какой входящий или исходящий видео-поток он записывает, то я разместил компоненты videoPlayer и videoPublisher внутри специального mxml-компонента ViewStack. В будущем я смогу внутри actionscript-кода переключать видимый видеоэкран с videoPlayer и на videoPublisher и обратно (изменяя свойство selectedChild):
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" >
  3.   <mx:Script><![CDATA[
  4.   ]]></mx:Script>
  5.   <mx:VBox paddingTop="10" paddingLeft="10">
  6.     <mx:HBox>
  7.        <mx:Button click="grab(event)" label="grab camera"/>
  8.        <mx:Button click="play(event)" label="show video stream"/>
  9.     </mx:HBox>
  10.     <mx:ViewStack id="pages">
  11.        <mx:Panel id="panePublisher">
  12.           <mx:VideoDisplay id="videoPublisher" width="320" height="240" />
  13.        </mx:Panel>
  14.        <mx:Panel id="panePlayer">
  15.           <mx:UIComponent id="videoPlayer" width="320" height="240" />
  16.        </mx:Panel>
  17.     </mx:ViewStack>
  18.   </mx:VBox>
  19. </mx:Application>
Теперь мне нужно привести пример кода, размещенного внутри секции “mx:Script” и обрабатывающего нажатия на кнопки grab и play:
  1. import mx.controls.Alert;
  2.  
  3. private var ncPublish:NetConnection;
  4. private var nsPublish:NetStream;
  5. private var ncPlay:NetConnection;
  6. private var nsPlay:NetStream;
  7. private var cam:Camera = null;
  8.  
  9. private function grab(event:MouseEvent):void {
  10.   cam = Camera.getCamera();
  11.   if (cam == null) {
  12.     Alert.show("Веб-камера не доступна");
  13.     return;
  14.   }
  15.   if (cam.muted) {
  16.    Alert.show("Доступ к веб-камере запрещен");
  17.     return;
  18.   }
  19.   pages.selectedChild = panePublisher;
  20.   videoPublisher.attachCamera(cam);
  21.   ncPublish = new NetConnection();
  22.   ncPublish.objectEncoding = ObjectEncoding.AMF0;
  23.   ncPublish.addEventListener(NetStatusEvent.NET_STATUS, netStatusPublish);
  24.   ncPublish.connect('rtmp://localhost/warmodule/');
  25. }
  26.  
  27. private function netStatusPublish(event:NetStatusEvent):void {
  28.   if (event.info.code == 'NetConnection.Connect.Success') {
  29.     nsPublish = new NetStream(ncPublish);
  30.     nsPublish.attachCamera(cam);
  31.     nsPublish.publish("my-home-video", "append");
  32.     nsPublish.pause()
  33.   } 
  34. }
  35.  
  36. private function play(event:MouseEvent):void {
  37.   pages.selectedChild = panePlayer;
  38.   ncPlay = new NetConnection();
  39.   ncPlay.objectEncoding = ObjectEncoding.AMF0;
  40.   ncPlay.addEventListener(NetStatusEvent.NET_STATUS, netStatusPlay);
  41.   ncPlay.connect('rtmp://localhost/warmodule/');
  42. }
  43.  
  44. private function netStatusPlay(event:NetStatusEvent):void {
  45.   if (event.info.code == 'NetConnection.Connect.Success') {
  46.     nsPlay = new NetStream(ncPlay);
  47.     var v:Video = new Video();
  48.     v.width=320;
  49.     v.height=240;
  50.     v.attachNetStream(nsPlay);
  51.     videoPlayer.addChild(v);
  52.     nsPlay.play("my-home-video");
  53.   }
  54. }
Вначале кода я объявил пять переменных: cam – для ссылки на веб-камеру, а также по две пары переменных, служащих для связи с red5-сервером. Так пара ncPublish и nsPublish – представляют собой объект “подключение” к red5-серверу и объект “поток”, по которому информация будет публиковаться на сервер от клиента с веб-камерой. Вторая пара переменных ncPlay и nsPlay представляют собой “подключение” и “поток”, по которым видеоинформация будет загружаться с red5-сервера. Поскольку не допустима одновременная работа flash-приложения и в режиме публикации, и в режиме просмотра видео-потока, то можно было обойтись всего двумя переменными NetConnection и NetStream (также можно было бы обойтись всего одним общим компонентом VideoDisplay), но я решил их разделить для большей наглядности. Далее, рассмотрим то, что происходит, когда пользователь нажимает на кнопку “начать захват видео” и вызывается функция grab. Внутри этой функции я выполняю ряд проверок: на предмет физического наличия веб-камеры и наличия разрешения на работу с ней. Если какое-то из этих условий не выполняется, то я завершаю работу с соответствующим сообщением об ошибке. Если же все было хорошо, то я создаю и настраиваю объект NetConnection и привязываю к нему функцию netStatusPublish, которая вызывается, когда соединение будет установлено. Если и здесь не возникло проблем, то я создаю объект NetStream, непосредственно служащий для публикации видео-потока, привязываю к нему веб-камеру (attachCamera) и вызываю метод publish. В качестве параметров для которого выступают, во-первых, имя flv-файла, в который red5 будет сохранять поступающее видеоизображение, а вторым параметром указывается режим работы с файлом. Так возможны три кодовых значения: “record” – когда видео сохраняется в файл, но если файл уже существует, то он будет затерт. В режиме “append” ранее существовавший файл не будет утерян – а будет дополнен новым видеорядом. Режим “live” служит для публикации видео-потока без сохранения его в видео-файл. В любом случае, новый файл будет размещен в подкаталоге “streams” в корне каталога с вашим веб-приложением (см. рис. 3).



Если вы хотите, чтобы записываемый видео-файл размещался в каком-то другом месте, то нужно использовать точно такую же методику со специальным классом “подсказчиком”, что я демонстрировал в прошлой статье, когда показывал как можно хранить в произвольном каталоге те видеофильмы, что клиент может загружать для просмотра. Тогда мы создали свой класс, реализующий интерфейс IStreamFilenameGenerator, и зарегистрировали его в файле red5-web.xml.

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



Где показано как приложение, запущенное в браузере opera, играет роль источника видео-потока и передает его на сервер, где видео сохраняется в виде статического flv-файла. А также этот же видео-поток идет на вход остальным двум flash-клиентам (в браузерах chrome и internet explorer).

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

Categories: Flash & Flex