]> git.sesse.net Git - nageru-docs/blobdiff - theme.rst
Document unsynchronized HDMI/SDI output.
[nageru-docs] / theme.rst
index 3fa5873587a0a665c8ea0061283983eb452fa3f5..8dbaf80b06b14d2aa09758348f2c0077b9b49a06 100644 (file)
--- a/theme.rst
+++ b/theme.rst
@@ -1,14 +1,6 @@
 The theme
 =========
 
-**NOTE**: Nageru 1.9.0 made significant improvements to themes
-and how scenes work. If you use an older version, you may want
-to look at `the 1.8.6 documentation <https://nageru.sesse.net/doc-1.8.6/>`_;
-themes written for older versions still work without modification in
-1.9.0, but are not documented here, and you are advised to change
-to use the new interfaces, as they are equally powerful and much simpler
-to work with.
-
 In Nageru, most of the business logic around how your stream
 ends up looking is governed by the **theme**, much like how a
 theme works on a blog or a CMS. Most importantly, the theme
@@ -83,6 +75,7 @@ produces a mix of them; Movit's *MixEffect* is exactly what you want here::
   local input1 = scene:add_input()
   input1:display(1)
 
+  -- Note that add_effect returns its input for convenience.
   local mix_effect = scene:add_effect(MixEffect.new(), input0, input1)
   scene:finalize()
 
@@ -94,9 +87,8 @@ this signature:
 “width” and “height” are what you'd expect (the output resolution).
 t contains the current stream time in seconds. “num” contains 0
 for the live view, 1 for the preview view, and 2, 3, 4, … for each
-of the individual stream previews. “signals“ contains a bit of
-information about each input signal, like its current resolution
-or frame rate.
+of the individual stream previews. “signals” contains a bit of
+information about each input signal (see :ref:`signal-info`).
 
 get_scene in return should return a scene. However, before you do that,
 you can set the parameters **strength_first** and **strength_second**
@@ -147,8 +139,8 @@ scaler before mixing; *ResampleEffect* provides one::
   local input0 = scene:add_input()
   input0:display(0)
   local input0_scaled = scene:add_optional_effect(ResampleEffect.new())  -- Implicitly uses input0.
-  scene_or_input.resample_effect:set_int("width", 1280)  -- Could also be set in get_scene().
-  scene_or_input.resample_effect:set_int("height", 720)
+  input0_scaled:set_int("width", 1280)  -- Could also be set in get_scene().
+  input0_scaled:set_int("height", 720)
   input0_scaled:enable()  -- Enable or disable as needed.
 
   local input1 = scene:add_input()
@@ -175,8 +167,8 @@ directly by the GPU) that instead. The scaling is set up like this::
   local input0 = scene:add_input()
   input0:display(0)
   local input0_scaled = scene:add_effect({ResampleEffect.new(), ResizeEffect.new()})  -- Implicitly uses input0.
-  scene_or_input.resample_effect:set_int("width", 1280)  -- Just like before.
-  scene_or_input.resample_effect:set_int("height", 720)
+  input0_scaled:set_int("width", 1280)  -- Just like before.
+  input0_scaled:set_int("height", 720)
 
   -- Pick one in get_scene() like this:
   input0_scaled:choose(ResizeEffect)
@@ -184,12 +176,28 @@ directly by the GPU) that instead. The scaling is set up like this::
   -- Or by numerical index:
   input0_scaled:choose(1)  -- Chooses ResizeEffect
 
-Note that add_effect returns its input for convenience.
-
 Actually, add_optional_effect() is just a wrapper around add_effect() with
 IdentityEffect as the other alternative, and disable() is a convenience version of
 choose(IdentityEffect).
 
+All alternatives must
+have the same amount of inputs, with an exception for IdentityEffect, which can
+coexist with an effect requiring any amount of inputs (if selected, the IdentityEffect
+just passes its first input unchanged). Similarly, if you set a parameter with
+set_int() or similar, it must be valid for all alternatives (again excepting
+IdentityEffect); if there is one that can only be used on a certain alternative,
+you must set it directly on the effect::
+
+  local resample_effect = ResampleEffect.new()
+  resample_effect:set_float("zoom_x", 1.0001)  -- Not valid for ResizeEffect.
+   
+  local input0_scaled = scene:add_effect({resample_effect, ResizeEffect.new()})
+  input0_scaled:set_int("width", 1280)  -- Set on both alternatives.
+  input0_scaled:set_int("height", 720)
+
+  -- This is also possible, as choose() returns the chosen effect:
+  input0_scaled:choose(ResampleEffect):set_float("zoom_y", 1.0001)
+
 Actually, more versions are created than you'd immediately expect.
 In particular, the output format for the live output and all previews are
 different (Y'CbCr versus RGBA), which is also handled transparently for you.
@@ -287,18 +295,28 @@ The theme should return a CSS color (e.g. “#ff0000”, or “cyan”) for each
 channel when asked; it can vary from frame to frame. A typical use is to mark
 the currently playing input as red, or the preview as green.
 
-And finally, there are two entry points related to white balance::
+
+.. _white-balance:
+
+White balance
+.............
+
+Finally, there are two entry points related to white balance. The first one
+is::
 
   Nageru.set_supports_wb(2, true)
-  function set_wb(channel, red, green, blue)
 
 If the first function is called with a true value (at the start of the theme),
 the channel will get a “Set WB” button next to it, which will activate a color
-picker. When the user picks a color (ostensibly with a gray point), the second
-function will be called (with the RGB values in linear light—not sRGB!),
-and the theme can then use it to adjust the white balance for that channel.
-The typical way to to this is to have a *WhiteBalanceEffect* on each input
-and set its “neutral_color” parameter using the “set_vec3” function.
+picker, to select the gray point. To actually *apply* this white balance change,
+add the white balance element to the scene::
+
+  scene:add_white_balance()
+
+The white balance effect will automatically figure out which input it is
+connected to, and fetch its gray point if needed. (If it is connected to
+e.g. a mix of several inputs, such as a camera and an overlay, you will need to
+give the input to fetch white balance from as as a parameter.)
 
 
 More complicated channels: Composites
@@ -383,11 +401,21 @@ crop is. If so, you can do this::
 
    resample_effect:always_disable_if_disabled(crop_effect)
 
+Also, you can disable an optional effect if a given other
+effect is *enabled*::
+
+   overlay1_effect:promise_to_disable_if_enabled(overlay2_effect)
+   overlay2_effect:promise_to_disable_if_enabled(overlay1_effect)
+
+Note that the latter is a promise from the user, not automatic disabling; since
+it is mostly useful for mutual exclusions, Nageru wouldn't know which of the
+two to disable. (If you violate the promise, you will get an error message at
+runtime.) It can still be useful for reducing the number of alternatives, though.
+
 For more advanced exclusions, you may choose to split up the scenes into several
-distinct ones that you manage yourself; indeed, before Nageru 1.9.0, that was
-the only option. At some point, however, you may choose to simply accept the
-added startup time and a bit of extra RAM cost; ease of use and flexibility often
-trumps such concerns.
+distinct ones that you manage yourself. At some point, however, you may choose
+to simply accept the added startup time and a bit of extra RAM cost; ease of
+use and flexibility often trumps such concerns.
 
 
 .. _menus:
@@ -448,3 +476,103 @@ or the likes. However, do note that since the theme is written in unrestricted
 Lua, so you can use e.g. `lua-http <https://github.com/daurnimator/lua-http>`_
 to listen for external connections and accept more complicated inputs
 from those.
+
+
+.. _signal-info:
+
+Signal information queries
+--------------------------
+
+As previously mentioned, get_scene() takes in a “signals” parameter
+that you can query for information about each signal (numbered from 0;
+live and preview are channels, not signals), like its current resolution
+or frame rate:
+
+  * get_frame_width(signal), get_frame_height(signal): Width and height of the last frame.
+  * get_width(signal), get_height(signal): Width and height of the last *field*
+    (the field height is half of the frame height for an interlaced signal).
+  * get_interlaced(signal): Whether the last frame was interlaced.
+  * get_has_signal(signal): Whether there is a valid input signal.
+  * get_is_connected(signal): Whether there is even a card connected
+    to this signal (USB cards can be swapped in or out); if not,
+    you will get a stream of single-colored frames.
+  * get_frame_rate_nom(signal), get_frame_rate_den(signal): The frame rate
+    of the last frame, as a rational (e.g. 60/1, or 60000/1001 for 59.94).
+  * get_last_subtitle(signal): See :ref:`subtitle-ingest`.
+  * get_human_readable_resolution(signal): The resolution and frame rate in
+    human-readable form (e.g. “1080i59.94”), suitable for e.g. stream titles.
+    Note that Nageru does not follow the EBU recommendation of using
+    frame rate even for interlaced signals (e.g. “1080i25” instead of “1080i50”),
+    since it is little-used and confusing to most users.
+
+You can use this either for display purposes, or for choosing the right
+effect alternatives. In particular, you may want to disable scaling if
+the frame is already of the correct resolution.
+
+
+Audio control
+-------------
+
+Before you attempt to control audio from the theme, be sure to have read
+the documentation about :doc:`audio`.
+
+The theme has a certain amount of control over the audio
+mix, assuming that you are in multichannel mode. This is useful in particular
+to be able to set defaults, if e.g. one channel should always be muted at
+startup, or to switch in/out certain channels depending on whether they are
+visible or not.
+
+In particular, these operations are available::
+
+  # Returns number of buses in the mapping.
+  local num_buses = Nageru.get_num_audio_buses()
+
+  # Gets the name from the mapping. All indexes start at zero,
+  # so valid indexes are 0..(num_buses-1), inclusive.
+  local name = Nageru.get_audio_bus_name(N)
+
+  # 0.0 is zero amplification, as in the UI. Valid range is
+  # -inf to +6.0, inclusive.
+  local level = Nageru.get_audio_bus_fader_level_db(N)
+  set_audio_bus_fader_level_db(N, level)
+
+  # Similar as the above, but valid range is -15.0..+15.0 (dB).
+  # Valid bands are Nageru.EQ_BAND_{BASS, MID, TREBLE}.
+  local eq_level = Nageru.get_audio_bus_eq_level_db(N, Nageru.EQ_BAND_BASS)
+  Nageru.set_audio_bus_eq_level_db(N, Nageru.EQ_BAND_BASS, level)
+
+  # A boolean. Does not affect the bus levels set/returned above.
+  local muted = Nageru_get_audio_bus_mute(N)
+  Nageru_set_audio_bus_mute(N, false)
+
+Note that any audio operation is inherently unsynchronized with the UI,
+so if the user reduces the number of audio buses while
+the theme tries to access one that is going away, you may get unpredictable
+behavior, up to and including crashes. Thus, you will need to be careful
+with such operations.
+
+Also, you cannot do anything with audio before the first *get_scene()* call,
+since the audio mixer is initialized only after the theme has been loaded and
+initialized. Thus, for things that should be done only the first frame, the
+recommended method is to put code into get_scene() and have a guard variable
+that makes sure it is only run
+once, ever.
+
+
+Overriding the status line
+--------------------------
+
+Some users may wish to override the status line, e.g. with recording time.
+If so, it is possible to declare a function **format_status_line**::
+
+  function format_status_line(disk_space_text, file_length_seconds)
+    if file_length_seconds > 86400.0 then
+      return "Time to make a new segment"
+    else
+      return "Disk space left: " .. disk_space_text
+    end
+  end
+
+As demonstrated, it is given the disk space text (that would normally
+be there), and the length of the current recording file in seconds.
+HTML is supported.