Предположим, вы хотите создать плагин для Erlyvideo. Легко ли это? Вообще-то легко, если знать как (и если вы умеете программировать на Erlang :) Но нюанс в том, что это дело пока что никак не документировано, и без прямого общения с Максом Лапшиным ничего сделать нельзя.
Ну есть один пример такого плагина. Но он не дает особой ясности. И там нет никакого взаимодействия с клиентом.
А нам хотелось бы иметь базовый функционал для такого взаимодействия:
Ну вот, этого для начала хватит. Сейчас я расскажу, как все это реализовать, и тем самым восполню недостаток информации. Сам проект здесь, только переключитесь на ветку base-features, а то master ушел далеко вперед с момента написания этой статьи.
Модуль представляет собой стандартное для Erlang OTP приложение. А именно, он включает:
и парочку модулей, собственно и выполняющих полезную работу:
erlypresence_event реализует behaviour gen_event, и erlyvideo любезно присылает ему события типа #erlyvideo_event, из которых нас интересуют user_connected и user_disconnected. Оный модуль перехватывает эти события и отправляет их на erlypresence_server. И больше ничего не делает.
erlypresence_server реализует behaviour gen_server, и делает все остальное: хранит, обрабатывает, принимает, отвечает -- все, что было перечислено в начале статьи как "базовый функционал". И все это мы рассмотрим чутка ниже.
Так уж вышло, что мне приходится использовать термин "модуль" в двух разных значениях. Весь этот проект представляет собой модуль для erlyvideo, и работает в контексте erlyvideo. Каждый отдельный erl-файл, это модуль в терминологии Erlang. В общем, когда мы говорим про отдельный erl-файл, то это модуль Erlang. А когда говорим про весь проект, то это модуль Erlyvideo.
Вообще-то оно тут описано. Но, наверное, надо подробнее, и по-русски :)
Когда мы скомпилируем наш проект, у нас в папке ebin появятся несколько beam файлов (байткод Erlang). Там еще лежит app файлы с метаданными OTP приложения. Теперь нужно это положить куда-нибудь, где Erlyvideo может их найти. А искать он их будет по пути {PATH}/erlypresence/ebin/, где {PATH} либо тот же каталог, где лежит сам erlyvideo, либо один из каталогов, указанных в erlyvideo.conf. По дефолту там так:
{paths, ["/var/lib/erlyvideo/plugins", "/usr/local/lib/erlyvideo/plugins"]}
но никто не мешает вам добавить любые свои каталоги.
То есть, должно быть либо так:
projects/erlyvideo/...
projects/erlypresence/ebin/
Либо так:
/usr/local/lib/erlyvideo/plugins/erlypresence/ebin/
Первый вариант удобен в разработке, ибо так мы просто пишем код и собираем проект, и нет надобности куда-либо перемещать содержимое ebin после каждой сборки. Второй вариант, очевидно, для продакшена.
Ну положить файлы в нужно место мало. Нужно еще попросить erlyvideo, чтобы он загружал этот модуль. Для этого нужно открыть конфиг erlyvideo/priv/erlyvideo.conf, добавить свой модуль сюда:
{modules,[erlypresence]}.
и еще добавить erlypresence_server сюда:
{rtmp_handlers, [{auth_users_limit, 200}, trusted_login, apps_push, remove_useless_prefix,
apps_streaming, apps_recording, erlypresence_server]},
в результате чего все вызовы методов с клиента будут обрабатываться не только дефолтными модулями, но и вашим (то бишь моим) erlypresence_server.
Вот и все, подключили.
В проекте есть тестовый флэш клиент. Нужно сказать пару слов о том, что он делает.
Он довольно прост.
Создает соединение
private function init() : void
{
_nc = new NetConnection();
_nc.client = this;
_nc.addEventListener(NetStatusEvent.NET_STATUS, onNetStatus);
_nc.addEventListener(IOErrorEvent.IO_ERROR, onError);
_nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
_nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onError);
}
коннектится
public function connect(rtmp : String, data : Object = null) : void
{
trace("connect to ", rtmp, data);
_nc.connect(rtmp, data);
}
вызывает метод на сервере
public function onConnect() : void
{
show("onConnect");
service.nc.call("hello", new Responder(helloFromServer), 123);
}
получает ответ
public function helloFromServer(data : Object) : void
{
show("helloFromServer " + data);
}
и принимает вызовы с сервера
public function callback1(data : Object) : void
{
if(listener) listener.onData("callback1", data, null);
}
public function callback2(data : Object, data2 : Object) : void
{
if(listener) listener.onData("callback2", data, data2);
}
События коннекта и дисконнекта ловит модуль erlypresence_event, и сообщает о них модулю erlypresence_server
handle_event(#erlyvideo_event{event = user_connected, session_id = SessionId, user = Client}, State) ->
gen_server:cast(erlypresence_server, {user_connected, SessionId, Client}),
{ok, State};
handle_event(#erlyvideo_event{event = user_disconnected, session_id = SessionId}, State) ->
gen_server:cast(erlypresence_server, {user_disconnected, SessionId}),
{ok, State};
erlypresence_server имеет объект состояния, в котором хранит список онлайн клиентов.
-record(online_clients, {clients}).
init([]) ->
{ok, #online_clients{clients = dict:new()}}.
И он обновляет этот список при коннекте новых клиентов и дисконнекте старых.
handle_cast({user_connected, SessionId, Client}, #online_clients{clients = Clients}) ->
NewClients = dict:store(SessionId, Client, Clients),
io:format("user connected ~p ~p ~n", [SessionId, dict:to_list(NewClients)]),
{noreply, #online_clients{clients = NewClients}};
handle_cast({user_disconnected, SessionId}, #online_clients{clients = Clients}) ->
NewClients = dict:erase(SessionId, Clients),
io:format("user disconnected ~p ~p ~n", [SessionId, dict:to_list(NewClients)]),
{noreply, #online_clients{clients = NewClients}};
Клиент вызывает метод hello на сервере, передает ему аргумент 123, и определяет callback-функцию, которая примет ответ сервера.
service.nc.call("hello", new Responder(helloFromServer), 123);
На сервере в модуле erlypresence_server определена функция hello/2, которая получает два аргумента -- запись #rtmp_session и запись #rtmp_funcall. Первая запись нам нужна целиком, а из второй записи берем аргументы и id вызова. После чего с помощью rtmp_session:reply мы можем ответить клиенту.
hello(RtmpSession, #rtmp_funcall{id = CallId, args = Args}) ->
rtmp_session:reply(RtmpSession, #rtmp_funcall{id = CallId, stream_id = 0, args = [null, 456]}),
RtmpSession.
И клиент получает ответ в своей callback-функции.
public function helloFromServer(data : Object) : void
{
show("helloFromServer " + data);
}
Это тож не сложно. Серверу достаточно сделать так:
rtmp_socket:invoke(Client, 0, 'callback1', [456]),
или так
rtmp_socket:invoke(Client, 0, 'callback2', [789, <<"abc">>]),
Для этого ему нужна ссылка на Pid процесса клиента. А его можно взять из #rtmp_session
#rtmp_session{socket = Client} = RtmpSession
Ну и на клиенте сработают соответствующие методы в объекте, который определен как client для NetConnection
_nc = new NetConnection();
_nc.client = this;
public function callback1(data : Object) : void
{
if(listener) listener.onData("callback1", data, null);
}
public function callback2(data : Object, data2 : Object) : void
{
if(listener) listener.onData("callback2", data, data2);
}
Разумеется, число аргументов и их типы должны совпасть, иначе на клиенте будет брошено исключение.
Тут чуть-чуть сложнее, ибо нам нужно иметь список всех клиентов, и вызвать rtmp_socket:invoke для каждого. Прямо в функции hello/2 такого списка нет. Поэтому нужно действовать через API gen_server
hello(RtmpSession, RtmpCall) ->
gen_server:cast(erlypresence_server, {broadcast, 'callback2', [<<"broadcast">>, <<":)">>]}),
RtmpSession.
handle_cast({broadcast, Command, Args}, #online_clients{clients = Clients} = State) ->
io:format("broadcast ~p ~p ~n", [Command, Args]),
[rtmp_socket:invoke(Client, 0, Command, Args) || {_, Client} И так метод callback2 будет вызван на всех подключенных клиентах.
А это просто
hello(RtmpSession, RtmpCall) ->
rtmp_session:close_connection(RtmpSession),
RtmpSession.
Вуаля, теперь у нас есть все, что нужно, чтобы писать модули под erlyvideo с мегасложной кастомной бизнес логикой :)
Comments
MicronXD (not verified)
Wed, 06/01/2011 - 15:59
Permalink
Спасибо
Thank you very much for this. I'm an American reading this through google translate. perhaps you could describe erlyvideo authentication and authorization in equally as much detail as my friends and I have had a bit of trouble understanding bit of Max's English that we can find in the documentation. I'd be happy to write an English version after we figure it out ourselves.
yzh44yzh
Wed, 06/01/2011 - 17:17
Permalink
Hello MicronXD.
Hello MicronXD.
Erlyvideo documentation is weak at the moment, but they are working on it.
Russian version is ready ( http://erlyvideo.org/doc-ru ), but English version is in progress and not published yet.
http://erlyvideo.org/doc-ru/ch04.htm try this with google translate
Komizart (not verified)
Fri, 01/27/2012 - 19:53
Permalink
Спасибо! просто и понятно
Спасибо! просто и понятно рассказал основы
Add new comment