Видео-камеры, видео-чаты и Flash-медиасервера (работающие по RTMP и самописным протоколам).
У меня на сайте довольно много заметок, посвященных обработке видео на бейсике и ASP.NET. Например Опыт видео-конвертации, в которой я описал как устроен мой видеоконвертер на портале http://video.votpusk.ru/. У меня есть несколько заметок, посвященным некоторым моим более мелким проектам, например Мультимедиа на Web-страничках. У меня на сайте есть также несколько заметок, посвященных работе с видеокамерой, например Этюды на ASP NET2. Наблюдаем за своим домом с работы, в которой я описал мой прокси-сервер для работы с IP-камерой. А на этой страничке я хотел бы показать как можно работать с видео на FLEX.
На мой взгляд, работа с видео на Flex делится на несколько тематических блоков. Первый блок - это побайтовая работа с видео-потоком, ровно так же, как я работал с видео в моем вышеуказанном прокси-сервере. Этот метод позволяет написать свой видеочат, в котором один клиент AIR-приложения (или браузера) общается с другим МИНУЯ СЕРВЕР. Это некий элементарный клон моего любимого скайпа - личка с видео - (Face-to-Face, Peer-to-Peer Communication). Только конечно скайп еще имеет много дополнительных накруток, таких как весьма защищенный код (а не просто элементарно декомпилируемый код ActionScript), навороченный защищенный протокол обмена, регистрация своего логина на сервере и генерация ключей для протокола после авторизации на сервере, промежуточные прокси-сервера ретрансляции трафика и прочие фишки. В моем мини-клоне скайпа этого ничего нет. Эту прогу я написал и отладил за пару выходных дней - а скайп писался и доводился до совершенства годами. Но, тем не менее - когда крутая защита трафика не требуется - моя простейшая прога видео-чата вполне имеет право на существование.
Мой простейший видеочат основан на простейшем сокетном протоколе, который я намеренно создал предельно простым и понятным для широкой слегкапрограммирующей публики. Этот протокол (и класс, реализующий сокетный обмен по этому протоколу) я описал в топике - Сокеты во Flash.
Технология байтовой обработки видеопотока, примененная в моем видеочате проста до невозможности. Определяется видеокамера и видео с нее воспроизводится в видео-плеере. Это необходимо для того, чтобы получить медиапоток. Сама по себе виртуальная машина Flash не предоставляет доступа собственно к байтовому потоку с видеокамеры. Видеопоток обрабатывается драйвером и выводится сразу в плеер. А вот уже к плееру существует интерфейс - считать из него рисунок. Далее этот рисунок можно сериализовать в обычный JPEG и например сохранить на диск. Или скормить моему классу, который передаст рисунок в сеть. Соответственно, приняв рисунок - его можно отобразить. Вот и весь механизм работы моего видео-чата - квантование видеопотока на JPEG и передача рисунков через сеть. В этом простом чате нет ни опорных точек, ни дельты от опорных точек - нет ничего из того, что придумано в протоколах RTMP / RTMFP / H.263 / H.264 и медиафайлах FLV / F4V.
Чтобы реализовать этот алгоритм - надо сначала создать на форме необходмые элементы. Для простого теста например вот так:
1: <s:TileGroup horizontalGap="12" verticalGap="12" left="10" right="10" top="10" bottom="10">
2:
3: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300">
4: <mx:FormItem >
5: <mx:VideoDisplay id="videoDisplay1"
6: creationComplete="videoDisplay_creationComplete();"
7: width="200"
8: height="200" horizontalCenter="0" verticalCenter="0" dropShadowVisible="false"/>
9: </mx:FormItem>
10: <mx:FormItem >
11: <mx:Button id="RefreshCamera"
12: label="Refresh local Camera"
13: click="videoDisplay_creationComplete();" />
14: </mx:FormItem>
15: <mx:FormItem>
16: <s:HSlider id="VideoQuality" width="143" stepSize="1" minimum="0" maximum="100" value="10" change="VideoQuality_changeHandler(event)"/>
17: <s:Label text="Quality"/>
18: </mx:FormItem>
19: </mx:Form>
20: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300" height="132">
21: <mx:FormHeading label="Remote camera:"/>
22: <mx:FormItem >
23: <mx:Image id="Image1" width="178" height="160"/>
24: </mx:FormItem>
25: </mx:Form>
26: <mx:Form width="300" dropShadowVisible="true" borderStyle="solid" borderVisible="true">
27: <mx:FormHeading label="SERVER"/>
28: <mx:FormItem label="Local IP" id="LocalIP">
29: <s:Label text="127.0.0.1"/>
30: </mx:FormItem>
31: <mx:FormItem label="Local Audio Port">
32: <s:TextInput text="101" id="LocalAudioPort"/>
33: </mx:FormItem>
34: <mx:FormItem label="Local Video Port">
35: <s:TextInput text="102" id="LocalVideoPort"/>
36: </mx:FormItem>
37: <mx:FormItem label="Local Text Port">
38: <s:TextInput text="103" id="LocalTextPort"/>
39: </mx:FormItem>
40: <mx:FormItem>
41: <s:Button label="Start" id="StartLocalNetwork" click="StartLocalNetwork_clickHandler(event)"/>
42: </mx:FormItem>
43: <mx:FormItem>
44: <s:Button label="Stop" id="StopLocalNetwork" click="StopLocalNetwork_clickHandler(event)"/>
45: </mx:FormItem>
46: </mx:Form>
47: <mx:Form width="300" dropShadowVisible="true" borderStyle="solid" borderVisible="true">
48: <mx:FormHeading label="SENDER:"/>
49: <mx:FormItem label="Remote IP">
50: <s:TextInput text="127.0.0.1" id="RemoteIP"/>
51: </mx:FormItem>
52: <mx:FormItem label="Remote Audio Port">
53: <s:TextInput text="101" id="RemoteAudioPort"/>
54: </mx:FormItem>
55: <mx:FormItem label="Remote Video Port">
56: <s:TextInput text="102" id="RemoteVideoPort"/>
57: </mx:FormItem>
58: <mx:FormItem label="Remote Text Port">
59: <s:TextInput text="103" id="RemoteTextPort"/>
60: </mx:FormItem>
61: <mx:FormItem>
62: <s:Button label="Start" id="StartRemoteNetwork" click="StartRemoteNetwork_clickHandler(event)"/>
63: </mx:FormItem>
64: <mx:FormItem>
65: <s:Button label="Stop" id="StopRemoteNetwork" click="StopRemoteNetwork_clickHandler(event)"/>
66: </mx:FormItem>
67: </mx:Form>
68: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300" height="132">
69: <mx:FormHeading label="System message:"/>
70: <mx:FormItem >
71: <s:RichText id="ErrorMessage" text="" />
72: </mx:FormItem>
73: </mx:Form>
74:
75: </s:TileGroup>
Как видите, на этой форме просто размещены параметры (айпишник и порты) партнера по видеочату, плеер, рисунок в котором будет воспроизводиться видеопоток и место, где можно увидеть диагностические сообщения. Конечно - это лишь зародыш реального приложения коммерческого качества. Даже журнал сообщений таким образом выводить нелепо - гораздо разумнее было бы его хотя бы уложить в базу - Открой для себя SQLite. Ну и айпишники и порты, конечно же должны быть или в настройках, или определяться каким-то более разумым образом.
Теперь осталось довесить на все это собственно алгоритм управления потоком по сети. Мой протокол (описанный в Сокеты во Flash) настолько прост, что у него нет управления потоком и его надо реализовывать в вызывающих классах. Альтернатива этому простейшему решению - старт/стоп передачи уложить прямо в классе сокетного обмена или сделать еще один враппер, в котором сервер бы говорил сендеру - притормози пока. Но в простейшем видеочате ничего этого нет. Из-за чего и его работа недолговечна. Время от времени сендер успевает передать данных больше, чем успевает вычитать сервер и поэтому у сервера получается ерунда вместо нормального JPG. Иногда закрывается порт и видео-чат обрывается. Надо нажимать кнопку рефреш и инициализировать передачу заново. Собственно говоря нижеследующий код вполне работоспособен, и на моих тестах связь нормально держится несколько секунд или десятки секунд, но для коммерческого продукта это явно мало и требуется еще немало поработать, чтобы добавить управление потоком данных, обработать все различные причины обрыва связи и сделать правильные обработчики каждой ошибки.
Итак, простейший алгоритм выглядит вот так:
1: <fx:Script>
2: <![CDATA[
3: import flash.events.*;
4: import mx.controls.Alert;
5: import mx.events.FlexEvent;
6: import mx.graphics.codec.JPEGEncoder;
7: import ru.net.asp.socket.*;
8:
9: protected function windowedapplication1_contentCreationCompleteHandler(event:FlexEvent):void
10: {
11: var Sender1:Network_Sender=new Network_Sender("127.0.0.1",101);
12: Loader1=new Loader();
13: }
14:
15: private function videoDisplay_creationComplete():void {
16: var camera:Camera = Camera.getCamera();
17: if (camera) {
18: videoDisplay1.attachCamera(camera);
19: videoDisplay1.autoPlay;
20: }
21: else {
22: ErrorMessage.text += "You dont have a camera.\n";
23: }
24: }
25:
26: private function ReadImageByteFromCamera():ByteArray {
27: //вычитываем рисунок из видеоплеера, в котором показывает локальная камера
28: var BitmapData1:BitmapData=new BitmapData(videoDisplay1.width, videoDisplay1.height);
29: BitmapData1.draw(videoDisplay1);
30: var Encoder1:JPEGEncoder=new JPEGEncoder(VideoQuality.value);
31: var Buf1:ByteArray=Encoder1.encode(BitmapData1);
32: return Buf1;
33: }
34:
35: //***********************************************************************
36:
37: private var ServerAudioListener:Network_Listener;
38: private var ServerVideoListener:Network_Listener;
39: private var ServerTextListener:Network_Listener;
40: private var Loader1:Loader;
41:
42: protected function StartLocalNetwork_clickHandler(event:MouseEvent):void
43: {
44: try {
45: //ServerAudioListener = NetworkServerPrepare(ServerAudioListener,LocalAudioPort.text);
46: ServerVideoListener = NetworkServerPrepare(ServerVideoListener,LocalVideoPort.text);
47: //ServerTextListener = NetworkServerPrepare(ServerTextListener, LocalTextPort.text);
48: //ServerAudioListener.addEventListener(Network_Listener.DATA_RECEIVED, Server_Audio_DATA_RECEIVED_handler);
49: ServerVideoListener.addEventListener(Network_Listener.DATA_RECEIVED, Server_Video_DATA_RECEIVED_handler);
50: //ServerTextListener.addEventListener(Network_Listener.DATA_RECEIVED, Server_Text_DATA_RECEIVED_handler);
51: }
52: catch (error:Error)
53: {
54: ErrorMessage.text += "Server Error: " + error.message + "\n";
55: }
56: }
57:
58: private function NetworkServerPrepare(X:Network_Listener, LocalPort:String):Network_Listener{
59: X = new Network_Listener(LocalPort);
60: X.addEventListener(Network_Listener.DATA_ERROR, ServerInfo_handler);
61: X.addEventListener(Network_Listener.CONNECT, ServerInfo_handler);
62: X.addEventListener(Network_Listener.OPEN_ERROR, Server_OPEN_ERROR_handler);
63: X.addEventListener(Network_Listener.CLOSE_ERROR, ServerInfo_handler);
64: X.addEventListener(Network_Listener.PORT_OPEN, ServerInfo_handler);
65: X.addEventListener(Network_Listener.PORT_CLOSE, ServerInfo_handler);
66: X.addEventListener(Network_Listener.PROTOCOL_ERROR, ServerInfo_handler);
67: X.addEventListener(Network_Listener.STRING_ERROR, ServerInfo_handler);
68: X.Network_Open();
69: return X;
70: }
71:
72: protected function StopLocalNetwork_clickHandler(event:MouseEvent):void
73: {
74: try {
75: ServerAudioListener.Network_Close();
76: ServerVideoListener.Network_Close();
77: ServerTextListener.Network_Close();
78: }
79: catch (error:Error)
80: {
81: ErrorMessage.text += "Local error: " + error.message + "\n";
82: }
83: }
84:
85: protected function ServerInfo_handler(e:Event):void{
86: ErrorMessage.text += "Server: " + e.type + " on local port " + e.currentTarget.PortNumber +"\n";
87: }
88: protected function Server_OPEN_ERROR_handler(e:Event):void{
89: ErrorMessage.text += "Server local port " + e.currentTarget.PortNumber + " in use. Select another port. \n";
90: }
91:
92: protected function Server_Video_DATA_RECEIVED_handler(e:Event):void{
93: var Arr1:ByteArray=e.currentTarget.ReceivedBytes;
94: ErrorMessage.text += "Server: " + e.type + " on local port " + e.currentTarget.PortNumber +" (" + Arr1.length + ")\n";
95: //из сети поступил рисунок - показываем его
96: ShowByteArray(Arr1);
97: }
98:
99: //***********************************************************************
100:
101: private var SenderAudioSender:Network_Sender;
102: private var SenderVideoSender:Network_Sender;
103: private var SenderTextSender:Network_Sender;
104:
105: protected function StartRemoteNetwork_clickHandler(event:MouseEvent):void
106: {
107: try {
108:
109: SenderVideoSender = new Network_Sender(RemoteIP.text, RemoteVideoPort.text);
110: SenderVideoSender.addEventListener(Network_Sender.DATA_SENDED, Video_DATA_SENDED_handler);
111: SenderVideoSender.addEventListener(Network_Sender.CONNECT, Video_CONNECT_handler);
112: SenderVideoSender.addEventListener(Network_Sender.PORT_CLOSE, Video_PORT_CLOSE_handler);
113: SenderVideoSender.addEventListener(Network_Sender.DATA_ERROR, SenderInfo_handler);
114: SenderVideoSender.addEventListener(Network_Sender.OPEN_ERROR, SenderInfo_handler);
115: SenderVideoSender.addEventListener(Network_Sender.CLOSE_ERROR, SenderInfo_handler);
116: SenderVideoSender.addEventListener(Network_Sender.PORT_OPEN, SenderInfo_handler);
117: SenderVideoSender.addEventListener(Network_Sender.PROTOCOL_ERROR, Video_PROTOCOL_ERROR_handler);
118: SenderVideoSender.addEventListener(Network_Sender.PROTOCOL_OK, Video_PROTOCOL_OK_handler);
119: SenderVideoSender.addEventListener(Network_Sender.SECURITY_ERROR, SenderInfo_handler);
120: SenderVideoSender..Network_Open();
121: }
122: catch (error:Error)
123: {
124: ErrorMessage.text += "Sender Error: " + error.message + "\n";
125: }
126: }
127:
128:
129: protected function StopRemoteNetwork_clickHandler(event:MouseEvent):void
130: {
131: try {
132: SenderAudioSender.Network_Close();
133: SenderVideoSender.Network_Close();
134: SenderTextSender.Network_Close();
135: }
136: catch (error:Error)
137: {
138: ErrorMessage.text += "Sender Error: " + error.message + "\n";
139: }
140: }
141:
142: protected function SenderInfo_handler(e:Event):void{
143: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber +"\n";
144: }
145:
146: private function ShowByteArray(Arr1:ByteArray):void{
147: try{
148: Arr1.position=0;
149: Loader1.loadBytes(Arr1);
150: Loader1.contentLoaderInfo.addEventListener(Event.COMPLETE, loaderCompleteHandler);
151: }
152: catch (error:Error)
153: {
154: ErrorMessage.text += "Video_DATA_RECEIVED Error: " + error.message + "\n";
155: }
156: }
157:
158: private function loaderCompleteHandler(evt:Event):void {
159: var LoaderInfo1:LoaderInfo = evt.currentTarget as LoaderInfo;
160: Image1.source = LoaderInfo1.content;
161: }
162:
163: private var VideoFrameNumber:int;
164: private function SendDataToNet(Buf1:ByteArray):void {
165: if (SenderVideoSender !== null){
166: //отправляем рисунок в сеть
167: VideoFrameNumber+=1;
168: SenderVideoSender.PacketNumber=VideoFrameNumber;
169: SenderVideoSender.SendBytes=Buf1;
170: SenderVideoSender.SendBytes.position=0;
171: SenderVideoSender.Send();
172: }
173: }
174:
175: var SenderVideoPortState:String;
176: private function Video_CONNECT_handler(e:Event):void{
177: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber +"\n";
178: SenderVideoPortState=e.type;
179: SendDataToNet(ReadImageByteFromCamera());
180: }
181:
182:
183: private function Video_PORT_CLOSE_handler(e:Event):void{
184: SenderVideoPortState=e.type;
185: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber +"\n";
186: //?
187: if (!SenderVideoSender.Sender_Socket.connected){
188: SenderVideoSender.Network_Open();
189: }
190: }
191:
192: private function Video_PROTOCOL_OK_handler(e:Event):void{
193: SenderVideoPortState=e.type;
194: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber +"\n";
195: SendDataToNet(ReadImageByteFromCamera());
196: }
197:
198: private function Video_PROTOCOL_ERROR_handler(e:Event):void{
199: SenderVideoPortState=e.type;
200: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber +"\n";
201:
202: }
203:
204: private function Video_DATA_SENDED_handler(e:Event):void{
205: SenderVideoPortState=e.type;
206: ErrorMessage.text += "Sender: " + e.type + " on " + e.currentTarget.IpAddress + ":" + e.currentTarget.PortNumber + " ("+ e.currentTarget.SendBytes.length + ") \n";
207: }
208:
209:
210: ]]>
211: </fx:Script>
Теперь рассмотрим противоположную технологию обработки видео. Когда медиа-поток передается на сервер, там обрабатывается серверным приложением и оттуда раздается клиентам. Такого софта написано немерянно, например видео-чатов. И этот софт писался не на скорую руку - как мой код выше, а годами и большими коллективами. Например OpenSource видеочат - http://bigbluebutton.org написан с использованием 40 известнейших OpenSource - http://www.red5.org/, http://www.mysql.com/, http://www.asterisk.org/, http://tomcat.apache.org/, http://www.openoffice.org/, http://nginx.net/, http://www.swftools.org/ и т.д. - это уже готовое приложение в среде медиа-сервера RED5.
Кроме того существует большое количество собственно серверов для приема медиапотоков по протоколу RTMP (адобовская версия H263) и раздачи их по многим клиентам. Два самых известных сервера: OpenSource RED5 и платный с закрытым кодом от компании ADOBE - FMS. И есть еще десяток менее популярных серверов такого же назначения - http://www.wowzamedia.com/, http://flazr.com/, http://mammothserver.org/, http://www.sothinkmedia.com/, http://www.rtmpd.com/, http://onlinelib.de - эти сервера поддерживают обмен по протоколу RTMP с флеш-плеерами и являются серверной средой для написания собственных приложений, работающих в среде этих серверов (типа вышеупомянутого видео-чата BigBlueButton).
Два ведущих медиа-серверов RED5 и FMS отличаются тем, что OpenSource проект RED5 управляется приложениями на языке JAVA, а FMS управляется ActionScript. Кроме того Adobe составила отличную документацию на работу с FMS, есть и отличная книга для начинающих, есть отличные сайты с обучающими видео по FMS . У остальных медиа-серверов тоже есть всевозможные достоинства, например какие-то фирменные платные сервера существенно дешевле адобовского сервера. Для каких-то медиа-серверов декларируется более высокая производительность. Для достижения еще большей производительности, чем у RED5 или FMS - медиа-сервера даже пишут на С, чтобы они работали не на виртуальных машинах Flash или JAVA, а прямо в натуральном машинном коде. Что приятно, что практически все сервера работают не только под билогейтсовской виндузней, но в современнной OpenSource-среде LINUX.
Обратите внимание, что в принципе наличие одного из упомянутых медиа-серверов для простой трансляции видео необходимым не является вовсе, с раздачей видео в интернет вполне справляется Apache или IIS (как я это описал в топике Опыт видео-конвертации). И даже онлайновую трансляцию видеопотока с видеокамеры можно делать своим простейшим кодом на бейсике в IIS, как например я это сделал в моем прокси-сервере Этюды на ASP NET2. Наблюдаем за своим домом с работы, но конечно полноценный собственный сервер такого качества как RED5 или FMS можно писать годами. Писать-писать-писать, но так и не добиться такого же сервиса и производительности, какие удалось достичь в RED5 и FMS.
Наиболее дорогая версия FMS INTERACT стоит $5000 и позволяет на канале толщиной 25 Mbps Bandwidth обслуживать 2500 одновременных раздач видео. Редакция за тысячу долларов позволяет раздать на канале 40 Mbps видео на 1500 клиентов. Такие цены на массовый продукт, написанный один раз и просто тиражируемый в миллионах экземплярах - это конечно полное безобразие, но - во, первых есть RED5 (и другие OpenSource сервера), а во-вторых по прежнему на свободе остается Билл Гейтс. Хоть он и работает под постоянным надзором судебных исполнителей, хоть и платит штрафы по миллиарду долларов за мошенничество, и даже WORD (который в принудительном порядке изучают в школе) он украл - но, он продает MS SQL Server за 54 тысячи долларов на процессор (!), серверную виндузню на 3 тысячи долларов и свою чудо-студию VS2010 за 12 тысят долларов. На каких проектах могут окупиться такие вложения? Тем не менее, все это билогейтсовское мракобесие активно рекламируется, некоторые люди (и даже я в силу ряда причин) часто программирую для этой нелепой среды. На фоне такого крупного мошенника как Билл Гейтс, компания Adobe с ее медиа-сервером за 1-5 тысячи долларов - это просто бессеребрянники какие-то. Тем более они все это разработали сами и купили у Macromedia, а не просто украли - как Билл Гейтс.
В бесплатной девелоперской версии FMS разрешены 10 раздач. Она в полтычка устанавливается на любую билогейтсовскую виндузню или на современную OpenSource операционную систему Linux:
Как видите (на вкладке управления сервером Manage Servers->Server 1->_defaultVHost_->Application) в FMS4 уже предустановлено четыре серверных приложения - live, livepkgr, multicast, vod. Они находятся в папке ...\Flash Media Server 4\samples\applications\... Безусловно это лишь примерные тестовые приложения - для любого более ли менее приличного сайта подобное серверное приложение придется писать самому - так же как пишут свои собственные сайты на сервере. Но чтобы поработать в админке FMS, отладить клиентскую часть, и посмотреть образцы сервеного кода ActionScript - эти тестовые приложения вполне подойдут.
Начнем с приложения vod. Оно практически ничего не делает - просто отдает по потоколу RTMP файл в сеть. Ровно это же может сделать и IIS и Apache - но только по HTTP. Здесь надо сделать остановку и вспомнить, какие вообще существуют медиа-потоки в идеологии Adobe:
- Camera Live Video - живое видео с видеокамеры (с этого примера я начал эту страничку когда описал свое небольшое peer-to-peer приложение).
- OnDemand HTTP Dynamic Streaming - видео с прогрессивной загрузкой через HTTP.
- Live HTTP Dynamic Streaming - живое видео по HTTP.
- RTMP Stream from the Flash Media Server - потоковое видео c серверов типа FMS или RED5 по протоколу RTMP.
- RTMFP peer-to-peer media stream - видеоконференции в группах (в том числе точка-точка) минуя FMS.
- On-demand and live adaptive bitrate video streaming of standards-based MP4 media over regular HTTP connections - MP4-видеопотоки с прогрессивной и потоковой загрузкой через HTTP.
Так вот, приложение vod - как раз формирует (потоковое видео) RTMP Stream. Для того, чтобы FMS выбросил этот медиа-поток в сеть - надо всего-навсего положить видео в нужную папку и обратиться с клиента (Flash-плеера) к приложению vod в сервере FMS. Это примерно то же самое, что сформировать вызов некоего сайта в Apache и IIS. Кстати обратите внимание, что помимо собственно серверного кода ActionScript в папке приложения присутствует файл Application.xml (аналог web.config в ASP.NET).
Чтобы приложение vod начало транслировать в сеть потоковое видео, надо правильно положить файлы FLV. Я видел в интернете вариант, когда файлы ложат в папку {FMS-Install-Dir}/applications/vod/Media, но я не думаю, что это правильно. Я протрассировал куда приложение реально обращается и понял, что более правильное местоположение медиафайлов по умолчанию {FMS-Install-Dir}\webroot\vod:
Теперь посмотрим как можно проверить работу FMS. Для этого воспользуемся FLEX-контролом VideoPlayer. Как обратится к медиа-потоку через HTTP - я уже описывал на страничке Реклама в видеоплеере (возможности объектного программирования ActionScript), а к потоковому видео FMS можно обратиться вот так:
1: <?xml version="1.0" encoding="utf-8"?>
2: <s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
3: xmlns:s="library://ns.adobe.com/flex/spark"
4: xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" >
5: <fx:Script>
6: <![CDATA[
7: import mx.events.FlexEvent;
8: import org.osmf.events.MediaPlayerStateChangeEvent;
9: import org.osmf.events.TimeEvent;
10:
11: protected function vpCompleteHandler(event:TimeEvent):void {
12: TextArea1.text = "Video complete - restarting."
13: }
14:
15: protected function vpMediaPlayerStateChangeHandler(event:MediaPlayerStateChangeEvent):void {
16: if (event.state == "loading")
17: TextArea1.text = "loading ...";
18: if (event.state == "playing")
19: TextArea1.text = "playing ...";
20: }
21: ]]>
22: </fx:Script>
23:
24: <fx:Declarations>
25: <!-- Place non-visual elements (e.g., services, value objects) here -->
26: </fx:Declarations>
27: <s:VideoPlayer id="VideoPlayer1" x="0" y="0" width="100%" height="100%"
28: source="rtmp://localhost:8008/vod/ladder"
29: complete="vpCompleteHandler(event);"
30: mediaPlayerStateChange="vpMediaPlayerStateChangeHandler(event);">
31:
32: </s:VideoPlayer>
33: <s:TextArea id="TextArea1" width="350" height="25"/>
34: </s:Application>
Обратите внимание, как отдача потокового видео отображается в консоли FMS:
Но этот код - наиболее простой вариант - фактически просто тест "Hello world" - чтобы просто обратиться к FMS. Он может продемонстриривать, что FMS правильно настроен и сконфигурирован, в нем правильно установлено тестовое приложение и у тестового приложения правильный конфиг, медиа-файл лежит в нужной папке и так далее. Никакой полезной работы такое простое приложение не делает (как и сайт с одной кнопкой и выводящий в браузер строчку "Hello world"). Вся работа с медиапотоком в этом тестовом приложении делается автоматически с помощью кода самого этого контрола - VideoPlayer. Как только потребуется хоть что-то реально полезное - от этого демонтстрационного варианта придется уходить - самому работать с потоками, натягивать скины с цветовой окраской сайта и прочее, прочее, прочее
Поэтому следующий простейший шаг - переход к более сложному контролу - в котором уже нет встроенных кнопок, втроенного ползунка, на который можно натянуть скины, и в котором можно самому работать с медиа-потоком собственным кодом (а не встроенным в контрол). Шаблон кода более реального клиентского выглядит вот так:
1: <?xml version="1.0" encoding="utf-8"?>
2: <s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
3: xmlns:s="library://ns.adobe.com/flex/spark"
4: xmlns:mx="library://ns.adobe.com/flex/mx"
5: contentCreationComplete="init();">
6: <fx:Script>
7: <![CDATA[
8: import mx.utils.ObjectUtil;
9: private var nsClient:Object;
10: private var nc:NetConnection;
11: private var ns:NetStream;
12: private var video:Video;
13: private var meta:Object;
14:
15: private function init():void {
16:
17: nsClient = {};
18: nsClient.onMetaData = ns_onMetaData;
19: nsClient.onCuePoint = ns_onCuePoint;
20: nsClient.onBWDone = ns_onBWDone;
21:
22:
23:
24: nc = new NetConnection();
25: nc.addEventListener (NetStatusEvent.NET_STATUS,checkConnect);
26: //nc.connect(null);
27: nc.connect("rtmp://localhost:8008/vod/");
28: nc.client=nsClient;
29:
30: }
31:
32: private function checkConnect (event:NetStatusEvent):void
33: {
34: if (event.info.code=="NetConnection.Connect.Success"){
35:
36: ns = new NetStream(nc);
37: //ns.play("//www.vb-net.com/flex6/mer.flv");
38: ns.play("ladder");
39: ns.client = nsClient;
40:
41: video = new Video();
42: video.attachNetStream(ns);
43: uic.addChild(video);
44: }
45: }
46:
47: private function ns_onMetaData(item:Object):void {
48: trace("meta");
49: meta = item;
50: // Resize Video object to same size as meta data.
51: video.width = item.width;
52: video.height = item.height;
53: // Resize UIComponent to same size as Video object.
54: uic.width = video.width;
55: uic.height = video.height;
56: panel.title = "framerate: " + item.framerate;
57: panel.visible = true;
58: trace(ObjectUtil.toString(item));
59: }
60:
61: private function ns_onCuePoint(item:Object):void {
62: trace("cue");
63: }
64: private function ns_onBWDone():void {
65: trace("BWDone");
66: }
67:
68: ]]>
69: </fx:Script>
70:
71: <mx:Panel id="panel" visible="false">
72: <mx:UIComponent id="uic" />
73: <mx:ControlBar>
74: <mx:Button label="Play/Pause" click="ns.togglePause();" />
75: <mx:Button label="Rewind" click="ns.seek(0); ns.pause();" />
76: </mx:ControlBar>
77: </mx:Panel>
78: </s:WindowedApplication>
Обратите внимание, что ровно этот же шаблон кода используется не только для потокового rtmp-видео, но и для прогрессивного видео через HTTP. Для этого надо изменить всего две строчки:
- закомментировать 26-ю строку и раскомментировать 27-ю строку
- закомментировать 38-ю строку и раскомментировать 37-ю строку
Также обратите внимание, что в потоковом видео имя файла указывается без расширения (для FLV), а для других типов файлов указывается довольно хитро - не просто с расширением, но и с именем кодека - например "mp4:myvideo.f4v", "mp4:myvideo.mp4", "mp3:mymp3stream". В прогрессивном видео расширение указывается всегда.
Теперь, когда более ли менее понятно как сделать простейший тестовый клиент для проверки FMS и сам FMS работает - можно пойти дальше и сделать тест для живого видео (по HTTP или RMTP). Но для начала надо разобраться как создать Live mediastream с видео-камеры. Это можно сделать своим собственным кодом, либо воспользоваться Адобовской утилитой Flash Media Live Encoder 3.2, которая умеет коннектится к FMS и передавать ему LiveVideo.
Эта утилита работает попросту (без аутентификации и юзеров) или с особым плагином (Flash Media Server Authentication Add-in) - тогда просто так к приложению в FMS не приконнектишься, сначала в приложении http://mydomain/live надо создать юзеров. И приконнектиться к своим потоком к приложению можно только введя логин и пароль.
Для того чтобы принять живое видео - надо всего лишь правильно указать имя потока и имя приложения в строке 28 примера с VideoPlayer'ом - в моей конфигурации это - source="rtmp://localhost:8008/live/livestream".
Опубликовать видеопоток через FMS можно (по идее) и своим кодом. Шаблон кода вы видите ниже. Но в моей среде этот код почему-то работает криво. Хотя я вижу в FMS, что потоки Live создаются, никаких ошибок нигде нет, но плеер отображает лишь несколько кадров в начале потока и в конце. Очевидно, знаний моих пока не хватает - ибо я хотя и программирую уже более 30 лет, в том числе на ASP.NET около 10 лет, но в детали Адобовских медиа технологий я вникаю пока только первый год. Где-то видимо что-то я не вижу.
1: <?xml version="1.0" encoding="utf-8"?>
2: <s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
3: xmlns:s="library://ns.adobe.com/flex/spark"
4: xmlns:mx="library://ns.adobe.com/flex/mx" applicationComplete="windowedapplication1_applicationCompleteHandler(event)" width="644" height="666">
5: <fx:Script>
6: <![CDATA[
7: import mx.events.FlexEvent;
8:
9: import org.osmf.events.MediaPlayerStateChangeEvent;
10: import org.osmf.events.TimeEvent;
11:
12: import spark.components.mediaClasses.DynamicStreamingVideoSource;
13:
14: private var nc:NetConnection;
15: private var ns:NetStream;
16:
17: var camera:Camera;
18: protected function windowedapplication1_applicationCompleteHandler(event:FlexEvent):void
19: {
20: // TODO Auto-generated method stub
21: }
22:
23: private function videoDisplay_creationComplete():void {
24: camera = Camera.getCamera();
25: if (camera) {
26: videoDisplay1.attachCamera(camera);
27: videoDisplay1.autoPlay;
28: SetNetworkStream();
29: }
30: else {
31: ErrorMessage.text += "You dont have a camera.\n";
32: }
33: }
34:
35: import mx.collections.ArrayCollection;
36: [Bindable]private var microphoneList:ArrayCollection;
37: protected var microphone:Microphone;
38:
39: protected function cbMicChoices_creationCompleteHandler(event:FlexEvent):void
40: {
41: microphoneList = new ArrayCollection(Microphone.names);
42: cbMicChoices.selectedIndex=0;
43: }
44:
45: private var nsClient:Object;
46: private var video:Video;
47: private var meta:Object;
48: protected function SetNetworkStream():void{
49: nsClient = {};
50: nsClient.onMetaData = ns_onMetaData;
51: nsClient.onCuePoint = ns_onCuePoint;
52: nsClient.onBWDone = ns_onBWDone;
53:
54:
55: nc = new NetConnection();
56: nc.client=nsClient;
57: nc.addEventListener (NetStatusEvent.NET_STATUS,checkConnect);
58: nc.connect("rtmp://localhost/live");
59: }
60:
61: private function checkConnect (event:NetStatusEvent):void
62: {
63: if (event.info.code=="NetConnection.Connect.Success"){
64: ns = new NetStream(nc);
65: ns.attachCamera(camera,VideoQuality.value);
66: ns.attachAudio(microphone);
67: ns.publish("livestream","live");
68: }
69: }
70:
71:
72: protected function vpCompleteHandler(event:TimeEvent):void {
73: TextArea1.text = "Video complete - restarting."
74: }
75:
76: protected function vpMediaPlayerStateChangeHandler(event:MediaPlayerStateChangeEvent):void {
77: if (event.state == "loading")
78: TextArea1.text = "loading ...";
79: if (event.state == "playing")
80: TextArea1.text = "playing ...";
81: }
82:
83: private function ns_onMetaData(item:Object):void {
84: trace("meta");
85: }
86:
87: private function ns_onCuePoint(item:Object):void {
88: trace("cue");
89: }
90: private function ns_onBWDone():void {
91: trace("BWDone");
92: }
93:
94: ]]>
95: </fx:Script>
96: <fx:Declarations>
97: <!-- Place non-visual elements (e.g., services, value objects) here -->
98: </fx:Declarations>
99: <s:TileGroup horizontalGap="12" verticalGap="12" left="10" right="10" top="10" bottom="10">
100: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300">
101: <mx:FormItem >
102: <mx:VideoDisplay id="videoDisplay1"
103: creationComplete="videoDisplay_creationComplete();"
104: width="200"
105: height="200" horizontalCenter="0" verticalCenter="0" dropShadowVisible="false"/>
106: </mx:FormItem>
107: <mx:FormItem >
108: <mx:Button id="RefreshLocalCamera"
109: label="Refresh local Camera"
110: click="videoDisplay_creationComplete();" />
111: </mx:FormItem>
112: <mx:FormItem>
113: <s:HSlider id="VideoQuality" width="143" stepSize="1" minimum="0" maximum="100" value="50" />
114: <s:Label text="Quality"/>
115: </mx:FormItem>
116: </mx:Form>
117: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300" height="132">
118: <mx:FormHeading label="Remote camera:"/>
119: <mx:FormItem >
120: <s:VideoPlayer id="VideoPlayer1" width="200" height="200" autoPlay="true"
121: complete="vpCompleteHandler(event);"
122: mediaPlayerStateChange="vpMediaPlayerStateChangeHandler(event); ">
123: <s:source>
124: <s:DynamicStreamingVideoSource id="Live1"
125: host="rtmp://192.168.0.21:8008/live">
126: <s:DynamicStreamingVideoItem id="livestream1"
127: streamName="livestream"
128: bitrate="150" />
129: </s:DynamicStreamingVideoSource>
130: </s:source>
131: </s:VideoPlayer>
132: <s:TextArea id="TextArea1" width="200" height="25"/>
133: </mx:FormItem>
134: </mx:Form>
135: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300" height="132">
136: <mx:FormHeading label="Local microphone:"/>
137: <mx:FormItem label="Volume">
138: <s:HSlider width="143"/>
139: </mx:FormItem>
140: <mx:FormItem >
141: <s:ComboBox id="cbMicChoices" dataProvider="{microphoneList}" selectedIndex="0" dropShadowVisible="true" creationComplete="cbMicChoices_creationCompleteHandler(event)"/>
142: </mx:FormItem>
143: </mx:Form>
144: <mx:Form dropShadowVisible="true" borderAlpha="1.0" borderVisible="true" borderStyle="solid" width="300" height="132">
145: <mx:FormHeading label="System message:"/>
146: <mx:FormItem >
147: <s:RichText id="ErrorMessage" text="" />
148: </mx:FormItem>
149: </mx:Form>
150: </s:TileGroup>
151: </s:WindowedApplication>
Существует еще один вариант вариант медиапотока - живое видео вообще без FMS. C такого приложения peer-to-peer я начал этот топик - только в своем приложении я сделал связь по собственному протоколу, а фирменный проработанный Адобовский протокол для этой цели называется RTMFP. Этот протокол поддерживается тем же самым стандартным Netstream, через который можно работать с FMS. Но при использовании этого протокола есть еще немало хитростей - группы, аутентификация, топология и прочее - которые я на данный момент пока не асилил и работающих шаблонов кода для коммерческих проектов я на данный момент еще не наработал. Поэтому описывать что-либо по поводу связи peer-to-peer через RTMFP я воздержусь, а подробнее о протоколе RTMFP и коммуникациях peer-to-peer вы можете узнать непосредственно из роликов компании Adobe - RTMFP.
Вы можете также посмотреть еще мой - Simple Freeware OpenSource Flex Player for Web (one sound).
<SITEMAP> <MVC> <ASP> <NET> <DATA> <KIOSK> <FLEX> <SQL> <NOTES> <LINUX> <MONO> <FREEWARE> <DOCS> <ENG> <CHAT ME> <ABOUT ME> < THANKS ME> |