Skip to content

Instantly share code, notes, and snippets.

@nwalker
Created June 25, 2012 07:47
Show Gist options
  • Save nwalker/2987224 to your computer and use it in GitHub Desktop.
Save nwalker/2987224 to your computer and use it in GitHub Desktop.
документация по системе

Installation for v1.4

Streaming Server

Зависимости

  • Erlang R15B. Можно из репозитория, если есть подходящий.
  • Erlyvideo, именно отсюда, там мелкие нужные патчи.

Установка

Собрать erlyvideo из исходников(make tgz) на системе той же архитектуры, что и целевая.

Полученный архив распаковать на целевой машине в /opt(по умолчанию, можно просто поправить init-скрипт для другого пути).

bash$ cd /opt
bash$ tar -xzF erlyvideo-X.XX.XX.tar.gz
bash$ mkdir /etc/erlyvideo
bash$ cp erlyvideo/etc/erlyvideo.conf.sample /etc/erlyvideo/erlyvideo.conf
bash$ cp erlyvideo/etc/log4erl.conf.sample /etc/erlyvideo/log4erl.conf
bash$ cp /opt/erlyvideo/bin/erlyvideo-initscript /etc/init.d/erlyvideo

Запустить erlyvideo.

bash$ /etc/init.d/erlyvideo start

Собрать модуль для erlyvideo на целевой системе или системе той же архитектуры и с той же версией Erlang:

bash$ mkdir /opt/ev
bash$ cd /opt/ev
bash$ git clone ssh://git.work/GIT/webinar.git
bash$ cd webinar
bash$ make module

Настройка

Отредактировать /etc/erlyvideo/erlyvideo.conf, см. официальную документацию:

{paths, ["/var/lib/erlyvideo/plugins", "/usr/local/lib/erlyvideo/plugins", "/opt/ev"]}. %% добавить путь к папке, в которой лежит модуль
{modules,[webinar]}. %% собственно модуль

{vhosts, [
	%% заменить default на нужный vhost
	{default, [
		{rtmp_handlers, [webinar_api, remove_useless_prefix, apps_streaming, apps_recording]}, %% выкинуть все остальные хэндлеры авторизации
		{webinar, [
			%% для ретранслятора раскомментировать следующую строку, вписать в нее адрес мастер-сервера
			% {master, "rtmp://192.168.0.16/streams/"}
			%% адрес, где висит Auth Server, обязательно без trailng slash
			{api_url, "http://192.168.0.115:8000/api"}
		]},
	%% остальная конфигурация
]}.

Auth Server

Зависимости

  • Python 2, 2.6+
  • предпочтительно pip, virtualenv
  • пакеты из webapp/requirements.txt(pip install -r requirements.txt)
  • WSGI-сервер на свой вкус(uwsgi, например)
  • установленный Branstalkd-сервер.

Установка

Обычное развертывание Django-проекта, ничего особенного. Пример settings.py - webinar/webapp/webinar/settings.example.py. Единственный нестандартный элемент конфига - BS_SERVER, адрес:порт Beanstalkd-сервера. MEDIA_ROOT и MEDIA_URL должны быть корректно настроены и обслуживаться внешним веб-сервером. django.contrib.sites также должно быть настроено верно, Site.objects.get_current() должно возвращать правильный домен.

Настройка

В принципе, не требуется.

File Server

Зависимости

  • Python 2, 2.6+
  • OpenOffice.org / LibreOffice(3.5, в 3.4 баг), который может работать в headless mode. Для презентаций (PPT/ODP).
  • GhostScript, для PDF.
  • python-uno
  • установленный Branstalkd-сервер.

Установка

Использовать нормальный изолированный virtualenv не получится, python-uno ставится только глобально. Прочие зависимости в webapp/requirements.fs.txt. Воркер оформлен как django management command. В settings.py нужно прописать корректные BS_SERVER, STATIC_ROOT, STATIC_PATH. STATIC_ROOT дожна быть доступна на запись. django.contrib.sites должно быть настроено верно, Site.objects.get_current() должно возвращать правильный домен.

В monit под рестарт по лимиту оперативной памяти поставить

libreoffice "--accept=socket,host=localhost,port=8100;urp;StarOffice.ServiceManager" --norestore --nofirststartwizard --nologo --headless

Инстанс воркера запускается командой webinar/webapp/manage.py bs_worker. Тоже лучше завернуть в monit несколько инстансов. Порт 8100 сейчас hardcoded. Позже перестанет быть таким.

Настройка

Необходимые параметры конфигурации перечислены выше. Также см. webinar/webapp/manage.py bs_worker --help. Он умеет форкаться, но насколько это юзабельно - большой вопрос.

Internal communications for v1.4

Условные обозначения

  • AS = Auth Server, отвечает за веб-интерфейс, авторизацию, хранение логических данных.
  • FS = File Server, отвечает за обработку загруженных пользователями файлов и их выдачу.
  • SS = Streaming Server, модуль для erlyvideo, отвечает за все рантайм-аспекты(роутинг сообщений, права доступа, хранение данных в рантайм).
  • Cli = Client, Flash/Flex приложение, отвечает за весь пользовательский интерфейс.

Общая структура системы

Обзор

Общую архитектуру системы я уже неоднократно описывал, на ней я заострять внимания здесь не буду, перечислю только пути взаимодействия компонент:

  • AS <--[ HTTP ]--> SS
  • Cli <--[ RTMP ]--> SS
  • AS --[ Beanstalkd ]--> FS --[ HTTP ]--> AS
  • Cli <--[ HTTP ]-- FS

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

AS <--[ HTTP ]--> SS

AS предоставляет 6 точек доступа, из них SS использует 5 - 4 на чтение, 1 на запись. Все GET-методы возвращают JSON.

/api/token

Method: GET

Result: { token: "xxxxx" }

Возвращает токен авторизации пользователя для подключения к конференции. Используется из JS на клиенте, полученный токен передается Cli при запуске.

/api/token/(?P[a-zA-Z0-9_.-]+)

Method: GET

Result:

{
	'uid' : int, // user id
	'cid' : int, // conference id
	'role' : string, // user role
	'username' : string, // username to display
	'confname' : string, // conference name, currently not used
}

Возвращает данные связаные с токеном. Запрашивается SS при подключении клиента(см. docs/protocol.md, Аутентификация).

/api/token/(?P[a-zA-Z0-9_.-]+)/presentations

Method: GET

Result:

[
	{
		"url" : "http://somewhere/index1", // адрес индекс-файла презентации или видео
		"label" : "bla-bla-bla" // просто какой-то описательный текст или заголовок
	},
	{
		"url" : "http://somewhere/index2",
		"label" : "bla-bla-bla2"
	},
	...
]

Список прикрепленных к конференции файлов, запрашивается после удачной авторизации активного пользователя в SS. Подробнее про описания, на которые ссылается url будет рассказано дальше, в описании коммуникаций Cli <-- FS.

/api/conference/(?P\d+)

Method: GET

Result:

{
	cid = event.id,
	name = event.name,
	start_time = unixtime(event.start_time), // UTC
	end_time = unixtime(event.end_time), // UTC
}

Данные о конференции, запрашиваются SS при подключении первого пользователя этой конференции. Имена переменных полностью описывают их суть. Время возвращается в UTC.

/api/conference/(?P\d+)/dump

Method: POST

Body: not decided yet, for now it is

{
	messages: [
		{
			id,
			author_id, author_name,
			timestamp,
			text
		}
	]
}

Result: None

На этот URL SS скидывает данные в момент завершения конференции. На данный момент только сообщения чата.

AS --[ Beanstalkd ]--> FS --[ HTTP ]--> AS

Диаграмма процесса:

AS --[ Beanstalkd ]--> FS

Здесь все просто:

  • пользователь загружает файл
  • AS создает запись о нем в БД
  • и кидает уведомление о нем в очередь Beanstalkd
  • его вытаскивает из очереди свободный воркер
  • и обрабатывает(см. ниже)

Уведомление следующего вида:

{
	id = sender.id,
    url = sender.file.url,
    label = sender.name,
    original_name = sender.name,
    notify_url = join_domain(notify_url),
}

Из всех параметров по настоящему важны только url и notify_url. По адресу url доступен оригинальный файл, загруженный пользователем. Адрес notify_url - коллбэк, по нему отписывается так или иначе закончивший работу воркер.

FS --[ HTTP ]--> AS

Сам процесс обработки выглядит примерно так:

  • воркер вытаскивает задание из очереди
  • скачивает к себе оригинальный файл
  • в зависимости от его типа выбирает нужный обработчик(LibreOffice/GhostScript)
  • скармливает файл обработчику.
  • результаты обработки воркер кидает POST-запросом на notify_url

В случае ошибки обработки воркер может не сразу сообщить об этом на AS, а положить задачу обратно в очередь с таймаутом на вынимание. Это производится в случаях, когда ошибка предполагается временной(например, потеряно соединение с LibreOffice).

Результатом обработки может быть url индекс-файла презентации или сообщение об ошибке. Индекс-файл выглядит примерно так:

{
	'label' : 'some text',
	'original_name' : 'original_file_name.pdf',
	'pages' : [
		"http://somewhere/page1.png",
		"http://somewhere/page2.png",
		"http://somewhere/page3.png",
		....
		"http://somewhere/pageN.png",
	]
}

Главная часть - массив pages, это файлы слайдов, которые будут непосредственно запрашиваться клиентами.

Cli <--[ RTMP ]--> SS

См. docs/protocol.md

Cli <--[ HTTP ]-- FS

Клиенты активных пользователей через RTMP-вызов get_presentations : Array<String> получаются список URL индекс-файлов своих презентаций, которые используются ими при управлении презентацией. Ссылки на индекс-файлы получают только их авторы, остальные пользователи получают только URL-слайдов, которые запрашивают с FS по HTTP.

Protocol documentation for v1.4

Meta

В качестве транспорта используется Flash.Network.NetConnection и AMF3. В протоколе есть и push-, и pull-методы. В дальнейшем, push-методы помечены [MSG] и далее именуются сообщениями, pull - [RPC]. Для push-вызовов используется единая точка входа event

public function event(type : String, message : String, data : Object) : void

Этот метод должен быть определен у объекта, используемого как NetConnection.client.

Аутентификация

Аутентификация проводится при вызове NetConnection.connect через дополнительный параметр, передаваемый клиенту через flashvars:

var token = loaderInfo.parameters.token;
var _nc = new NetConnection();
_nc.connect(server, token);

После этого клиент должен ожидать либо разрыва соединения(NetConnection.Connect.Rejected, не авторизован), либо вызова event("init", role, self:User).

Роли пользователей

Роли пользователей по сути всего лишь маркеры для наборов разрешений. Итоговый набор разрешений пользователя может быть изменен на лету админом. На данный момент эти наборы разрешений выглядят так:

default_acl(<<"admin">>) ->
    [can_grant,
     can_prolong,
     can_moderate
     | default_acl(<<"active">>) ];

default_acl(<<"chosen_one">>) ->
    [can_present,
     can_start_vote];

default_acl(<<"active">>) ->
    [can_chat,
     can_ask_voice];

default_acl(<<"passive">>) ->
    [can_vote].

Пояснения по коду выше:

  • роль admin расширяет роль active.
  • роль chosen_one не является самостоятельной, это mixin, добавляемый к active/admin.

API Meta

Нижеследующее описание методов RPC сгруппировано по логическому типу сообщения. Под записью

presence

[MSG] userJoin(User)

следует понимать следующий вызов с сервера:

event("presence", "userJoin", data : User)

Описанные следующим образом функции

chat

[RPC] message (ChatMessage())

[RPC] load_chat : Array<Message>

следует вызывать соответственно так:

var nc: NetConnection;
....
var msg = new ChatMessage();
msg.text = "some text";
nc.call('message', null, msg);

и так:

function onChatLoad(messages : Array) : void
{
    for each(var u : ChatMessage in messages) {
        ....
    }
}

var nc: NetConnection;
....
nc.call('load_chat', new Responder(onChatLoad));

API

presence

[MSG] userJoin(User) пользователь подключился

[MSG] userLeave(User) пользователь отключился

[MSG] askingVoice(SessID) пользователь просит голос

[RPC] list_users : Array<User> получить список пользователей

[RPC] get_asks : Array<int> возвращает список просьб голоса(SessID пользователя), в правильном порядке.

chat

[MSG] chatMessage (ChatMessage) Пришло новое сообщение в чат

[RPC] message (ChatMessage()) Отправить сообщение

[RPC] load_chat : Array<ChatMessage> Загрузить все сообщения чата

moderate(админам и модераторам)

[MSG] moderateMessage (ChatMessage) Новое сообщение на модерацию.

[MSG] removeMessage (ChatMessage) Модерация сообщения выполнена другим модератором

[RPC] load_unmoderated : Array<ChatMessage> Загрузка всего списка немодерированных сообщений.

[RPC] accept (ChatMessage) Пропустить сообщение в общий чат.

[RPC] reject (ChatMessage) Заблокировать сообщение.

[RPC] pass_up (ChatMessage) Отправить сообщение ведущему/ведущим.

publish

[MSG] startPublish (StreamData) Начать публикацию потока на сервер. В StreamData смысл имеет только name, соединение используется уже установленное.

[MSG] stopPublish Очевидно.

play

[MSG] addStream (StreamData) Добавить поток в отображение. Если StreamData.server_url == null, использовать текущее соединение с сервером.

[MSG] updateStream (StreamData) Обновить данные потока c id == StreamData.id.

[MSG] removeStream (ID) Очевидно.

[RPC] get_streams : Array<StreamData> Получить список отображаемых потоков.

present

Здесь я еще ничего не решил пока.

[MSG] set_url (URL) TODO

[RPC] get_presentations : Array<String> Список URL индекс-файлов презентаций, которые пользователь добавил к конференции в веб-интерфейсе. Главная часть индекс-файла - массив pages, в котором находятся непосредственно URL отдельных слайдов.

[RPC] set_slide (URL) TODO

polls

[RPC] get_polls : Array<Poll> получить список своих опросов

[RPC] start_poll (PollID, ?timeout) начать опрос, опционально время опроса

[RPC] vote (PollID, indexOfAnswer) ответ на опрос. indexOfAnswer - индекс выбранного ответа в Poll.answers.

[MSG] newPoll (Poll) Уведомление о старте опроса.

[MSG] newVote ({PollId, indexOfAnswer}) Уведомление о голосе от пользователя (для реалтайм отображения процесса опроса).

Типы данных

Presentation {
	id: Number;
    label, slug : String;
    slides : Array<String>;
}

ChatMessage {
	registerClassAlias("Message", ChatMessage);

    id : Number; // filled on server
    author_name, author_id : String; // filled on server
    timestamp : Date = new Date();
    text : String;
}

User {
	registerClassAlias("User", User);

    // TODO: здесь должен быть какой-то вменяемый ID.
    id : Number;
    name : String;
    role : String;
}

StreamData {
    id : Number,
    server_url : String; // if null use main server
    stream_name : String;
    audio_only : Boolean;
}

Poll {
	id: Number;
	question: String;
	maxAnswersCount: Number; // количество возможных вариантов ответа.
							 // если >1 - пользователь может проголосовать за несколько пунктов.
							 // но это в будущем - сейчас можно только один ответ.
	answers: Array<String>;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment