Создание плагинов под Erlyvideo

Предположим, вы хотите создать плагин для 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.

Подключение к 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);
}

Разумеется, число аргументов и их типы должны совпасть, иначе на клиенте будет брошено исключение.

Сервер рассылает данные всем клиентам (broadcast)

Тут чуть-чуть сложнее, ибо нам нужно иметь список всех клиентов, и вызвать 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

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's picture

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

Спасибо! просто и понятно рассказал основы

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
question for bots )
Image CAPTCHA
Enter the characters shown in the image.