Привет мир. Задумал я упростить взаимодействие флэш-клиента с FMS и другими медиа-серверами, да написать расширение для Mate, дабы работало это взаимодействие прямо из карты событий, аналогично как работают в Mate HttpServiceInvoker, RemoteObjectInvoker, WebServiceInvoker.
Ну написал и выложил на github. Там помимо расширения есть еще и небольшой проект для примера (примитивный чат), в котором оное расширение юзается. Так что разобраться будет не сложно.
Тем не менее, здесь, в этой статье, я распишу все подробненько, разложу по полочкам.
Но начну издалека. В разное время у меня были разные подходы к такому взаимодействию. Думаю есть смысл пройтись по ним, дабы осознать те проблемы, которые я попытался решить этим расширением.
Первый подход был весьма простым архитектурно. Были два класса: FmsService и FmsClient.
FmsService соеденялся с сервером, посылал запросы и опционально получал асинхронные ответы на них. FmsClient содержал callback-методы, которые сервер вызывал активно.
Выглядело примерно так:
public class FmsService
{
private var nc : NetConnection;
public function doSomething1(param1 : String, param2 : int) : void
{
this.nc.call("doSomething1", new Responder(onResult1), param1, param2);
}
public function onResult1(param1 : Object) : void
{
}
public function doSomething2(param1 : String, param2 : int, param3 : Boolean) : void
{
this.nc.call("doSomething2", new Responder(onResult2), param1, param2, param3);
}
public function onResult2(param1 : Object) : void
{
}
public function doSomething3(param1 : Number) : void
{
this.nc.call("doSomething3", null, param1);
}
...
public function doSomething100500(param1 : Number) : void
{
this.nc.call("doSomething100500", null, param1);
}
}
public class FmsClient
{
public function callback1(param1 : int, param2 : Boolean) : void
{
}
public function callback2(param1 : int, param2 : Boolean) : void
{
}
...
public function callback100500(param1 : int, param2 : Boolean) : void
{
}
}
Штука относительно удобная, но одна беда -- в большом проекте таких методов накапливается много. Само по себе это не так и страшно. Но видно, что методы FmsService весьма однообразны, шаблонны. Они отличаются именами и аргументами, но содержат одинаковую логику -- вызов nc.call и передачей ему параметров, с Responder или без. Налицо дублирование кода, и это хочется исправить.
Сперва кажется, что дублирование легко исправить. Первое, что приходит в голову:
public class FmsService
{
public function call(methodName : String, param1 : Object, param2 : Object) : void
{
this.nc.call(methodName, null, param1, param2);
}
}
Однако это не очень хорошо -- плохая работа с аргументами. И, самое главное, не ясно, как использовать респондеры. Вариант чуть лучше:
public class FmsService
{
public function call(methodName : String, result : Function, ...params) : void
{
this.nc.call(methodName, new Responder(result), params[0], params[1]);
}
}
Как бы лучше. Но только не ясно, зачем тогда нам вообще нужен класс FmsService? С тем же успехом мы можем напрямую использовать NetConnection, не оборачивая его в FmsService.
В общем, я придумал более-менее юзабельную штуку, наследовав FmsService от Proxy. Я писал об этом раньше: Прокси всемогущий, повторятся не буду, гляньте по линку.
Proxy позволил убрать из FmsService все методы, где не используется Responder. И спокойно вызывать из карты Mate что угодно, лишь бы оно было определено на стороне FMS.
<mate:EventHandlers type="{RoomEvent.JOIN}">
<mate:MethodInvoker generator="{FmsService}"
method="JoinRoom" arguments="{[event.roomID]}"/>
</mate:EventHandlers>
Однако не так удобно было работать с теми методами, где нужен Responder. В этом случае метод нужно явно определить в классе FmsService, ибо ему нужен метод-обработчик ответа. А передать метод-обработчик из карты не так просто (ну я решил не копать в этом направлении).
Что же касается FmsClient, то в нем выгода от Proxy и вовсе неясна. Все callback-методы разные, выделить что-то общее проблематично. Тем не менее, их можно унифицировать. Например, я решил, что эти методы ничего сами не будут делать, а только передавать информацию куда-то дальше. И это удобно делать, генерируя в callback-методах кастомные события, которые будут ловиться в карте Mate. Я так и сделал.
Мне удалось уменьшить количество шаблонного кода в FmsService, но не в FmsClient. Так или иначе, но тут каждый callback все равно должен быть определен явно.
В итоге классы FmsService и FmsClient представляют собой промежуточный слой между FMS сервером и картой Mate. Методы FmsService вызываются из карты напрямую, а информация от callback в FmsClient идет в карту в кастомных событиях.
И все это плохо, ибо нужно писать много кода: много методов, много callbackов, много кастомных событий. Нужно писать тупой, механический код, который всего лишь перекидывает инфу из одного места в другое.
Сам по себе этот промежуточный слой, он нужный и полезный. Но требует больше усилий на свое поддержание, чем я хочу на него потратить.
Ну ладно, если это плохо, то что хорошо? А за примером хорошего далеко ходить не надо, ибо в том же Mate есть HttpServiceInvoker, которым удобно пользоваться.
<mate:EventHandlers type="{ArticleEvent.LOAD_LIST}">
<mate:HTTPServiceInvoker instance="{service}">
<mate:Request action="getArticlesList" showDrafts="{event.showDrafts}"/>
<mate:resultHandlers>
<mate:MethodInvoker generator="{ArticlesManager}"
method="initFromXMLData" arguments="{resultObject}"/>
</mate:resultHandlers>
<mate:faultHandlers>
<!-- TODO show error to user -->
</mate:faultHandlers>
</mate:HTTPServiceInvoker>
</mate:EventHandlers>
И вот тоже самое я хочу для FMS. Классы FmsService и FmsClient останутся, но они должны уйти в библиотеку и быть идентичными для всех проектов. Я не хочу в каждом проекте создавать уникальные FmsService и FmsClient и заполнять их кучей уникальных для этого проекта методов, отражающих API сервера.
API сервера описано и реализовано на сервере. Я не хочу дублировать его на клиенте. Даже частично.
Ну и все это я сделал.
Итак, я сделал библиотеку и выложил ее на github.
Она небольшая, там всего лишь 3 класса и 4 события. Я решил все классы назвать с префиксом Rtmp, а не Fms, ибо на стороне клиента не важно, какой именно медиа сервер используется. Код идентичен для FMS, Wowza, Red5.
RtmpService -- создает NetConnection и соеденяется с FMS. Можно вызвать любые методы на FMS и передать любые параметры через один метод этого класса:
public function call(methodName : String, ... args) : void
{
logger.info('call [' + methodName + ']', args);
args.unshift(new Responder(onResult, onCallStatus));
args.unshift(methodName);
_netConnection.call.apply(_netConnection, args);
}
Ответ FMS или ошибка будут обработаны в методах onResult и onCallStatus и с помощью событий RtmpResultEvent или RtmpErrorEvent будут отправлены в карту (или еще куда-то, где будут их слушать).
Еще RtmpService регистирует callback для RtmpClient (который internal class и не доступен внешним объектам). callback может быть любым методом любого объекта (для Mate это будет какой-нибудь менеджер). Если FMS вызовет какой-нибудь метод, для которого не зарегистрирован callback, то RtmpClient отправит событие RtmpDataEvent.
Это все можно использовать без Mate и без Flex в чистых AS проектах.
А вот чтобы подружить это с Mate (и под эту дружбу все и затачивалось), нужен класс RtmpServiceInvoker. В нем небольше количество Mate-магии, позволяющее делать в карте такое:
<mate:EventHandlers type="{RoomEvent.LOAD_LIST}">
<rtmp:RtmpServiceInvoker generator="{RtmpService}">
<mate:Request action="getRooms"/>
<rtmp:resultHandlers>
<mate:MethodInvoker generator="{MainManager}"
method="onRooms" arguments="{[currentEvent.data]}"/>
</rtmp:resultHandlers>
<rtmp:faultHandlers>
<mate:MethodInvoker generator="{MainManager}"
method="onError" arguments="{[currentEvent]}"/>
</rtmp:faultHandlers>
</rtmp:RtmpServiceInvoker>
</mate:EventHandlers>
Красота! Весь код -- запрос на FMS и получение результата -- сосредоточен в карте, нам не нужно ничего добавлять на уровне RtmpService.
Но нужно немного приложить усилий, чтобы обрабатывать активные вызовы со стороны FMS. Допустим, у нас несколько менеджеров, которые должны реагировать на такие вызовы. Тогда мы должны передать RtmpService в эти менеджеры, и каждый менеджер зарегистрирует те callback, которые ему нужны.
<mate:EventHandlers type="{FlexEvent.INITIALIZE}">
<mate:ObjectBuilder generator="{RtmpService}"
constructorArguments="{[scope.dispatcher]}"/>
<mate:MethodInvoker generator="{MainManager}"
method="initService" arguments="{lastReturn}"/>
</mate:EventHandlers>
public class MainManager
{
public function initService(service : RtmpService) : void
{
service.registerCallback('newMessage', onNewMessage);
service.registerCallback('ban', onBan);
service.registerCallback('kick', onKick);
}
}
Конечно, это можно делать и через инъекции в сеттеры:
<mate:Injectors target="{SomeManager}">
<mate:PropertyInjector source="{RtmpService}" targetKey="service"/>
</mate:Injectors>
<mate:Injectors target="{OtherManager}">
<mate:PropertyInjector source="{RtmpService}" targetKey="service"/>
</mate:Injectors>
public class SomeManager
{
public function set service(value : RtmpService) : void
{
service.registerCallback('newMessage', onNewMessage);
service.registerCallback('ban', onBan);
service.registerCallback('kick', onKick);
}
}
Ну вот, жизнь стала легче.
PROFIT :)
UPDATE
<mate:EventHandlers type="{FlexEvent.INITIALIZE}">
<mate:ObjectBuilder generator="{RtmpService}"
constructorArguments="{[scope.dispatcher]}"/>
<mate:MethodInvoker generator="{RtmpService}"
method="connect" arguments="{'rtmp://localhost/rtmp-service-mate-ext/'}"/>
<rtmp:CallbackRegistrator action="newMessage" target="{MainManager}" method="onNewMessage"/>
</mate:EventHandlers>
Так тоже теперь работает :)
Add new comment