Русский
Русский
English
Статистика
Реклама

Video

Из песочницы OBS Studio Lua Скриптинг

25.09.2020 18:15:34 | Автор: admin

Всем привет, в этом руководстве рассмотрим создание скриптов для OBS на языке Lua.


Скриптинг в OBS доступен начиная с версии 21, на данный момент новейшая 26.0.0-rc3 версия доступна для тестирования.Обновление включает в себя виртуальную веб камеру (пока что только на Windows), улучшенный UI, возможность скриншота любого источника( КДПВ была сделана с помощью этой функции).


image


Описание глобальных функций, API, настроек


Добавить скрипт можно через меню -> Инструменты -> Скрипты -> значок "плюс".
Скрипты могут быть добавлены, перезагружены, удалены в режиме реального времени.


Сходства и различия c С-API


Сходства: почти полный доступ к API, СБОЙ или УТЕЧКА ПАМЯТИ с неправильно написанным скриптом.


Различия: некоторые функции(с двойными указателями) недоступны, некоторые заменены на другие.


У каждого скрипта своё пространство имён, убедиться в этом можно открыв текущую коллекцию сцен "~/obs-studio/basic/scenes".


Настройки settings представляют собой JSON строку, они могут быть созданы/загружены/сохранены с помощью JSON строк или файлов.


Описание функций:


  • obslua модуль для доступа к функциям OBS
  • script_description() описание скрипта, поддерживает примитивный HTML
  • script_properties() пользовательский интерфейс
  • script_defaults(settings) устанавливает настройки по умолчанию
  • script_update(settings) вызывается каждый раз когда пользователь изменил настройки через пользовательский интерфейс
  • script_load(settings) загружает настройки при первом запуске
  • script_unload() вызывается при закрытии скрипта
  • script_save(settings) используется в основном для сохранения горячих клавиш, настройки c пользовательского интерфейса сохраняются автоматически
  • script_tick(seconds) вызывается каждый кадр, аргумент seconds получает значение потраченных секунд с предыдущего кадра
  • script_path() возвращает абсолютный путь к папке скрипта
  • timer_add(callback,milliseconds) вызов функции периодично
  • timer_remove(callback) удаление функции с таймера, также есть вариант использовать remove_current_callback() внутри функции которая вызывается периодично

Пример скрипта


Скрипт: Движение по линии с использованием кнопок и таймера.


local obs = obslualocal selected_sourcepos = obs.vec2()switch = falsecounter = 0

Короткая запись модуля, local var инициализация значения как nil, pos структура предоставляемая OBS для перемещения источников на сцене.


function script_properties()  local props = obs.obs_properties_create()  obs.obs_properties_add_button(props, "button1", "Вкл/Выкл",on_off)  obs.obs_properties_add_button(props, "button2", "Добавить источник",add_source)  obs.obs_properties_add_button(props, "button3", "Подвинуть источник на +10,0",move_button)  local p = obs.obs_properties_add_list(props, "selected_source", "Выберите источник", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)  local sources = obs.obs_enum_sources()  if sources ~= nil then    for _, source in ipairs(sources) do      source_id = obs.obs_source_get_unversioned_id(source)      if source_id == "color_source" then        local name = obs.obs_source_get_name(source)        obs.obs_property_list_add_string(p, name, name)      end    end  end  obs.source_list_release(sources)  return propsend

Добавляем пользовательский интерфейс. obs.obs_properties_add_button(props, "имя", "Описание",функция), local p = obs.obs_properties_add_list выпадающие меню с выбором источника, source_id = obs.obs_source_get_unversioned_id(source) получение имени источника при этом игнорируя его версию, obs.source_list_release(sources) освобождение памяти


function script_update(settings)  selected_source = obs.obs_data_get_string(settings,"selected_source")end

Обновление selected_source каждый раз когда настройки (выпадающее меню в этом случае) изменены.


function add_source()  current_scene = obs.obs_frontend_get_current_scene()  scene = obs.obs_scene_from_source(current_scene)  settings = obs.obs_data_create()  counter = counter + 1  green = 0xff00ff00  hotkey_data = nil  obs.obs_data_set_int(settings, "width",200)  obs.obs_data_set_int(settings, "height",200)  obs.obs_data_set_int(settings, "color",green)  source = obs.obs_source_create("color_source", "ист#" .. counter, settings, hotkey_data)  obs.obs_scene_add(scene, source)  obs.obs_scene_release(scene)  obs.obs_data_release(settings)  obs.obs_source_release(source)end

Выбор сцены и создание настроек для источника, добавление на сцену, освобождение памяти.


function move_source_on_scene()  current_scene = obs.obs_frontend_get_current_scene()  scene = obs.obs_scene_from_source(current_scene)  scene_item = obs.obs_scene_find_source(scene, selected_source)  if scene_item then    dx, dy = 10, 0    obs.obs_sceneitem_get_pos( scene_item, pos) -- обновить позицию если источник был перемещён мышкой    pos.x = pos.x + dx    pos.y = pos.y + dy    obs.obs_sceneitem_set_pos(scene_item, pos)   end  obs.obs_scene_release(scene)end

Функция перемещения источника в рамках сцены.


function move_button(props,p)  move_source_on_scene()end

Кнопка перемещения источника и 2 необходимых аргумента.


function on_off()  if switch then     obs.timer_add(move_source_on_scene,50)  else    obs.timer_remove(move_source_on_scene)  end  switch = not switchend

Кнопка переключатель и таймер периодического запуска функции в миллисекундах.
Гифка


Исходный код
local obs = obslualocal selected_sourcepos = obs.vec2()switch = falsecounter = 0function on_off()  if switch then     obs.timer_add(move_source_on_scene,50)  else    obs.timer_remove(move_source_on_scene)  end  switch = not switchendfunction add_source()  current_scene = obs.obs_frontend_get_current_scene()  scene = obs.obs_scene_from_source(current_scene)  settings = obs.obs_data_create()  counter = counter + 1  green = 0xff00ff00  hotkey_data = nil  obs.obs_data_set_int(settings, "width",200)  obs.obs_data_set_int(settings, "height",200)  obs.obs_data_set_int(settings, "color",green)  source = obs.obs_source_create("color_source", "ист#" .. counter, settings, hotkey_data)  obs.obs_scene_add(scene, source)  obs.obs_scene_release(scene)  obs.obs_data_release(settings)  obs.obs_source_release(source)endfunction move_button(props,p)  move_source_on_scene()endfunction move_source_on_scene()  current_scene = obs.obs_frontend_get_current_scene()  scene = obs.obs_scene_from_source(current_scene)  scene_item = obs.obs_scene_find_source(scene, selected_source)  if scene_item then    dx, dy = 10, 0    obs.obs_sceneitem_get_pos( scene_item, pos) -- обновить позицию если источник был перемещён мышкой    pos.x = pos.x + dx    pos.y = pos.y + dy    obs.obs_sceneitem_set_pos(scene_item, pos)   end  obs.obs_scene_release(scene)endfunction script_properties()  local props = obs.obs_properties_create()  obs.obs_properties_add_button(props, "button1", "Вкл/Выкл",on_off)  obs.obs_properties_add_button(props, "button2", "Добавить источник",add_source)  obs.obs_properties_add_button(props, "button3", "Cдвинуть источник на +10,0",move_button)  local p = obs.obs_properties_add_list(props, "selected_source", "Выберите источник", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)  local sources = obs.obs_enum_sources()  if sources ~= nil then    for _, source in ipairs(sources) do      source_id = obs.obs_source_get_unversioned_id(source)      if source_id == "color_source" then        local name = obs.obs_source_get_name(source)        obs.obs_property_list_add_string(p, name, name)      end    end  end  obs.source_list_release(sources)  return propsendfunction script_update(settings)  selected_source = obs.obs_data_get_string(settings,"selected_source")end

Пример горячих клавиш


Скрипт: Создание постоянных и изменяющихся горячих клавиш.


Создание изменяющихся горячих клавиш, в том смысле что их можно поменять в настройках OBS.


hotkeys = {  htk_stop = "Стоп",  htk_start = "Старт",}hk = {}function hotkey_mapping(hotkey)  if hotkey == "htk_stop" then    print('Стоп')  elseif hotkey == "htk_start" then    print('Старт')  endend

Словарь с клавишами и функция типа "switch"


function script_load(settings)  for k, v in pairs(hotkeys) do     hk[k] = obs.obs_hotkey_register_frontend(k, v, function(pressed)      if pressed then         hotkey_mapping(k)      end     end)    a = obs.obs_data_get_array(settings, k)    obs.obs_hotkey_load(hk[k], a)    obs.obs_data_array_release(a)  end  ...function script_save(settings)  for k, v in pairs(hotkeys) do    a = obs.obs_hotkey_save(hk[k])    obs.obs_data_set_array(settings, k, a)    obs.obs_data_array_release(a)  endend

Сохранение/загрузка изменяющихся горячих клавиш.


function htk_1_cb(pressed)   if pressed then    print('1')  endendfunction htk_2_cb(pressed)   if pressed then    print('2 активно')  else    print('2 не активно')  endendkey_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ],'key_2 = '"htk_2": [ { "key": "OBS_KEY_2" } ]}'json_s = key_1 .. key_2default_hotkeys = {  {id='htk_1',des='Кнопка 1 ',callback=htk_1_cb},  {id='htk_2',des='Кнопка 2 ',callback=htk_2_cb},}

Создание постоянных клавиш, их можно поменять в настройках, но при перезапуске OBS они примут значения по умолчанию. В данном случае кнопку 1 и 2.


function script_load(settings)  ...  s = obs.obs_data_create_from_json(json_s)  for _,v in pairs(default_hotkeys) do     a = obs.obs_data_get_array(s,v.id)    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)    obs.obs_hotkey_load(h,a)    obs.obs_data_array_release(a)  end  obs.obs_data_release(s)end

Гифка


Исходный код
local obs = obsluahotkeys = {  htk_stop = "Стоп",  htk_start = "Старт",}hk = {}function hotkey_mapping(hotkey)  if hotkey == "htk_stop" then    print('Стоп')  elseif hotkey == "htk_start" then    print('Старт')  endendfunction htk_1_cb(pressed)   if pressed then    print('1')  endendfunction htk_2_cb(pressed)   if pressed then    print('2 активно')  else    print('2 не активно')  endendkey_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ],'key_2 = '"htk_2": [ { "key": "OBS_KEY_2" } ]}'json_s = key_1 .. key_2default_hotkeys = {  {id='htk_1',des='Кнопка 1 ',callback=htk_1_cb},  {id='htk_2',des='Кнопка 2 ',callback=htk_2_cb},}function script_load(settings)  for k, v in pairs(hotkeys) do     hk[k] = obs.obs_hotkey_register_frontend(k, v, function(pressed)      if pressed then         hotkey_mapping(k)      end     end)    a = obs.obs_data_get_array(settings, k)    obs.obs_hotkey_load(hk[k], a)    obs.obs_data_array_release(a)  end  s = obs.obs_data_create_from_json(json_s)  for _,v in pairs(default_hotkeys) do     a = obs.obs_data_get_array(s,v.id)    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)    obs.obs_hotkey_load(h,a)    obs.obs_data_array_release(a)  end  obs.obs_data_release(s)endfunction script_save(settings)  for k, v in pairs(hotkeys) do    a = obs.obs_hotkey_save(hk[k])    obs.obs_data_set_array(settings, k, a)    obs.obs_data_array_release(a)  endend

Задачи


Задача на движение по кругу:
На основе скрипта движение по линии, создайте скрипт с движением вокруг часовой/против.


Гифка


Задача на использование кнопок клавиатуры:
На основе скрипта с горячими клавишами, создайте скрипт с переключателем вкл/выкл,
доп кнопкой через JSON, доп кнопкой с комбинацией клавиш через JSON.


Гифка


Ссылки


Подробнее..

OBS Studio Lua Скриптинг. Часть 2

06.10.2020 16:11:14 | Автор: admin

Всем привет, в этой части руководства рассмотрим:
фильтры, сцены, предметы сцен, Frontend API, создание функциональных фильтров и прочее
С первой частью можно ознакомиться по этой ссылке.


КДПВ сделана в OBS 26.0.0


Краткая справка для этой части


  • Источник Источники используются для рендера аудио/видео, например: захват камеры, игры, звука. С помощью источников можно создавать фильтры, переходы


  • Фильтр Источник который дополняет другие источники


  • Сцены Коллекция источников, сцена является источником ( сцену можно добавить как источник в другой сцене)


  • Предмет сцены конкретный источник в сцене, его можно: перемещать, увеличивать/уменьшать, переворачивать, менять состояние выкл/вкл и.т.д


  • Frontend API Набор функций который предоставляет OBS Studio, например:


    • подписка на событие о переключении сцен
    • запрос статуса о том, идёт ли стрим/запись
    • вкл/выкл стрим/запись
    • переключение сцен


Фильтры


Типы фильтров так же как и источников, можно узнать через функцию obs_source_get_unversioned_id


Название Внутреннее представление типа
Компрессор compressor_filter
Экспандер expander_filter
Усиление gain_filter
Инвертировать полярность invert_polarity_filter
Лимитер limiter_filter
Пропускной уровень шума noise_gate_filter
Шумоподавление noise_suppress_filter
VST 2.x плагин vst_filter
Задержка видео (асинхронность) async_delay_filter
Хромакей chroma_key_filter
Коррекция цвета color_filter
Цветовой ключ color_key_filter
Кадрирование crop_filter
Маска изображения/Смешивание mask_filter
Яркостный ключ luma_key_filter
Задержка отображения gpu_delay
Масштабирование/Соотношение сторон scale_filter
Прокрутка scroll_filter
Увеличить резкость sharpness_filter

В английском варианте: ссылка


Скрипт: изменение параметра прозрачности у фильтра на случайную величину от 1 до 100.


Чтобы узнать название параметра "прозрачность" необходимо добавить фильтр с прозрачностью на какой-нибудь источник, изменить этот параметр. Далее открыть файл коллекции сцен, путь к директории можно узнать через меню OBS:
Справка > Файлы журнала > Показать файлы журнала
далее с этой директории поднимаемся выше, и получаем путь ~/basic>scenes>название_сцены.json
В этом файле ищем color_filter или color_key_filter (оба фильтра могут изменить прозрачность источника).
В строке settings видим что прозрачность записана как opacity.
Ещё один способ узнать название параметра, прочитать исходный код фильтра ссылка


Находим источник по имени


function add_filter_to_source(random_n)  source = obs.obs_get_source_by_name(source_name)

Создаём настройки с изменением параметра opacity на случайное число


settings = obs.obs_data_create()obs.obs_data_set_int(settings, "opacity",random_n)

Проверяем существует ли уже фильтр на источнике, если нет добавляем


_color_filter = obs.obs_source_get_filter_by_name(source,"opacity_random")if _color_filter == nil then -- if not exists  _color_filter = obs.obs_source_create_private( "color_filter", "opacity_random", settings)  obs.obs_source_filter_add(source, _color_filter)end

Обновляем и освобождаем память


  obs.obs_source_update(_color_filter,settings)  obs.obs_source_release(source)  obs.obs_data_release(settings)  obs.obs_source_release(_color_filter)end

Привязка к горячей клавише


function htk_1_cb(pressed)   if pressed then    n = math.random(1,100)    add_filter_to_source(n)  endend

Гифка


Исходный код
local obs = obsluasource_name = ''function htk_1_cb(pressed)   if pressed then    n = math.random(1,100)    add_filter_to_source(n)  endendfunction add_filter_to_source(random_n)  source = obs.obs_get_source_by_name(source_name)  settings = obs.obs_data_create()  obs.obs_data_set_int(settings, "opacity",random_n)  _color_filter = obs.obs_source_get_filter_by_name(source,"opacity_random")  if _color_filter == nil then -- if not exists    _color_filter = obs.obs_source_create_private( "color_filter", "opacity_random", settings)    obs.obs_source_filter_add(source, _color_filter)  end  obs.obs_source_update(_color_filter,settings)  obs.obs_source_release(source)  obs.obs_data_release(settings)  obs.obs_source_release(_color_filter)endfunction script_properties()  -- source https://raw.githubusercontent.com/insin/obs-bounce/master/bounce.lua  local props = obs.obs_properties_create()  local source = obs.obs_properties_add_list(    props,    'source',    'Source:',    obs.OBS_COMBO_TYPE_EDITABLE,    obs.OBS_COMBO_FORMAT_STRING)  for _, name in ipairs(get_source_names()) do    obs.obs_property_list_add_string(source, name, name)  end  return propsendfunction script_update(settings)  source_name = obs.obs_data_get_string(settings, 'source')end--- get a list of source names, sorted alphabeticallyfunction get_source_names()  local sources = obs.obs_enum_sources()  local source_names = {}  if sources then    for _, source in ipairs(sources) do      -- exclude Desktop Audio and Mic/Aux by their capabilities      local capability_flags = obs.obs_source_get_output_flags(source)      if bit.band(capability_flags, obs.OBS_SOURCE_DO_NOT_SELF_MONITOR) == 0 and        capability_flags ~= bit.bor(obs.OBS_SOURCE_AUDIO, obs.OBS_SOURCE_DO_NOT_DUPLICATE) then        table.insert(source_names, obs.obs_source_get_name(source))      end    end  end  obs.source_list_release(sources)  table.sort(source_names, function(a, b)    return string.lower(a) < string.lower(b)  end)  return source_namesendkey_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'json_s = key_1default_hotkeys = {  {id='htk_1',des='Кнопка 1 ',callback=htk_1_cb},}function script_load(settings)  s = obs.obs_data_create_from_json(json_s)  for _,v in pairs(default_hotkeys) do     a = obs.obs_data_get_array(s,v.id)    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)    obs.obs_hotkey_load(h,a)    obs.obs_data_array_release(a)  end  obs.obs_data_release(s)end

Стоит упомянуть также о функции obs_source_enum_filters с её помощью можно получить
список всех фильтров у конкретного источника, кстати эта функция не работает в obspython,
но об этом чуть позже.


function check()  source = obs.obs_get_source_by_name(source_name)  result = obs.obs_source_enum_filters(source)  for k,v in pairs(result) do     name = obs.obs_source_get_name(v)    print('name'.. name)  end  obs.source_list_release(result)  obs.obs_source_release(source)end

Эвенты и состояние


Скрипт: звуковое оповещение о том что сцена изменена, с использованием .mp3 файла.
На основе этого скрипта


Создадим функцию для проигрывания звука при смене сцен.


function on_event(event)   if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED    then obs_play_sound_release_source()  end end

Добавим источник медиа, установим настройки: файл alert.mp3 относителен директории нахождения
скрипта, obs_source_set_monitoring_type выставляет прослушивание аудио.


function play_sound()  mediaSource = obs.obs_source_create_private("ffmpeg_source", "Global Media Source", nil)  local s = obs.obs_data_create()  obs.obs_data_set_string(s, "local_file",script_path() .. "alert.mp3")  obs.obs_source_update(mediaSource,s)  obs.obs_source_set_monitoring_type(mediaSource,obs.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)  obs.obs_data_release(s)  obs.obs_set_output_source(outputIndex, mediaSource)  return mediaSourceendfunction obs_play_sound_release_source()  r = play_sound()  obs.obs_source_release(r)end

Исходный код
local obs = obsluamediaSource = nil -- Null pointeroutputIndex = 63 -- Last indexfunction play_sound()  mediaSource = obs.obs_source_create_private("ffmpeg_source", "Global Media Source", nil)  local s = obs.obs_data_create()  obs.obs_data_set_string(s, "local_file",script_path() .. "alert.mp3")  obs.obs_source_update(mediaSource,s)  obs.obs_source_set_monitoring_type(mediaSource,obs.OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT)  obs.obs_data_release(s)  obs.obs_set_output_source(outputIndex, mediaSource)  return mediaSourceendfunction obs_play_sound_release_source()  r = play_sound()  obs.obs_source_release(r)endfunction on_event(event)   if event == obs.OBS_FRONTEND_EVENT_SCENE_CHANGED    then obs_play_sound_release_source()  end endfunction script_load(settings)  obs.obs_frontend_add_event_callback(on_event)endfunction script_unload()  obs.obs_set_output_source(outputIndex, nil)end

Время и файлы


Запись в файл, "a" создаст(если нет) файл и добавит "content", а "w" перезапишет .


io.output(io.open(script_path() .. "out.txt","a"))io.write("content")io.close()

print(os.date("%c"))-- День недели Месяц Время Год


Сцены и предметы сцен


  • obs_sceneitem_get_source предметы сцен в источник
  • obs_scene_from_source преобразование сцены в источник
  • obs_scene_find_source преобразование источника в предмет сцены
  • obs_frontend_get_scenes получение всех сцен, освобождать с source_list_release
  • obs_frontend_get_current_scene получение текущей сцены
  • obs_scene_enum_items список всех предметов в сцене, освобождать с sceneitem_list_release

Скрипт: включение и выключение предмета сцены(источника на сцене).


Получение всех сцен и предметов в них


function toggle_source()  scenes = obs.obs_frontend_get_scenes()  for _,scene in pairs(scenes) do    scene_source = obs.obs_scene_from_source(scene)    items = obs.obs_scene_enum_items(scene_source)...

Поиск конкретного источника и его включение или выключение, source_name и boolean определены глобально.


...for _,scene_item in pairs(items) do  _source = obs.obs_sceneitem_get_source(scene_item)  _name = obs.obs_source_get_name(_source)  if _name == source_name then    boolean = not boolean     obs.obs_sceneitem_set_visible(scene_item, boolean)  endend...

Гифка


Исходный код
local obs = obsluasource_name = ''boolean = truefunction htk_1_cb(pressed)   if pressed then    toggle_source()  endendfunction toggle_source()  scenes = obs.obs_frontend_get_scenes()  for _,scene in pairs(scenes) do    scene_source = obs.obs_scene_from_source(scene)    items = obs.obs_scene_enum_items(scene_source)    for _,scene_item in pairs(items) do      _source = obs.obs_sceneitem_get_source(scene_item)      _name = obs.obs_source_get_name(_source)      if _name == source_name then        boolean = not boolean         obs.obs_sceneitem_set_visible(scene_item, boolean)      end    end    obs.sceneitem_list_release(items)  end  obs.source_list_release(scenes)endfunction script_properties()  -- source https://raw.githubusercontent.com/insin/obs-bounce/master/bounce.lua  local props = obs.obs_properties_create()  local source = obs.obs_properties_add_list(    props,    'source',    'Source:',    obs.OBS_COMBO_TYPE_EDITABLE,    obs.OBS_COMBO_FORMAT_STRING)  for _, name in ipairs(get_source_names()) do    obs.obs_property_list_add_string(source, name, name)  end  obs.obs_property_set_long_description(source,"?" )  return propsendfunction script_update(settings)  source_name = obs.obs_data_get_string(settings, 'source')end--- get a list of source names, sorted alphabeticallyfunction get_source_names()  local sources = obs.obs_enum_sources()  local source_names = {}  if sources then    for _, source in ipairs(sources) do      -- exclude Desktop Audio and Mic/Aux by their capabilities      local capability_flags = obs.obs_source_get_output_flags(source)      if bit.band(capability_flags, obs.OBS_SOURCE_DO_NOT_SELF_MONITOR) == 0 and        capability_flags ~= bit.bor(obs.OBS_SOURCE_AUDIO, obs.OBS_SOURCE_DO_NOT_DUPLICATE) then        table.insert(source_names, obs.obs_source_get_name(source))      end    end  end  obs.source_list_release(sources)  table.sort(source_names, function(a, b)    return string.lower(a) < string.lower(b)  end)  return source_namesendkey_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'json_s = key_1default_hotkeys = {  {id='htk_1',des='Кнопка 1 ',callback=htk_1_cb},}function script_load(settings)  s = obs.obs_data_create_from_json(json_s)  for _,v in pairs(default_hotkeys) do     a = obs.obs_data_get_array(s,v.id)    h = obs.obs_hotkey_register_frontend(v.id,v.des,v.callback)    obs.obs_hotkey_load(h,a)    obs.obs_data_array_release(a)  end  obs.obs_data_release(s)end

Регистрация фильтров


В obslua доступна функция obs_register_source, с её помощью можно зарегистрировать
источник( переходы и фильтры это источники). Для этого понадобится создать таблицу источника
Регистрация фильтров удобна тем, что позволяет закрепить функциональность скрипта за
определенным источником. Поддерживает горячие клавиши, интерфейс, таймеры.


Скрипт: закрепление горячих клавиш на фильтре, и полный доступ к источнику.


Импорт библиотеки, и определение типа как источник-фильтр.


local obs = obslualocal bit = require("bit")local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.htmlinfo.id = "uniq_filter_id"info.type = obs.OBS_SOURCE_TYPE_FILTERinfo.output_flags = bit.bor(obs.OBS_SOURCE_VIDEO)info.get_name = function() return 'default filter name' end

Инициализация фильтра, будет вызываться при запуске программы или при добавлении к источнику


info.create = function(settings,source)   local filter = {}  filter.context = source

Начальная регистрация горячих клавиш, принадлежащих конкретному фильтру.


filter.hotkeys = {  htk_stop = "[stop] ",  htk_restart = "[start] ",}filter.hotkey_mapping = function(hotkey,data)  if hotkey == "htk_stop" then    print('stop '.. data.srsn .. " : " .. data.filn)  elseif hotkey == "htk_restart" then    print('restart ' .. data.srsn .. " : " .. data.filn)  endendfilter.hk = {}for k,v in pairs(filter.hotkeys) do   filter.hk[k] = obs.OBS_INVALID_HOTKEY_IDend

Создание функции которая запустится не сразу ( это необходимо т.к фильтр ещё не создан)
Он будет создан после return


filter._reg_htk = function()    info.reg_htk(filter,settings)  end  obs.timer_add(filter._reg_htk,100) -- callback to register hotkeys, one time only

Завершающая регистрация горячих клавиш,obs_filter_get_parent источник который фильтруется
к которому прикреплён фильтр этого типа. Удаление таймера.


info.reg_htk = function(filter,settings) -- register hotkeys after 100 ms since filter was created  local target = obs.obs_filter_get_parent(filter.context)  local srsn = obs.obs_source_get_name(target)   local filn =  obs.obs_source_get_name(filter.context)  local data = {srsn = srsn, filn = filn}   for k, v in pairs(filter.hotkeys) do     filter.hk[k] = obs.obs_hotkey_register_frontend(k, v .. srsn .. " : " .. filn, function(pressed)    if pressed then filter.hotkey_mapping(k,data) end end)    local a = obs.obs_data_get_array(settings, k)    obs.obs_hotkey_load(filter.hk[k], a)    obs.obs_data_array_release(a)  end  obs.remove_current_callback()end

Необходимый код для пропуска фильтрации, без него при добавлении фильтра источник "выключится"


info.video_render = function(filter, effect)   -- called every frame  local target = obs.obs_filter_get_parent(filter.context)  if target ~= nil then    filter.width = obs.obs_source_get_base_width(target)    filter.height = obs.obs_source_get_base_height(target)  end  obs.obs_source_skip_video_filter(filter.context) endinfo.get_width = function(filter)  return filter.widthendinfo.get_height = function(filter)  return filter.heightend

Функция .save вызывается при сохранении настроек, т.е можно переназначить горячие клавиши.
obs.obs_register_source(info) регистрация фильтра, теперь его видно при нажатии ПКМ


info.save = function(filter,settings)  for k, v in pairs(filter.hotkeys) do    local a = obs.obs_hotkey_save(filter.hk[k])    obs.obs_data_set_array(settings, k, a)    obs.obs_data_array_release(a)  endendobs.obs_register_source(info)

info.load также как и script_load, вызывается при запуске программы, но в данном
случае дублирует функциональность и требует перезапуска. .update, .get_properties
функции аналогичные script_update, script_properties.


Гифка


Исходный код
local obs = obslualocal bit = require("bit")local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.htmlinfo.id = "uniq_filter_id"info.type = obs.OBS_SOURCE_TYPE_FILTERinfo.output_flags = bit.bor(obs.OBS_SOURCE_VIDEO)info.get_name = function() return 'default filter name' endinfo.create = function(settings,source)   local filter = {}  filter.context = source  filter.hotkeys = {    htk_stop = "[stop] ",    htk_restart = "[start] ",  }  filter.hotkey_mapping = function(hotkey,data)    if hotkey == "htk_stop" then      print('stop '.. data.srsn .. " : " .. data.filn)    elseif hotkey == "htk_restart" then      print('restart ' .. data.srsn .. " : " .. data.filn)    end  end  filter.hk = {}  for k,v in pairs(filter.hotkeys) do     filter.hk[k] = obs.OBS_INVALID_HOTKEY_ID  end  filter._reg_htk = function()    info.reg_htk(filter,settings)  end  obs.timer_add(filter._reg_htk,100) -- callback to register hotkeys, one time only  return filterendinfo.reg_htk = function(filter,settings) -- register hotkeys after 100 ms since filter was created  local target = obs.obs_filter_get_parent(filter.context)  local srsn = obs.obs_source_get_name(target)   local filn =  obs.obs_source_get_name(filter.context)  local data = {srsn = srsn, filn = filn}   for k, v in pairs(filter.hotkeys) do     filter.hk[k] = obs.obs_hotkey_register_frontend(k, v .. srsn .. " : " .. filn, function(pressed)    if pressed then filter.hotkey_mapping(k,data) end end)    local a = obs.obs_data_get_array(settings, k)    obs.obs_hotkey_load(filter.hk[k], a)    obs.obs_data_array_release(a)  end  obs.remove_current_callback()endinfo.video_render = function(filter, effect)   -- called every frame  local target = obs.obs_filter_get_parent(filter.context)  if target ~= nil then    filter.width = obs.obs_source_get_base_width(target)    filter.height = obs.obs_source_get_base_height(target)  end  obs.obs_source_skip_video_filter(filter.context) endinfo.get_width = function(filter)  return filter.widthendinfo.get_height = function(filter)  return filter.heightend--info.load = function(filter,settings) -- restart required--... same code as in info.reg_htk, but filters will be created from scratch every time--obs restarts, there is no reason to define it here again becuase hotkeys will be duplicated--endinfo.save = function(filter,settings)  for k, v in pairs(filter.hotkeys) do    local a = obs.obs_hotkey_save(filter.hk[k])    obs.obs_data_set_array(settings, k, a)    obs.obs_data_array_release(a)  endendobs.obs_register_source(info)

obspython


В OBS также доступен скриптинг через Python, для Windows только 3.6 версия, для Linux встроенная (т.к в настройках нельзя указать путь),
для MacOS Python не доступен для текущей (26.0.0) версии.
В отличии от Lua тут нельзя регистрировать источники, перебор фильтров не работает,
т.к не написан wrapper на функции с аргументом типа указатель-указатель.
Но в контексте скриптинга имеет место быть т.к:



Задачи


Перед тем как начать делать задачи, рекомендую сделать бэкап коллекции сцен,
с осторожностью использовать script_tick(вызывается раз в каждый кадр)
Проверять утечки памяти в папке logs, последняя строка последнего файла
пример время: Number of memory leaks: 0, если скрипт написан неправильно то
этой строчки там не окажется т.к OBS вылетит с ошибкой при закрытии.


3)[фильтры] "Динамическая прокрутка"
Создать программно или выбрать через интерфейс источник который будет фильтроваться,
к этом источнику добавить(если нет) фильтр Прокрутка (scroll_filter),
добавить интерфейс и/или горячие клавиши которые меняют значение вертикальной скорости
на случайную величину от 0 до 1000 при этом включать/выключать повторение с 50% шансом.


Гифка


4)[эвенты] "Проверка"
При переключении сцен проверять идёт ли запись.
Если нет вывести оповещение ( например через error())


5)[время и файлы] "Пост-продакшен"
Создать скрипт который при нажатии горячей клавиши записывает текущее время,
относительное время от старта записи, добавляет текст "МЕТКА",
а через интерфейс UI кнопку записать текст, и место для набора текста.
изображение


6)[предметы сцены] "Сумма"
Посчитать количество сцен и предметов сцен, записать ответ в названии первой сцены.
Не учитывать группы, т.к перебор предметов груп не работает.
Гифка


7) [фильтры и источники] "Нэйтив скриптинг"
Создать фильтр который будет с интервалом в 2 секунды включать и выключать источник за которым он закреплён.
Гифка


Ответы на задачи и код скриптов включая первую часть на Github


Ссылки


Подробнее..

Разработка hexapod с нуля (часть 9) завершение версии 1.00

07.09.2020 14:15:25 | Автор: admin

Всем привет! 2 года разработки, 9723 строчек кода и киллограмы пластика сделали свое дело разработка гексапода подошла к заключительному этапу в рамках текущей версии. К этому этапу проект полностью перешел на красивые зеленые железки, слегка изменен дизайн и появилась трансляция видео. Так же будет клёвое демо-видео. Все выходные были успешно слиты на его съемки и монтаж, надеюсь понравится.

Этапы разработки:

Часть 1 проектирование
Часть 2 сборка
Часть 3 кинематика
Часть 4 математика траекторий и последовательности
Часть 5 электроника
Часть 6 переход на 3D печать
Часть 7 новый корпус, прикладное ПО и протоколы общения
Часть 8 улучшенная математика передвижения
Часть 9 завершение версии 1.00

Силовая часть


Прошлая плата была собрана из того что было в кейсах с компонентами: LM2596S и no-name дроссели. Нехорошо нужно переделать. На этот раз я решил сделать 6 канальный блок питания по одному каналу на конечность, в качестве DC-DC взял LM2678. Получилась довольно приличная плата:



Нагрузочные тесты показали хорошую эффективность. При нагрузке 4А эффективность преобразования составила 92% при 12В входном и 6.5В выходном напряжении. Один такой канал вытягивает 3 сервопривода без серьезной просадки напряжения (менее 0.2В).

Внутри гексапода плата смотрится просто шикарно никакого колхоза и висящих проводов.



Плата управления


Данная часть получила минимальные изменения. В функционале осталось все так же, были переразведены USARTы для коммуникации с камерой, перемещены транзисторы для управления светодиодами, изменены тип кнопок BOOT и RESET, ну и всё в таким духе.

Была добавлена возможность управления питанием сервоприводов, т.к. текущие приводы при потере импульса продолжают удерживать последнее положение. Тут всё просто вывод микроконтроллера подключен к выводу ENABLE микросхемы LM2678 и в случае ошибки или разряда батареи контроллер сможет выключить питание.

Трансляция видео


О да, теперь гексапод может транслировать видео на телефон, либо другое устройство где есть браузер. Сделано это на базе ESP32-CAM. Я не хотел создавать себе лишних проблем и пришлось прибегнуть к запретной технике Arduino. Да, я просто взял готовый пример с передачей кадров по HTTP, немного его допилил и всё готово.

При получении HTTP GET запроса ESP32 забирает фрейм с камеры, преобразует его в JPEG формат разрешением 640х480 и отсылает чанками по WI-FI на приложение\браузер.

На ESP нет крепежных отверстий, поэтому пришлось сделать для него корпус. Получилась очень компактная камера. К сожалению, фотографии результата нет, а вытаскивать её из корпуса не очень хочется.


Корпус собирается путем запаивания крышки и дна нагретым предметом. Данный узел не планируется когда либо разбирать, всё необходимое было выведено наружу. Прошивать можно без разборки корпуса.

Теперь немного об архитектуре. Гексапод это ходячая точка доступа WI-FI, ESP32 в данном случае настроен в качестве клиента. При подаче питания гексапод поднимает точку доступа WI-FI в течении 30-40 секунд, ESP32 в это время делает попытки подключится ней и в случае успеха передает по USARTу свой IP адрес в STM. В результате мы имеем беспроводную локальную сеть.

Такая архитектура сделана по нескольким причинам:

  • STM32F373 не потянет обработку такого потока данных;
  • Не нужно делать свой протокол передачи изображения. На борту есть HTTP, почему бы его не использовать сразу?;
  • Прямая передача данных на устройство по воздуху, минуя STM и провода;
  • Возможность трансляции видео на любое устройство с браузером, которое подключилось к гексаподу. К примеру, я могу управлять гексподом с телефона и спокойно смотреть его глазами с ноутбука. Мне показалось это очень удобным.

Для просмотра видео из браузера нужно ввести в адресную строку IP адрес камеры, который будет показан в программе управления гексподом:



Долгожданный результат



Планы на будущее


Мы вроде как мир собрались захватывать с помощью него, но он до сих пор заряжается от розетки нужно это исправить. Я планирую немного передохнуть от этого проекта и заняться чем-то более спокойным и простым. Меня заинтересовали солнечные панели с системой слежения за солнцем (солнечный трекер). Данное устройство будет в качестве зарядного устройства для гекспода на улице, да и вообще полезная шутка. Я думаю будет очень интересно, тем более приводов для хорошего редуктора у меня теперь навалом.
Подробнее..

Деконструкция TDD

23.09.2020 20:15:33 | Автор: admin

Здравствуйте, меня зовут Дмитрий Карловский. А вы на канале "Core Dump", где мы берём разные темы из компьютерной науки и деконструируем их по полочкам. Начнём мы с разработки через тестирование.


Test Driven Development

Суть этого подхода заключается в ритуализации процесса разработки. То есть в некритическом безусловном выполнении определённых простых действий.


Этот ритуал сделает ваш код красивым и надёжным. Поддерживать его будет легко и просто. А разработка будет простой и быстрой. Так во всяком случае настоятельно убеждают нас проповедники TDD.


Видео запись этого разбора.


Суть TDD


Вкратце, ритуальный цикл состоит из 3 шагов: сначала пишется красный тест; потом пишется или исправляется код так, чтобы тест позеленел; и наконец код рефакторится, сохраняя зеленоватость тестов.


Pure TDD


И тут сразу возникает вопрос вопрос на миллион...


Что делать, когда тест изначально зелёный?


Варианты ответов...


  • Сломать код
  • Удалить тест
  • Это невозможно

Если сломать код, то тесты естественным образом покраснеют. А после того как мы откатим изменение, тесты снова станут зелёными.


Можно удалить тест. Ведь если нет теста, то нет и проблемы с его изначальным цветом.


Наконец, моё любимое: по TDD такого быть не должно. Где-то ты накосячил, что у тебя так получилось. Покайся, грешник.


А теперь, внимание, правильный ответ: все эти варианты это полная беспросветная чушь. Хотя именно один из них, как правило, слышишь от адептов TDD.


Парадокс воронов


Говоря про ломание кода, нельзя не упомянуть про парадокс воронов. Суть его в том, что задаётся вопрос: "Все ли вороны чёрные?". И для ответа на него берутся нечёрные предметы. Например красные яблоки. И каждое такое яблоко как бы подтверждает тезис о том, что "все вороны чёрные", ведь яблоки не чёрные и при этом не вороны. Что-то тут не так с логикой, не правда ли?



Так же как в парадоксе воронов, адепт TDD нередко думает, что падение теста на явно некорректном коде может хоть что-то сказать о том, будет ли тест падать на коде, который похож на корректный. Однако, тут нет никакой связи. Это просто два разных кода.


Поломка и починка кода это не более чем бессмысленное действие, необходимое только для того, чтобы формально пройти 2 обязательных шага ритуального цикла. Но насколько вероятна эта зелёная ситуация?


Изначально зелёные тесты неизбежны


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


  1. R G
  2. R G
  3. R G
  4. G ?
  5. G ?
  6. G ?
  7. G ?
  8. G ?

Если мы оставим такие тесты, то это уже будет не TDD, ведь мы написали большую часть тестов уже после кода, а не до. Если же мы их удалим, то код останется попросту недотестированным. И на ближайшем рефакторинге мы скорее всего допустим дефект, который не будет обнаружен ввиду отсутствия теста для соответствующего кейса.


То есть, для обеспечения качества мы вынуждены явно нарушать основную идею TDD: сначала тест, потом код.


Правильный TDD


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


Fixed TDD


Первым делом мы должны задаться вопросом "А нужен ли нам ещё один тест?". Если мы ещё не покрыли тестами все тест кейсы, значит нужен. И следующим шагом мы пишем тест. Потом проверяем проходит ли он. И если проходит, то возвращаемся на исходную. Если же не проходит, то это значит, что тест и реализация не соответствуют друг другу и нам надлежит это починить. Починка заключается либо в исправлении кода, либо исправлении теста. Проблема может быть и там, и там.


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


И, наконец, обратите внимание, что даже после рефакторинга мы задаёмся вопросом "А нужен ли ещё один тест?". Но как же так, спросите вы, мы же уже покрыли все тест-кейсы. Причина тут в том, что рефакторинг может приводить к существенному изменению кода. А это в свою очередь может изменить граничные условия, для учёта которых могут потребоваться дополнительные тесты.


В такой форме TDD уже можно применять с пользой. Однако...


TDD приводит к куче лишней работы


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


Давайте рассмотрим типичный сценарий написания простой функции...


Итерация В начале В процесссе В результате
1 R R G
2 GR RR GG
3 GGR RRR GGG
4 GGGR GGRR GGGG
5 GGGGR GGGGR GGGGG
6 GGGGGR RRRRRR GGGGGG

Первые несколько итераций мы выкидываем ранее написанный код полностью. Это приводит к покраснению воообще всех тестов, после чего нам приходится уже писать код так, чтобы он проходил сразу несколько тестов. То есть мы приходим к ситуации, как если бы мы сразу написали несколько тестов, и только после этого взялись писать код. А все предыдущие итерации получается не имели никакого смысла.


Но всё не так плохо, с какого-то момента мы перестанем выбрасывать код полностью, и добавление нового теста уже не будет приводить нас к существенному переписыванию кода. То есть цикл TDD наконец-то начнёт работать как задумывался.


Однако, всё ещё остаётся довольно высокая вероятность в любой момент столкнуться с такой ситуацией, что очередное изменение кода сломает вообще все тесты. Ну или как минимум большую их часть. А значит мы снова возвращаемся к ситуации "все тесты уже написаны, нужно лишь написать правильно код". А все те осторожные шажки по одному тесту, что мы делали ранее, просто обесцениваются.


Когда TDD полезен


Как было отмечено ранее применение TDD в общем случае скорее вредно, чем полезно. Но в некоторых случаях оно может быть оправдано..


  • Исправление дефектов
  • Заранее известный контракт
  • Не заставить себя писать тесты

Если к вам пришёл баг, то вы его можете сперва формализовать в виде теста, а далее исправлять код, пока все тесты не позеленеют.


Кроме того, в ситуации, когда контракт вам известен заранее, вы можете сразу написать все тесты, а потом уже весь код, который им соответствует. Формально, это будет не TDD, так как вы не будете менять код после добавления каждого теста. Однако, это самый что ни на есть Test Driven, так как тесты будут диктовать вам реализацию.


Наконец, TDD может помочь вам с самодисциплиной и заставить-таки писать тесты. Но если у вас проблема с самодисциплиной, то возможно и заставить применять TDD вам будет не очень просто. Но тут вера может творить чудеса.


Программировать ли по TDD?


Если кто-то вам скажет, что он "программирует по TDD", то можете быть уверены, что он попросту не ведает, что творит. У TDD есть ряд фундаментальных проблем, поэтому его применение оправдано лишь в весьма ограниченном числе случаев. И то, не в той форме в которой ему как правило учат многочисленные коучи.


- Ритуализация :-(- Явно некорректный код :-(- Бесполезная работа :-(- Там, где это уместно :-)- Не зацикливаться :-)

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


Что ещё посмотреть по TDD?


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



Продолжение следует..


  • Лайк
  • Подписка
  • Комментарий
  • Поделись-ка

Если данный разбор показался вам полезным, то дайте мне об этом знать посредством лайка. А так же поделитесь ссылкой на него со своими коллегами.


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


Если вы не боитесь подискутировать со мной в прямом эфире или даже готовы стать соавтором будущих разборов, то пишите телеграмы.


Наконец, подписывайтесь на канал, чтобы не пропустить дальнейшие разборы. Нам ещё много чего с вами нужно будет обсудить.


На этом пока что всё. С вами был боевой программер Дмитрий Карловский.

Подробнее..

Зачем мы создали свою собственную систему видеосвязи с блэкджеком и фичами

20.01.2021 18:14:16 | Автор: admin

2020 оказался годом удаленки. В марте мы всей командой были вынуждены перейти на полностью дистанционный формат работы и все процессы пришлось настраивать заново.

Отдельной болью для нас стали видеозвонки. И началось: Ой, а давайте в Скайпе, Дискорде, Телеграме, Зуме. А потом то девайсы программное обеспечение криво поддерживают, то технические сбои, то аккаунты вне доступа, то обновление софта и еще вагон проблем. Уходила куча времени, чтобы просто связаться и провести совещание.

В общем, уже через месяц подобных экзекуций мы психанули и стали разрабатывать свою собственную систему видеосвязи на базе CRM Мегаплан. Сразу многофункциональную, чтобы запустить ее и для клиентов. С какими трудностями столкнулись, как решали задачу и что из этого получилось читайте в статье.

Чем нас не устроил Zoom и другие приложения

Буквально за несколько месяцев после начала карантина количество пользователей Zoom увеличилось в 30 раз. В декабре 2019 года ежедневно сервисом пользовались 10 млн людей, а в апреле 2020 уже 300 млн.

Поначалу мы тоже использовали Zoom для общих видеоконференций. Были небольшие проблемы со стабильностью сервиса сервера не выдерживали нагрузок и иногда звонки жутко тормозили неприятно, но ничего критичного. Во всяком случае, мы так думали.

А потом мы узнали о массивной утечке данных. В апреле 2020 хакеры взломали базы данных Zoom и в сеть утекли данные свыше 500 000 аккаунтов. Логины, пароли, email, URL личных чатов, коды администраторов для управления конференциями.

Более того, уязвимости Zoom можно было использовать для получения удаленного доступа к компьютеру и защищенным файлам. Топовый сервис для видеоконференций оказался не просто ненадежным, а даже опасным. Вопрос безопасности встал очень остро. С зумбомбингом мы хоть не встречались, но были наслышаны и о нем.

Насчет удобства использования сторонних сервисов, все также было не очень гладко. При обсуждении рабочих вопросов один на один или малыми группами чаще всего использовали Skype или Facebook Messenger. Для встреч по отделам и общих конференций Zoom. И самым проблемным оказалось отсутствие единой базы записей видеозвонков. Попытка пересмотреть обсуждение какого-нибудь вопроса превращалась в квест.

Со стабильностью все было тоже не очень гладко. В марте у нас была ситуация, когда из-за обновления софта три участника важного видеозвонка просто не смогли подключиться. Танцы с бубном не помогали фактического контроля над внешними зарубежными приложениями нет никакого и нет никакой возможности понять, что не так. В результате через полчаса пришлось переносить встречу на другой день. Время и нервы потрачены, результат нулевой.

Да и в целом организационные моменты подталкивали на разработку своего софта для видеозвонков. С той лишь разницей, что мы видели его не как отдельное решение, а встроенный в нашу CRM видеочат для обсуждения рабочих задач и проектов, уже заведенных в систему. Тем более, что идеи уже были. Нам нужно было добиться стабильной работы на всех ОС и мобильных платформах.

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

В общем, в апреле 2020 года мы серьезно нацелились на разработку многофункциональной платформы видеосвязи, которая покроет не только наши хотелки, но и решит проблемы наших клиентов.

Разработка как прогулка по полю с граблями

Впервые идея создать собственную платформу видеосвязи появилась у нас в 2015 году.

Мы даже выделили немного времени и разработали тестовую версию сервиса видеосвязи на Flash. Прописали буквально только базовый функционал, так что даже до демо-версии она не дотягивала.

В результате аналитики решили, что допиливать софт до релиза нецелесообразно. В разработку нужно было огромное количество денег, а рентабельность идеи была очень сомнительной. Мы не собирались превращаться в конкурента Skype, а в качестве дополнительной функции CRM вложения в видеосвязь были неоправданно высокими.

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

И хорошо, что мы тогда не стали искать выходы. Было бы печально вложить кучу денег во второстепенную функцию, а через 5 лет узнать, что все нужно делать заново, потому что Flash отключают.

***

Весной 2020 года мы вернулись к идее собственной платформы видеосвязи. Потому что проблемы с организацией рабочего процесса, с которым столкнулись мы не были уникальными. У многих наших клиентов были такие же, а у нас были технические и профессиональные возможности их решить.

Разработка первоначальной версии велась буквально на коленке. Сначала выбрали OpenVidu, который нас максимально устраивал и был доступен в плане лицензии. А затем буквально в течение нескольких недель собрали MVP и попытались интегрировать его в CRM Мегаплана.

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

  1. Продукт получился очень требователен к мощности сервера. Если для внутреннего использования это еще не критично, то для релиза нужны были отдельные серверы для видеосвязи. Коммерческая лицензия позволяла сделать продукт масштабируемым, но тогда все упиралось в стоимость серверов оплата была не за использование фактических возможностей, а за время аренды серверов. Даже если сервер простаивает, за него нужно платить.

  2. Вылез целый ряд багов и проблем, решение которых мы в процессе первичной разработки не нашли. Среди них:

    a. Отсутствие записи трансляции. Наши специалисты так и не смогли придумать, как реализовать запись без слишком больших переделок или без использования сторонних утилит;

    b. У технологии не было мультиплексирования, поэтому использование видеосвязи слишко быстро сажало аккумуляторы смартфонов. Для ее решения нужны были специализированные хардварные решения, к которым мы были не готовы.

    c. В мобильной версии в целом было много багов, для фикса которых нужно было много времени. Самым проблемным оказалось то, что приложение во время звонка вылезает на передний план и свернуть его никак нельзя. Работать со смартфоном во время конференции становилось попросту невозможно.

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

  4. Стоимость использования OpenVidu считается за одно серверное ядро в минуту. Для небольших решений она приемлема, но это крайне затрудняет масштабирование сервиса все упирается в деньги, много денег.

Количество минусов перекрывало все перспективы использования технологии. Поэтому мы решили от нее отказаться.

Janus Gateway: именно то, что нужно

Для передачи видеоданных мы используем стандартный протокол WebRTC. Стабильная платформа для видеосвязи, на которой можно реализовать все хотели и свистелки, которые нам нужны.

Но обычного p2p соединения нам недостаточно, ведь нужно было реализовать возможность записи трансляции на сервер. Поэтому решили использовать ретранслятор Janus Gateway.

В целом выбирали между тремя платформами: OpenVidu, которую оставили как контрольный образец, Jitsi и Janus. В результате чтения аналитики выяснили, что OpenVidu сильно проседает по качеству картинки трансляции (как будто и так вопросов было мало), а у Jitsi во время нагрузки качество плавает. Не то, чтобы мы планировали делать конференции из 200-300 пользователей, но стабильность работы видеосвязи для нас ключевой момент.

Janus же наиболее стабилен. Он выдает стабильный битрейт, рационально используя ресурсы процессора. В целом Jitsi и Janus показывают хорошие результаты, но у Jitsi есть порог производительности, выше которого он просто падает. Как поведет себя система с кучей дополнительных фишек и на полной нагрузке, было неясно.

У Janus есть куча полезных фич, которые не реализованы или платные в OpenVidu. К примеру, контроль качества связи и автоматическое выставление битрейта в зависимости от него. Или же поддержка кодеков VP9 и режима симулькаста.

Janus имеет ничтожное влияние на процессор и память сервера. Всё упирается только в пропускную способность сети. Поэтому можно брать не самые мощные сервера, а много маленьких и пару больших для декодирования и склейки записанных видеозвонков. Мы не стали прибегать к сторонним решениям, вроде Janus cloud, где узким местом и точкой входа всё равно является janus-proxy, а сделали балансировку на уровне продукта.

Михаил Пирогов, руководитель отдела эксплуатации и администрирования Мегаплана

С Janus все также получилось не слишком гладко. Для реализации WebRTC используется библиотека React Native WebRTC. У нас нет разработчиков, которые хорошо знают React Native, поэтому чтобы устранить проблемы совместимости протокола с софтом сервера, ушло довольно много времени.

Были сложности и со сборкой. Компилятор Metro bundler хорошо работает в веб-версии, но не подтягивает дополнительные библиотеки в мобильной, из-за чего мобильная версия была нестабильной и падала.

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

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

Вообще с реализацией мобильных версий на iOS и Android было неожиданно много проблем. Разница на платформах между управлением динамиками и микрофоном, разные принципы переключения на громкую связь и наушники. Пришлось создавать универсальный код, который одинаково работал бы на обеих ОС. Им тоже можем поделиться, если что.

Самой главной головной болью для нас стала проблема пятого человека. Пять человек стабильно могли участвовать в конференции, но как только присоединялся шестой одного выбрасывало из трансляции. С ней тоже удалось справиться.

Бета-версия и что дальше

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

Сохранять записи звонков. Эту фичу мы планировали, но она пока в стадии реализации и тестирования.

Нужно предоставить возможность скачать запись только участнику с наибольшими правами в конференции, чтобы она не ушла на сторону. Проигрывание записи через встроенный плеер тоже будет актуальным, чтобы клиент не использовал сторонний софт.

Скорее всего, придется ограничить время существования записи. Ведь в противном случае нам придется закупать отдельные серверы для хранения видеоконтента. Но доступ к записи на протяжении 24 часов убирает проблему через сутки запись трансляции автоматически удаляется с сервера.

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

Правда, с вебинарной комнатой у нас немного затык. Нужно добавить инструмент работы со слайдами без демонстрации экрана и вайтборд для рисования маркерами.

Если у вас есть идеи, как это сделать в рамках Janus с минимальным количеством вытекающих проблем с радостью послушаем.

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

В целом разработка видеосвязи получилась неожиданной. Из инструмента, предназначенного для внутреннего использования, она стала частью нашей CRM и уже крепко вошла в инфраструктуру сервиса.

Да, еще остается целая куча проблем с совместимостью, но результат уже рабочий и наши клиенты им активно пользуются. Если будет интересно, мы запустим серию постов про конкретные проблемы с разработкой видеосвязи и путями их решения. Пишите в комменты.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru