Skip to content

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

@upgradeQ

Description

@upgradeQ

Всем привет,в этой части руководства рассмотрим :
фильтры, сцены, предметы сцен, 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)
  end
end

Гифка

Исходный код
local obs = obslua
source_name = ''

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

function 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)
end

function 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 props
end

function script_update(settings)
  source_name = obs.obs_data_get_string(settings, 'source')
end


--- get a list of source names, sorted alphabetically
function 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_names
end


key_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'
json_s = key_1
default_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 mediaSource
end

function obs_play_sound_release_source()
  r = play_sound()
  obs.obs_source_release(r)
end
Исходный код
local obs = obslua
mediaSource = nil -- Null pointer
outputIndex = 63 -- Last index

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 mediaSource
end

function obs_play_sound_release_source()
  r = play_sound()
  obs.obs_source_release(r)
end

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

function script_load(settings)
  obs.obs_frontend_add_event_callback(on_event)
end

function 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)
  end
end
...

Гифка

Исходный код
local obs = obslua
source_name = ''
boolean = true

function htk_1_cb(pressed) 
  if pressed then
    toggle_source()
  end
end

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)

    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)
end

function 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 props
end

function script_update(settings)
  source_name = obs.obs_data_get_string(settings, 'source')
end


--- get a list of source names, sorted alphabetically
function 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_names
end


key_1 = '{"htk_1": [ { "key": "OBS_KEY_1" } ]}'
json_s = key_1
default_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 = obslua
local bit = require("bit")

local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.html
info.id = "uniq_filter_id"
info.type = obs.OBS_SOURCE_TYPE_FILTER
info.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)
  end
end

filter.hk = {}
for k,v in pairs(filter.hotkeys) do 
  filter.hk[k] = obs.OBS_INVALID_HOTKEY_ID
end

Создание функции которая запустится не сразу ( это необходимо т.к фильтр ещё не создан)
Он будет создан после 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) 
end

info.get_width = function(filter)
  return filter.width
end

info.get_height = function(filter)
  return filter.height
end

Функция .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)
  end
end
obs.obs_register_source(info)

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

Гифка

Исходный код
local obs = obslua
local bit = require("bit")

local info = {} -- obs_source_info https://obsproject.com/docs/reference-sources.html
info.id = "uniq_filter_id"
info.type = obs.OBS_SOURCE_TYPE_FILTER
info.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)
    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 filter
end

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) 
end

info.get_width = function(filter)
  return filter.width
end

info.get_height = function(filter)
  return filter.height
end

--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
--end

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)
  end
end

obs.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)[предметы сцены] "Сумма"
Посчитать количество сцен и предметов сцен, записать ответ в названии первой сцены.
Не учитывать группы, т.к перебор предметов груп не работает.
Гифка

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

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

Ссылки

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions