Расширение для Mate RtmpServiceInvoker.

Привет мир. Задумал я упростить взаимодействие флэш-клиента с FMS и другими медиа-серверами, да написать расширение для Mate, дабы работало это взаимодействие прямо из карты событий, аналогично как работают в Mate HttpServiceInvoker, RemoteObjectInvoker, WebServiceInvoker.

Ну написал и выложил на github. Там помимо расширения есть еще и небольшой проект для примера (примитивный чат), в котором оное расширение юзается. Так что разобраться будет не сложно.

Тем не менее, здесь, в этой статье, я распишу все подробненько, разложу по полочкам.

Но начну издалека. В разное время у меня были разные подходы к такому взаимодействию. Думаю есть смысл пройтись по ним, дабы осознать те проблемы, которые я попытался решить этим расширением.

FmsService и FmsClient

Первый подход был весьма простым архитектурно. Были два класса: 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 или без. Налицо дублирование кода, и это хочется исправить.

Proxy

Сперва кажется, что дублирование легко исправить. Первое, что приходит в голову:


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 сервера описано и реализовано на сервере. Я не хочу дублировать его на клиенте. Даже частично.

Ну и все это я сделал.

Rtmp Service Mate Extention

Итак, я сделал библиотеку и выложил ее на 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

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.