Esercitazione sullo scripting Fonte Shake

Questa pagina propone un tutorial per lo scripting Python e Lua. Come esempio educativo, vogliamo creare uno script che possa "scuotere una fonte" quando l'utente preme un tasto di scelta rapida. In altre parole, per una data sorgente già presente in OBS e visualizzata sulla scena corrente, vogliamo ruotarla continuamente avanti e indietro secondo una data frequenza e ampiezza angolare, in modo tale che la sorgente si muova, quando l'utente preme continuamente un tasto .

A seconda di come sei arrivato a questa pagina, è consigliabile ora dare un'occhiata alla documentazione dello scripting OBS e all'introduzione allo scripting sul Wiki se non è già stato fatto.

Se non puoi aspettare e vuoi vedere come si comporta l'effetto Source Shake, gli script completi sono disponibili in Python e Lua.

Andiamo a fare un giro!

Ciao mondo

Utilizzare un editor di testo e creare un file denominato source-shake.pyo source-shake.lua(nelle prossime sezioni verranno fornite sia le versioni Python che Lua del codice). Per ora scrivi solo la seguente riga nel file, che funziona per Python e Lua:

print("Hello World!")

Aggiungi il file nella finestra di dialogo Script , apri il registro degli script, dovresti vedere qualcosa del genere:

Ciao mondo!

Si prega di notare che printfa il lavoro ma non è la scelta migliore per la registrazione, prendere in considerazione l'utilizzo blog.

Se ha funzionato, puoi rimuovere la riga Hello World dal tuo file.

Importazione di obspython o obslua

Nella parte superiore dello script, inserisci alcune righe come segue:

  • In uno script Python, è obbligatorio importare il obspythonmodulo perché l'interprete non è incorporato. Per convenzione, viene importato come obs(e importiamo già alcuni moduli Python aggiuntivi):
import obspython as obs
import math, time
  • In Lua il obsluamodulo è preimportato, così come i moduli standard disponibili. Di nuovo per convenzione, poiché è più breve e alcuni frammenti di codice sono quindi simili in Lua e Python, una variabile globale chiamata di obssolito fa riferimento al modulo:
obs = obslua

Descrizione dello script

Come passaggio successivo, creare la funzione chiamata script_descriptionsenza argomenti, che dovrebbe restituire una stringa di caratteri contenente la descrizione dello script.

In Python:

<a id='description-displayed-in-the-scripts-dialog-window' class='anchor' aria-hidden='true'></a>
# Description displayed in the Scripts dialog window
def script_description():
  return """Source Shake!!
            Shake a source in the current scene when a hotkey is pressed. Go to Settings
             then Hotkeys to select the key combination.Check the 
            Source Shake Scripting Tutorial on the OBS Wiki for more information."""

A Lua:

-- Description displayed in the Scripts dialog window
function script_description()
  print("in script_description")
  return [[Source Shake!!
           Shake a source in the current scene when a hotkey is pressed. Go to Settings
            then Hotkeys to select the key combination.Check the 
           Source Shake Scripting Tutorial on the OBS Wiki for more information.]]
end

Dopo aver ricaricato lo script, ora dovrebbe essere visualizzata la descrizione:

Descrizione

Si noti che la stringa di descrizione viene visualizzata da Qt, il che significa che un sottoinsieme di HTML può essere utilizzato per la formattazione. Un URI dati autonomo di un file PNG o BMP può essere utilizzato anche per mostrare un'immagine nella descrizione.

Configurazione della sorgente

A questo punto, imposta una sorgente in OBS (un'immagine, la tua webcam, qualunque cosa) e dagli un nome. Per questo tutorial, chiamiamo la sorgente "Spaceship" in modo che sia facilmente identificabile (adattare il nome come conveniente).

Poiché ruoteremo la sorgente, è meglio averla centrata. Non è assolutamente necessario, ma l'effetto shake sembra migliore se la sorgente ruota attorno al suo centro piuttosto che attorno a uno dei suoi angoli.

Per farlo: seleziona la sorgente, fai clic destro su di essa, Trasforma > Modifica trasformazione .. , per Allineamento posizionale seleziona Centro , quindi Chiudi (la modifica dell'allineamento cambierà la posizione della sorgente):

Configurazione della sorgente

Si noti che la "trasformazione" di una sorgente dipende dalla sua posizione effettiva in una data scena. La stessa sorgente in un'altra scena avrebbe un'altra trasformazione collegata al suo "elemento scena" (ne parleremo nella sezione successiva). Un parametro è la "Rotazione" che verrà modificata dallo script.

Trovare le funzioni API OBS rilevanti

La base è a posto (torneremo alle proprietà e ai tasti di scelta rapida più avanti) e vogliamo scoprire come ruotare la sorgente. Durante la scrittura di codice per OBS, un'attività molto dispendiosa in termini di tempo è sfogliare l'enorme API OBS per determinare quali funzioni o combinazioni di funzioni sono necessarie per raggiungere i dati OBS.

Una rapida ricerca di "rotazione" nell'API OBS evidenzia una funzione promettente tra molti risultati: obs_sceneitem_set_rotimposta l'angolo di rotazione di un "elemento scena" ( obs_sceneitem_get_rotper recuperare l'angolo corrente). Quindi in realtà non possiamo ruotare direttamente la sorgente ma solo la sua incarnazione in una scena, rappresentata da una struttura C con tipo obs_sceneitem_t.

Nella stessa pagina dell'API OBS, possiamo trovare la funzione obs_find_source, o ancora meglio anche la funzione obs_find_source_recursiveper scansionare i gruppi, che restituiscono sia un puntatore a un obs_sceneitem_toggetto da una scena che un nome sorgente.

Ora come trovare la scena attuale? Ancora una volta, una ricerca per "scena" fornisce la funzione obs_frontend_get_current_scenetra i primi colpi. Ma la funzione restituisce una fonte (tutto sembra essere una fonte in OBS). La funzione obs_scene_from_source, anche tra i risultati della ricerca, effettua la conversione.

: avviso: obs_frontend_get_current_scenerestituisce un " nuovo riferimento alla scena attualmente attiva". Come per alcune altre funzioni, il nuovo riferimento significa che i dati restituiti dalla funzione devono essere rilasciati esplicitamente, qui con obs_source_release. OBS andrà in crash se la fonte non viene rilasciata (a partire da OBS 26.1).

Scuotilo

Ora sappiamo come trovare un elemento di scena corrispondente a una sorgente nella scena corrente, come cambiare il suo angolo di rotazione tramite diverse funzioni OBS, e dalla documentazione delle "Esportazioni di funzioni script" sappiamo che script_tickviene chiamato ogni fotogramma.

È ora di scrivere una funzione di animazione veloce e sporca di "Spaceship", in Python:

def script_tick(seconds):
  current_scene_as_source = obs.obs_frontend_get_current_scene()
  if current_scene_as_source:
    current_scene = obs.obs_scene_from_source(current_scene_as_source)
    scene_item = obs.obs_scene_find_source_recursive(current_scene, "Spaceship")
    if scene_item:
      obs.obs_sceneitem_set_rot(scene_item, 10*math.sin(12*time.time()))
    obs.obs_source_release(current_scene_as_source)

A Lua:

function script_tick(seconds)
  local current_scene_as_source = obs.obs_frontend_get_current_scene()
  if current_scene_as_source then
    local current_scene = obs.obs_scene_from_source(current_scene_as_source)
    local scene_item = obs.obs_scene_find_source_recursive(current_scene, "Spaceship")
    if scene_item then
      obs.obs_sceneitem_set_rot(scene_item, 10*math.sin(12*os.clock()))
    end
    obs.obs_source_release(current_scene_as_source)
  end
end

L'animazione dipende dal tempo in secondi, implementato nella linea obs.obs_sceneitem_set_rot(scene_item, 10*math.sin(12*time.time()))in Python, dove 10è l'ampiezza delle oscillazioni (± 10 gradi) e 12un fattore di frequenza (per 12 / 2π oscillazioni al secondo).

Aggiungi la funzione sopra, ricarica lo script e l'animazione dovrebbe iniziare:

Effetto shake

Sembra buono!! Controlla come la finestra Modifica trasformazione .. mostra bene i cambiamenti dell'angolo di rotazione.

Alcuni ri-factoring

La funzione definita nella sezione precedente funziona bene, è robusta contro alcune modifiche del sorgente (spostamento, aggiunta in un'altra scena, ecc.) Ma è tutt'altro che perfetta:

  • L'elemento scena oscilla intorno all'angolo 0, ignorando qualsiasi rotazione preesistente impostata dall'utente
  • Non c'è modo di ripristinare la sorgente nella sua posizione iniziale dopo l'animazione, in modo tale che in uscita OBS salvi la sorgente in una posizione storta

Quindi, prima di tutto, è necessario memorizzare l'angolo di rotazione iniziale dell'oggetto scena prima di toccarlo, in modo da ripristinare tutto quando necessario (notare che l'angolo verrà utilizzato anche per le oscillazioni nell'animazione). Viene mantenuto anche un riferimento all'elemento di scena modificato.

Definiamo le due nuove funzioni save_sceneitem_for_shakee restore_sceneitem_after_shakepiù le relative variabili globali, in Python:

<a id='global-variables-to-restore-the-scene-item-after-shake' class='anchor' aria-hidden='true'></a>
# Global variables to restore the scene item after shake
shaken_sceneitem = None     # Reference to the modified scene item
shaken_sceneitem_angle = 0  # Initial rotation angle, used as well for oscillations

<a id='saves-the-original-rotation-angle-of-the-given-sceneitem-assumed-not-none' class='anchor' aria-hidden='true'></a>
# Saves the original rotation angle of the given sceneitem (assumed not None)
def save_sceneitem_for_shake(sceneitem):
  global shaken_sceneitem, shaken_sceneitem_angle
  shaken_sceneitem = sceneitem
  shaken_sceneitem_angle = obs.obs_sceneitem_get_rot(sceneitem)

<a id='restores-the-original-rotation-angle-on-the-scene-item-after-shake' class='anchor' aria-hidden='true'></a>
# Restores the original rotation angle on the scene item after shake
def restore_sceneitem_after_shake():
  global shaken_sceneitem, shaken_sceneitem_angle
  if shaken_sceneitem:
    obs.obs_sceneitem_set_rot(shaken_sceneitem, shaken_sceneitem_angle)
    shaken_sceneitem = None

A Lua:

-- Global variables to restore the scene item after shake
shaken_sceneitem = nil     -- Reference to the modified scene item
shaken_sceneitem_angle = 0 -- Initial rotation angle, used as well for oscillations

-- Saves the original rotation angle of the given sceneitem (assumed not nil)
function save_sceneitem_for_shake(sceneitem)
  shaken_sceneitem = sceneitem
  shaken_sceneitem_angle = obs.obs_sceneitem_get_rot(sceneitem)
end

-- Restores the original rotation angle on the scene item after shake
function restore_sceneitem_after_shake()
  if shaken_sceneitem then
    obs.obs_sceneitem_set_rot(shaken_sceneitem, shaken_sceneitem_angle)
    shaken_sceneitem = nil
  end
end

Quindi definiamo una funzione per incapsulare la ricerca dell'elemento della scena, principalmente per la chiarezza del codice. Questa funzione è autonoma, quindi può essere riutilizzata così com'è (nessuna variabile globale coinvolta), in Python:

<a id='retrieves-the-scene-item-of-the-given-source-name-in-the-current-scene-or-none-if-not-found' class='anchor' aria-hidden='true'></a>
# Retrieves the scene item of the given source name in the current scene or None if not found
def get_sceneitem_from_source_name_in_current_scene(name):
  result_sceneitem = None
  current_scene_as_source = obs.obs_frontend_get_current_scene()
  if current_scene_as_source:
    current_scene = obs.obs_scene_from_source(current_scene_as_source)
    result_sceneitem = obs.obs_scene_find_source_recursive(current_scene, name)
    obs.obs_source_release(current_scene_as_source)
  return result_sceneitem

A Lua:

-- Retrieves the scene item of the given source name in the current scene or nil if not found
function get_sceneitem_from_source_name_in_current_scene(name)
  local result_sceneitem = nil
  local current_scene_as_source = obs.obs_frontend_get_current_scene()
  if current_scene_as_source then
    local current_scene = obs.obs_scene_from_source(current_scene_as_source)
    result_sceneitem = obs.obs_scene_find_source_recursive(current_scene, name)
    obs.obs_source_release(current_scene_as_source)
  end
  return result_sceneitem
end

Infine definiamo la funzione principale shake_source, da richiamare script_tick, che riunisce tutto e implementa l'animazione. Cerchiamo l'elemento scena in ogni fotogramma per seguire meglio i cambiamenti innescati dall'utente. La logica consiste nell'animare l'elemento scena se può essere trovato nella scena corrente, altrimenti ripristinare qualsiasi elemento scena precedentemente modificato. Questo è un approccio piuttosto ingenuo perché presuppone che il ripristino dell'angolo iniziale sia sempre possibile qualunque cosa sia accaduta all'elemento scena nel mezzo (ne parleremo nelle sezioni successive).

Per semplicità in questo tutorial, le variabili globali vengono utilizzate direttamente dalle funzioni (una versione più riutilizzabile di queste funzioni dovrebbe passare tutto come argomenti). Ogni script viene eseguito nel proprio contesto, quindi non è prevista alcuna interferenza con altri script. Le variabili globali contengono valori che verranno definiti come proprietà modificabili in una sezione successiva:, source_nameoscillation frequencye oscillation amplitude.

In Python, inclusa la chiamata in script_tick:

<a id='global-variables-holding-the-values-of-data-settings--properties' class='anchor' aria-hidden='true'></a>
# Global variables holding the values of data settings / properties
source_name = "Spaceship"  # Name of the source to shake
frequency = 2              # Frequency of oscillations in Hertz
amplitude = 10             # Angular amplitude of oscillations in degrees

<a id='animates-the-scene-item-corresponding-to-sourcename-in-the-current-scene' class='anchor' aria-hidden='true'></a>
# Animates the scene item corresponding to source_name in the current scene
def shake_source():
  sceneitem = get_sceneitem_from_source_name_in_current_scene(source_name)
  if sceneitem:
    id = obs.obs_sceneitem_get_id(sceneitem)
    if shaken_sceneitem and obs.obs_sceneitem_get_id(shaken_sceneitem) != id:
      restore_sceneitem_after_shake()
    if not shaken_sceneitem:
      save_sceneitem_for_shake(sceneitem)
    angle = shaken_sceneitem_angle + amplitude*math.sin(time.time()*frequency*2*math.pi)
    obs.obs_sceneitem_set_rot(sceneitem, angle)
  else:
    restore_sceneitem_after_shake()

<a id='called-every-frame' class='anchor' aria-hidden='true'></a>
# Called every frame
def script_tick(seconds):
  shake_source()

A Lua:

-- Animates the scene item corresponding to source_name in the current scene
function shake_source()
  local sceneitem = get_sceneitem_from_source_name_in_current_scene(source_name)
  if sceneitem then
    local id = obs.obs_sceneitem_get_id(sceneitem)
    if shaken_sceneitem and obs.obs_sceneitem_get_id(shaken_sceneitem) ~= id then
      restore_sceneitem_after_shake()
    end
    if not shaken_sceneitem then
      save_sceneitem_for_shake(sceneitem)
    end
    local angle = shaken_sceneitem_angle + amplitude*math.sin(os.clock()*frequency*2*math.pi)
    obs.obs_sceneitem_set_rot(sceneitem, angle)
  else
    restore_sceneitem_after_shake()
  end
end

-- Called every frame
function script_tick(seconds)
  shake_source()
end

Adatta il codice, ricarica lo script e dovresti avere lo stesso comportamento della sezione precedente. Si noti che possiamo chiamare restore_sceneitem_after_shakein qualsiasi momento (a condizione che l'elemento della scena scossa sia ancora presente), la chiamata successiva a shake_sourcereinizializzerà tutto in modo trasparente e farà avanzare l'animazione.

Gestione del ricaricamento dello script

La funzione restore_sceneitem_after_shakedefinita nella sezione precedente sarà utile per gestire i vari eventi che si verificano in OBS, ripristinando l'angolo originale al momento giusto. Il passaggio da una scena all'altra o la ridenominazione della sorgente funziona correttamente, l'angolazione originale viene ripristinata correttamente. Ma se provi a ricaricare lo script, vedrai che l'orientamento della sorgente animata è perso e diverge quasi in modo casuale a ogni nuovo caricamento dello script.

Questo punto è facile da risolvere, ripristiniamo solo l'angolo in script_unload, parte delle funzioni di script globali .

In Python:

<a id='called-at-script-unload' class='anchor' aria-hidden='true'></a>
# Called at script unload
def script_unload():
  restore_sceneitem_after_shake()

A Lua:

-- Called at script unload
function script_unload()
  restore_sceneitem_after_shake()
end

Una volta che la funzione è a posto, ricaricare lo script è solo trasparente, nessun effetto visibile sull'animazione shake. A questo punto potresti voler modificare la trasformazione delle sorgenti e ripristinare manualmente l'angolo di rotazione iniziale. Basta rinominare temporaneamente la sorgente per poter modificare l'angolo se lo script è ancora attivo.

Gestione dell'uscita da OBS

Anche con il ripristino aggiunto script_unload, lo stesso tipo di problema rimane quando OBS viene chiuso: al successivo avvio di OBS l'orientamento della sorgente è quasi casuale. Questo evento è un po 'più difficile da gestire perché script_unloade script_save(chiamati solo all'uscita da OBS) vengono chiamati dopo che i dati della scena sono stati salvati, quindi il ripristino dell'angolo in queste funzioni è troppo tardi.

Una possibile soluzione è forzare nuovamente il salvataggio dei sorgenti in script_saveuso obs_save_sources, in Python:

<a id='called-before-data-settings-are-saved' class='anchor' aria-hidden='true'></a>
# Called before data settings are saved
def script_save(settings):
  restore_sceneitem_after_shake()
  obs.obs_save_sources()

A Lua:

-- Called before data settings are saved
function script_save(settings)
  restore_sceneitem_after_shake()
  obs.obs_save_sources()
end

Gestione dell'eliminazione degli elementi della scena

Ora abbiamo finito con il salvataggio e il ripristino dell'angolo, tutti gli eventi dovrebbero essere coperti, ma c'è ancora una cosa piuttosto sgradevole da gestire: se l'elemento scena viene eliminato dall'utente, il relativo oggetto C viene liberato e tenta di ripristinare il suo angolo con restore_sceneitem_after_shakepuò portare a un crash di OBS! Il problema potrebbe non essere affatto visibile, a seconda dell'effettiva gestione della memoria e di altri oggetti allocati.

Fortunatamente, OBS fornisce una serie sofisticata di segnali di scena per gestire tali eventi, uno di questi è item_remove. Non è ben documentato, ma il richiamo di questo segnale è chiamato prima che l'oggetto viene definitivamente distrutta, come di OBS 26.1 (necessità di aggiungere la registrazione in obs_sceneitem_destroyin obs-scene.ce ricompilazione OBS veramente controllare questo comportamento ..).

Iniziamo definendo la funzione di callback che chiamerà restore_sceneitem_after_shake. Il parametro calldatanon viene utilizzato, in Python:

<a id='callback-for-itemremove-signal' class='anchor' aria-hidden='true'></a>
# Callback for item_remove signal
def on_shaken_sceneitem_removed(calldata):
  restore_sceneitem_after_shake()

A Lua:

-- Callback for item_remove signal
function on_shaken_sceneitem_removed(calldata)
  restore_sceneitem_after_shake()
end

Useremo un gestore di segnale per connettere il segnale e manterremo un riferimento ad esso come variabile globale per disconnettere il segnale, in Python:

shaken_scene_handler = None # Signal handler of the scene kept to restore

A Lua:

shaken_scene_handler = nil -- Signal handler of the scene kept to restore

La funzione signal_handler_connectper impostare un callback save_sceneitem_for_shake(con qualche funzione aggiuntiva per rideterminare la scena come sorgente per ottenere il gestore del segnale), mantenendo il riferimento al gestore del segnale. Aggiungi la connessione del segnale alla fine della save_sceneitem_for_shakefunzione, in Python:

  # Handles scene item deletion
  global shaken_scene_handler
  scene_as_source = obs.obs_scene_get_source(obs.obs_sceneitem_get_scene(sceneitem))
  shaken_scene_handler = obs.obs_source_get_signal_handler(scene_as_source)
  obs.signal_handler_connect(shaken_scene_handler, "item_remove", on_shaken_sceneitem_removed)

A Lua:

  -- Handles scene item deletion
  local scene_as_source = obs.obs_scene_get_source(obs.obs_sceneitem_get_scene(sceneitem))
  shaken_scene_handler = obs.obs_source_get_signal_handler(scene_as_source)
  obs.signal_handler_connect(shaken_scene_handler, "item_remove", on_shaken_sceneitem_removed)

Infine aggiungi la disconnessione alla fine di in restore_sceneitem_after_shake, prima della riga che si imposta shaken_sceneitemsu None, in Python:

    obs.signal_handler_disconnect(shaken_scene_handler, "item_remove", on_shaken_sceneitem_removed)

Aggiungi questa riga prima della riga che si imposta shaken_sceneitemalla nilfine di restore_sceneitem_after_shake, in Lua:

    obs.signal_handler_disconnect(shaken_scene_handler, "item_remove", shaken_sceneitem_remove_callback)

Questo lungo cambiamento non è niente di spettacolare, ma mostra come catturare un evento OBS ed evitare potenziali arresti anomali.

Proprietà e impostazioni dei dati

È ora di aggiungere di nuovo la funzionalità reale dopo diverse sezioni sulla gestione delle condizioni d'angolo.

Fino ad ora i parametri dell'animazione sono memorizzati solo in variabili globali e dovrebbero essere personalizzabili dall'utente. OBS fornisce un ampio set di funzioni per gestire le impostazioni utente, sulla base di due oggetti principali:

  • Le impostazioni dei dati contengono i valori, supportano i valori predefiniti e vengono salvate automaticamente da OBS per gli script
  • Le proprietà creano la GUI per modificare le impostazioni dei dati

Le funzioni di script globale di interesse sono:

  • script_defaults(settings)per impostare i valori predefiniti utilizzando funzioni quali obs_data_set_default_string(settings, name, value), dove si settingstrova l'oggetto delle impostazioni dei dati e nameil nome del parametro di impostazione dell'utente
  • script_properties()per costruire e restituire l'oggetto delle proprietà della GUI, utilizzando funzioni come obs_properties_add_text(props, name, description, type), dove si propstrova l'oggetto delle proprietà, nameil nome del relativo parametro di impostazione utente, descriptionuna breve descrizione HTML del parametro e typeil tipo di campo di immissione del testo (qui classica stringa a riga singola)
  • script_update(settings)trasferire i valori dai parametri delle impostazioni dei dati alle variabili rilevanti utilizzando funzioni come obs_data_get_string(settings, name)(la funzione viene chiamata sistematicamente una volta durante il caricamento dello script per una prima inizializzazione delle variabili)

Come una sorta di convenzione in questo tutorial, gli stessi nomi vengono utilizzati per le variabili globali e per i parametri di impostazione dei dati correlati (non vi è alcun obbligo di farlo).

Gli oggetti proprietà si occuperanno di aggiornare i valori delle impostazioni dei dati se modificati dall'utente, ei valori vengono memorizzati in modo persistente senza ulteriori sforzi. Il codice è semplice per le nostre 3 impostazioni / proprietà, in Python:

<a id='called-to-set-default-values-of-data-settings' class='anchor' aria-hidden='true'></a>
# Called to set default values of data settings
def script_defaults(settings):
  obs.obs_data_set_default_string(settings, "source_name", "")
  obs.obs_data_set_default_double(settings, "frequency", 2)
  obs.obs_data_set_default_int(settings, "amplitude", 10)

<a id='called-to-display-the-properties-gui' class='anchor' aria-hidden='true'></a>
# Called to display the properties GUI
def script_properties():
  props = obs.obs_properties_create()
  obs.obs_properties_add_text(props, "source_name", "Source name", obs.OBS_TEXT_DEFAULT)
  obs.obs_properties_add_float_slider(props, "frequency", "Shake frequency", 0.1, 20, 0.1)
  obs.obs_properties_add_int_slider(props, "amplitude", "Shake amplitude", 0, 90, 1)
  return props

<a id='called-after-change-of-settings-including-once-after-script-load' class='anchor' aria-hidden='true'></a>
# Called after change of settings including once after script load
def script_update(settings):
  global source_name, frequency, amplitude
  restore_sceneitem_after_shake()
  source_name = obs.obs_data_get_string(settings, "source_name")
  frequency = obs.obs_data_get_double(settings, "frequency")
  amplitude = obs.obs_data_get_int(settings, "amplitude")

A Lua:

-- Called to set default values of data settings
function script_defaults(settings)
  obs.obs_data_set_default_string(settings, "source_name", "")
  obs.obs_data_set_default_double(settings, "frequency", 2)
  obs.obs_data_set_default_int(settings, "amplitude", 10)
end

-- Called to display the properties GUI
function script_properties()
  props = obs.obs_properties_create()
  obs.obs_properties_add_text(props, "source_name", "Source name", obs.OBS_TEXT_DEFAULT)
  obs.obs_properties_add_float_slider(props, "frequency", "Shake frequency", 0.1, 20, 0.1)
  obs.obs_properties_add_int_slider(props, "amplitude", "Shake amplitude", 0, 90, 1)
  return props
end

-- Called after change of settings including once after script load
function script_update(settings)
  restore_sceneitem_after_shake()
  source_name = obs.obs_data_get_string(settings, "source_name")
  frequency = obs.obs_data_get_double(settings, "frequency")
  amplitude = obs.obs_data_get_int(settings, "amplitude")
end

Notare che:

  • restore_sceneitem_after_shakeviene chiamato all'inizio di script_updatenel caso in cui il nome della fonte sia stato cambiato
  • C'è sempre una leggera differenza tra le proprietà relative alla GUI e le impostazioni dei dati nell'API OBS. Ad esempio, un numero decimale è denominato doublenelle impostazioni dei dati mentre l'elemento della GUI è un float_slider(con un minimo, un massimo e un passaggio per guidare l'utente). Una stringnelle impostazioni dei dati viene modificata con una textproprietà sulla GUI.

Una volta aggiornato il codice, ricaricare lo script, immettere il nome della sorgente e quindi le proprietà possono essere modificate in tempo reale:

Shake proprietà

Elenco a discesa delle fonti

Nella sezione precedente obs_properties_add_textè stata introdotta la funzione per consentire all'utente di inserire manualmente il nome della sorgente. Ovviamente, poiché OBS conosce tutti i nomi di origine, qualsiasi utente si aspetterebbe che la fonte possa essere selezionata in un elenco a discesa.

Una proprietà dell'elenco a discesa viene implementata tramite la funzione obs_properties_add_listche restituisce un propertyoggetto elenco . Una volta che la proprietà dell'elenco a discesa esiste, deve essere popolata con voci selezionabili utilizzando obs_property_list_add_string. Una voce selezionabile è sempre una coppia di valore visualizzato ( nameargomento di obs_property_list_add_string, visibile all'utente) e valore codificato ( val, utilizzato per l'elaborazione). Nel nostro caso, entrambi i valori sono gli stessi.

Le fonti esistenti in OBS vengono enumerate con la funzione specifica dello script obs_enum_sources. Restituisce un array Python / Lua di riferimenti a oggetti sorgente. Usiamo obs_source_get_nameper recuperare il nome di ogni fonte. Alla fine, source_list_releasedeve essere utilizzato per rilasciare gli oggetti allocati.

Iniziamo definendo una funzione autonoma che legge l'elenco delle fonti e le popola nell'oggetto proprietà dell'elenco specificato. Aggiunge anche una voce vuota per gestire il valore predefinito di source_nameset in script_defaults, in Python:

<a id='fills-the-given-list-property-object-with-the-names-of-all-sources-plus-an-empty-one' class='anchor' aria-hidden='true'></a>
# Fills the given list property object with the names of all sources plus an empty one
def populate_list_property_with_source_names(list_property):
  sources = obs.obs_enum_sources()
  obs.obs_property_list_clear(list_property)
  obs.obs_property_list_add_string(list_property, "", "")
  for source in sources:
    name = obs.obs_source_get_name(source)
    obs.obs_property_list_add_string(list_property, name, name)
  obs.source_list_release(sources)

A Lua:

-- Fills the given list property object with the names of all sources plus an empty one
function populate_list_property_with_source_names(list_property)
  local sources = obs.obs_enum_sources()
  obs.obs_property_list_clear(list_property)
  obs.obs_property_list_add_string(list_property, "", "")
  for _,source in pairs(sources) do
    local name = obs.obs_source_get_name(source)
    obs.obs_property_list_add_string(list_property, name, name)
  end
  obs.source_list_release(sources)
end

Quindi ridefiniamo la proprietà relativa a source_namecon una proprietà di elenco a discesa implementata tramite la funzione obs_properties_add_listche restituisce un propertyoggetto elenco . Nel nostro caso non sarà modificabile ( OBS_COMBO_TYPE_LIST) e verrà utilizzato per selezionare un tipo di dati stringa ( OBS_COMBO_FORMAT_STRING).

In script_properties, sostituisci la riga relativa a source_name(commentata di seguito) con il seguente blocco, in Python:

  # Drop-down list of sources
  list_property = obs.obs_properties_add_list(props, "source_name", "Source name",
              obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING)
  populate_list_property_with_source_names(list_property)
  # obs.obs_properties_add_text(props, "source_name", "Source name", obs.OBS_TEXT_DEFAULT)

A Lua:

  -- Drop-down list of sources
  local list_property = obs.obs_properties_add_list(props, "source_name", "Source name",
              obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING)
  populate_list_property_with_source_names(list_property)
  -- obs.obs_properties_add_text(props, "source_name", "Source name", obs.OBS_TEXT_DEFAULT)

Aggiorna il codice, ricarica lo script, quindi l'elenco a discesa dovrebbe essere visibile:

menu `A tendina

Pulsante per aggiornare l'elenco a discesa

Se una fonte viene aggiunta o rinominata, l'elenco delle fonti deve essere aggiornato. Un modo semplice per farlo è ricaricare lo script, ma potrebbe non essere sempre desiderabile se alcune risorse devono essere disallocate e riallocate (questo non è esattamente il caso nello script Source Shake, questa sezione mostra solo un'alternativa metodo).

In generale, script_propertiesviene chiamato una volta la prima volta che il widget delle proprietà deve essere visualizzato (es. Prima aggiunta dello script o prima visualizzazione della finestra degli script ). Dopodiché, le proprietà possono essere modificate dall'utente o dallo script e non vi è alcuna nuova chiamata a script_properties. Ma il widget viene aggiornato solo se uno dei callback impostati su una proprietà restituisce true . Questo meccanismo funziona per i callback impostati utilizzando obs_property_set_modified_callback(ad esempio per adattare la visibilità delle proprietà a seconda di altre proprietà) e per i callback dei pulsanti.

Quindi un altro modo per aggiornare le voci dell'elenco a discesa è definire un pulsante che attiverà l'aggiornamento dell'elenco e restituirà true. La nuova proprietà viene definita utilizzando la funzione specifica dello script obs_properties_add_button. La richiamata accetta 2 parametri che non verranno utilizzati. Si trova appena sotto la proprietà dell'elenco a discesa in script_properties, in Python ( list_propertyè un riferimento sulla proprietà dell'elenco a discesa, vedere la sezione precedente):

  # Button to refresh the drop-down list
  obs.obs_properties_add_button(props, "button", "Refresh list of sources",
    lambda props,prop: True if populate_list_property_with_source_names(list_property) else True)

Ammira la costruzione lambda / chiusura molto compatta che insiste davvero sul valore di ritorno True!

In Lua la stessa costruzione è possibile e più facile da capire (non è necessario fornire argomenti di callback):

  -- Button to refresh the drop-down list
  obs.obs_properties_add_button(props, "button", "Refresh list of sources",
    function() populate_list_property_with_source_names(list_property) return true end)

I pulsanti dovrebbero ora essere visualizzati sotto la proprietà dell'elenco a discesa:

pulsante di aggiornamento

Si noti che se una fonte viene rinominata o eliminata, source_nameviene mantenuto il vecchio valore di . Il widget dell'elenco a discesa sarà impostato sulla prima voce, che è vuota, in modo che sembri che nessuna fonte sia selezionata e in realtà nessuna fonte è animata perché il vecchio nome non può essere trovato nell'elenco delle fonti, quindi tutto sembra normale. Ci sono alcuni casi in cui questa piccola incongruenza potrebbe essere visibile, ad esempio se l'utente elimina una fonte, quindi aggiunge un'altra fonte con lo stesso nome, quest'altra fonte si animerà anche se l'elenco di selezione probabilmente non mostrerebbe alcuna fonte selezionata. Questo potrebbe essere risolto ricontrollando shake_sourcese source_nameè nell'elenco restituito da enum_sources, quindi impostato source_namesu una stringa vuota in caso contrario. Ma poiché il caso è altamente improbabile, consideriamolo come una caratteristica!

Stranamente, non esiste un metodo noto in OBS per attivare un aggiornamento ad eccezione delle funzioni di gestione delle proprietà utilizzate dal pulsante di aggiornamento. Questa è probabilmente l'unica soluzione finora.

Hotkey

Dall'inizio del tutorial, la descrizione menziona il tasto di scelta rapida, è ora di implementarlo.

Un tasto di scelta rapida viene registrato in OBS tramite l' obs_hotkey_register_frontendutilizzo di alcuni identificatori e una richiamata. La registrazione del tasto di scelta rapida non imposta la combinazione di tasti, che è impostata dall'utente nelle impostazioni di OBS. Ma poiché la combinazione di tasti selezionata non fa parte delle impostazioni dello script, OBS non la memorizzerà automaticamente nel file di configurazione dell'utente. Questa memorizzazione delle impostazioni è in realtà la parte complessa dell'aggiunta di un tasto di scelta rapida a uno script, soprattutto perché sono coinvolti gli array.

Cominciamo dall'inizio. Fino ad ora l'effetto shake è sempre attivo, quindi abbiamo bisogno di un flag di attività globale e dobbiamo cambiare script_tickper animare la sorgente con shake_sourceo ripristinare l'angolo originale con restore_sceneitem_after_shake, in Python:

<a id='global-animation-activity-flag' class='anchor' aria-hidden='true'></a>
# Global animation activity flag
is_active = False

<a id='called-every-frame' class='anchor' aria-hidden='true'></a>
# Called every frame
def script_tick(seconds):
  if is_active:
    shake_source()
  else:
    restore_sceneitem_after_shake()

A Lua:

-- Global animation activity flag
is_active = false

-- Called every frame
function script_tick(seconds)
  if is_active then
    shake_source()
  else
    restore_sceneitem_after_shake()
  end
end

Il callback di un tasto di scelta rapida ha un argomento booleano pressedimpostato su true se la combinazione di tasti viene premuta, false se è stata rilasciata. La richiamata viene generalmente chiamata una volta per la pressione del tasto, una volta per il rilascio. Il is_activeflag assumerà solo il valore di pressed, in Python:

<a id='callback-for-the-hotkey' class='anchor' aria-hidden='true'></a>
# Callback for the hotkey
def on_shake_hotkey(pressed):
  global is_active
  is_active = pressed

A Lua:

-- Callback for the hotkey
function on_shake_hotkey(pressed)
  is_active = pressed
end

Ora diamo un'occhiata alle cose difficili. Una combinazione di tasti è rappresentata da un array contenente la chiave principale più modificatori opzionali come "shift" o "control". Tale array viene memorizzato all'interno di un oggetto impostazioni dati, viene creato o letto se già esistente utilizzando obs_data_get_array(rilasciato da obs_data_array_release) e viene archiviato con obs_data_set_array.

Sono disponibili due funzioni (finora non documentate) per caricare / salvare il tasto di scelta rapida:

  • obs_hotkey_save(id) restituisce la matrice di combinazioni di tasti impostata in OBS per un dato tasto di scelta rapida id
  • obs_hotkey_load(id, data)carica in OBS l'array di combinazioni di tasti dataper il tasto di scelta rapida specificatoid

L'ID tasto di scelta rapida viene restituito da obs_hotkey_register_frontend. Deve essere mantenuto come variabile globale per creare il collegamento tra i dati caricati e quelli salvati. Il tasto di scelta rapida viene in genere registrato in script_load, dove la combinazione di tasti viene letta dalle impostazioni dei dati e caricata in OBS, in Python:

<a id='identifier-of-the-hotkey-set-by-obs' class='anchor' aria-hidden='true'></a>
# Identifier of the hotkey set by OBS
hotkey_id = obs.OBS_INVALID_HOTKEY_ID

<a id='called-at-script-load' class='anchor' aria-hidden='true'></a>
# Called at script load
def script_load(settings):
  global hotkey_id
  hotkey_id = obs.obs_hotkey_register_frontend(script_path(), "Source Shake", on_shake_hotkey)
  hotkey_save_array = obs.obs_data_get_array(settings, "shake_hotkey")
  obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
  obs.obs_data_array_release(hotkey_save_array)

A Lua:

-- Identifier of the hotkey set by OBS
hotkey_id = obs.OBS_INVALID_HOTKEY_ID

-- Called at script load
function script_load(settings)
  hotkey_id = obs.obs_hotkey_register_frontend(script_path(), "Source Shake", on_shake_hotkey)
  local hotkey_save_array = obs.obs_data_get_array(settings, "shake_hotkey")
  obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
  obs.obs_data_array_release(hotkey_save_array)
end

Si prega di notare che script_pathviene utilizzato come nome in obs_hotkey_register_frontend. Il vantaggio è che ogni copia dello stesso script (rinominata o in una directory diversa) può avere il proprio tasto di scelta rapida.

Il salvataggio delle impostazioni dei tasti di scelta rapida è simile alla parte di caricamento, aggiungi il seguente blocco alla fine della script_savefunzione, in Python:

  # Hotkey save
  hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
  obs.obs_data_set_array(settings, "shake_hotkey", hotkey_save_array)
  obs.obs_data_array_release(hotkey_save_array)

A Lua:

  -- Hotkey save
  local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
  obs.obs_data_set_array(settings, "shake_hotkey", hotkey_save_array)
  obs.obs_data_array_release(hotkey_save_array)

Adatta il codice, ricarica lo script. La sorgente dovrebbe smettere di muoversi e la tabella in Impostazioni > Tasti di scelta rapida dovrebbe mostrare il nuovo tasto di scelta rapida:

tasti di scelta rapida

Definisci una combinazione di tasti (qui una semplice virgola) e gioca con il tasto di scelta rapida!

Parole conclusive

Questo tutorial inizia da una soluzione rapida e sporca immediata a un effetto video ben integrato, robusto e personalizzabile. Diversi aspetti dell'API OBS vengono messi in pratica per mostrare i concetti principali e la logica generale dell'API. Ma graffia solo la superficie, ci sono ancora tonnellate di funzioni in OBS.

Divertiti con lo scripting!