1 -- Nageru theme for TFK mini-tournament 2017, based on the default theme.
3 local futatabi_server = "http://gruessi.trd.sesse.net:9096"
6 transition_start = -2.0,
9 transition_src_signal = 0,
10 transition_dst_signal = 0,
13 {0.5, 0.5, 0.5}, -- Input 0.
14 {0.5, 0.5, 0.5}, -- Input 1.
15 {0.5, 0.5, 0.5}, -- Input 2.
16 {0.5, 0.5, 0.5}, -- Input 3.
17 {0.5, 0.5, 0.5}, -- Input 4.
18 {0.5, 0.5, 0.5} -- Input 5.
19 -- Will also be filled with VIDEO_SIGNAL_NUM below.
22 overlay_transition_start = -2.0,
23 overlay_transition_end = -1.0,
24 overlay_alpha_src = 0.0,
25 overlay_alpha_dst = 1.0,
26 overlay_enabled = false,
28 stinger_in_progress = false,
30 stinger_src_signal = 0,
31 stinger_dst_signal = 0,
32 stinger_save_overlay = false,
35 preview_signal_num = 1
37 local NUM_CAMERAS = 6 -- Remember to update neutral_colors, too.
39 -- Valid values for live_signal_num and preview_signal_num.
40 local INPUT0_SIGNAL_NUM = 0
41 local INPUT1_SIGNAL_NUM = 1
42 local INPUT2_SIGNAL_NUM = 2
43 local INPUT3_SIGNAL_NUM = 3
44 local INPUT4_SIGNAL_NUM = 4
45 local INPUT5_SIGNAL_NUM = 5
46 local SBS_SIGNAL_NUM = NUM_CAMERAS
47 local STATIC_SIGNAL_NUM = NUM_CAMERAS + 1
48 local VIDEO_SIGNAL_NUM = NUM_CAMERAS + 2
50 state.neutral_colors[VIDEO_SIGNAL_NUM - INPUT0_SIGNAL_NUM + 1] = {0.5, 0.5, 0.5}
52 -- Preview-only signal showing the current signal with the overlay.
53 -- Not valid for live_signal_num!
54 local OVERLAY_SIGNAL_NUM = NUM_CAMERAS + 3
56 -- Valid values for transition_type. (Cuts are done directly, so they need no entry.)
57 local NO_TRANSITION = 0
58 local ZOOM_TRANSITION = 1
59 local FADE_TRANSITION = 2
61 -- Last width/height/frame rate for each channel, if we have it.
62 -- Note that unlike the values we get from Nageru, the resolution is per
63 -- frame and not per field, since we deinterlace.
64 local last_resolution = {}
66 local cef_path = Nageru.THEME_PATH:match("(.*)/nageru/")
67 local cef_input = HTMLInput.new("file://" .. cef_path .. "/score.html")
68 cef_input:execute_javascript_async("play()")
70 local bg_video = VideoInput.new(cef_path .. "/flow-720.mp4", Nageru.VIDEO_FORMAT_YCBCR)
71 -- local iptv_video = VideoInput.new("http://10.34.129.69:9060/1/v.flv", Nageru.VIDEO_FORMAT_YCBCR)
72 local iptv_video = VideoInput.new(futatabi_server, Nageru.VIDEO_FORMAT_YCBCR)
74 local static_image = ImageInput.new(cef_path .. "/nageru/dsn-bg.png")
78 cef_input:execute_javascript_async("play()")
81 function disconnect_iptv()
82 iptv_video:disconnect()
85 -- An overlay with variable alpha.
86 function make_overlay(scene, base)
87 local image = scene:add_input(cef_input)
88 local multiply_effect = scene:add_effect(MultiplyEffect.new())
89 local overlay_effect = scene:add_optional_effect(OverlayEffect.new(), base, multiply_effect)
92 multiply_effect = multiply_effect,
93 overlay_effect = overlay_effect
97 function make_fade_input(scene)
99 input = scene:add_input(),
100 resample_effect = scene:add_optional_effect(ResampleEffect.new()), -- Activated if scaling.
101 wb_effect = scene:add_optional_effect(WhiteBalanceEffect.new()) -- Activated for video inputs.
103 ret.overlay = make_overlay(scene, ret.wb_effect)
107 -- A scene to fade between two inputs, of which either can be a picture
109 function make_fade_scene()
110 local scene = Scene.new(16, 9)
112 -- If fading between two live inputs, the overlay is put on top.
113 -- If fading between the static picture and a live input,
114 -- the overlay is put on the live input.
115 local input0 = make_fade_input(scene)
116 local input1 = make_fade_input(scene)
117 local mix_effect = scene:add_effect(MixEffect.new(), input0.overlay.overlay_effect, input1.overlay.overlay_effect)
118 local overlay = make_overlay(scene, mix_effect)
120 -- At most one overlay can be active at any given time.
121 input0.overlay.overlay_effect:promise_to_disable_if_enabled(input1.overlay.overlay_effect)
122 input0.overlay.overlay_effect:promise_to_disable_if_enabled(overlay.overlay_effect)
123 input1.overlay.overlay_effect:promise_to_disable_if_enabled(input0.overlay.overlay_effect)
124 input1.overlay.overlay_effect:promise_to_disable_if_enabled(overlay.overlay_effect)
125 overlay.overlay_effect:promise_to_disable_if_enabled(input0.overlay.overlay_effect)
126 overlay.overlay_effect:promise_to_disable_if_enabled(input1.overlay.overlay_effect)
128 scene:finalize(true) -- Only used live, don't bother creating LQ versions.
134 mix_effect = mix_effect,
138 fade_scene = make_fade_scene()
140 function make_sbs_input(scene, has_overlay)
141 local input = scene:add_input()
142 local resample_effect = ResampleEffect.new()
143 local resize_effect = ResizeEffect.new()
146 overlay = has_overlay and make_overlay(scene, input) or nil,
147 resample_effect = resample_effect,
148 resize_effect = resize_effect,
149 resample_switcher = scene:add_effect({resample_effect, resize_effect}),
150 wb_effect = scene:add_effect(WhiteBalanceEffect.new()),
151 padding_effect = scene:add_effect(IntegralPaddingEffect.new())
155 -- Side-by-side scene.
156 function make_sbs_scene()
157 local scene = Scene.new(16, 9)
159 local bg = scene:add_input(bg_video)
160 local input0 = make_sbs_input(scene, true)
161 local input1 = make_sbs_input(scene, false)
163 input0.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 0.0)
164 input1.padding_effect:set_vec4("border_color", 0.0, 0.0, 0.0, 0.0)
166 input0.input:display(0)
167 input1.input:display(1)
169 local i0 = scene:add_effect(OverlayEffect.new(), bg, input0.padding_effect)
170 scene:add_effect(OverlayEffect.new(), i0, input1.padding_effect)
177 overlay = input0.overlay -- May be nil.
180 local sbs_scene = make_sbs_scene()
182 function make_simple_scene_no_finalize()
183 local scene = Scene.new(16, 9)
185 local input = scene:add_input()
186 local resample_effect = scene:add_effect({ResampleEffect.new(), ResizeEffect.new(), IdentityEffect.new()})
187 local wb_effect = scene:add_effect(WhiteBalanceEffect.new())
188 local overlay = make_overlay(scene, wb_effect)
193 wb_effect = wb_effect,
194 resample_effect = resample_effect,
199 -- A scene to show a single input on screen.
200 function make_simple_scene()
201 local scene = make_simple_scene_no_finalize()
202 scene.scene:finalize()
206 local simple_scene = make_simple_scene()
208 -- Load all the stinger frames.
209 local stinger_images = {}
211 local filename = cef_path .. "/stinger/blur" .. string.rep("0", 3 - string.len(tostring(idx))) .. idx .. ".png"
212 stinger_images[idx] = ImageInput.new(filename)
215 -- A scene to show a single input on screen, with a stinger on top.
216 function make_stinger_scene()
217 local scene = make_simple_scene_no_finalize()
218 scene.stinger_input = scene.scene:add_input()
219 scene.scene:add_effect(OverlayEffect.new(), scene.overlay.overlay_effect, scene.stinger_input)
220 scene.scene:finalize()
223 local stinger_scene = make_stinger_scene()
225 -- A scene to show a single static picture on screen. Never with HTML overlay.
226 local static_scene = Scene.new(16, 9)
227 static_scene:add_input(static_image)
228 static_scene:finalize()
230 -- A scene to show the overlay and nothing more. LQ only,
231 -- since it is not a valid live signal.
232 local overlay_scene_lq = Scene.new(16, 9)
233 local overlay_scene_lq_input = overlay_scene_lq:add_input(cef_input)
234 overlay_scene_lq:finalize(false)
236 function needs_scale(signals, signal_num, width, height)
237 if signal_num == STATIC_SIGNAL_NUM then
238 -- We assume this is already correctly scaled at load time.
241 assert(is_plain_signal(signal_num))
242 return (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height)
245 function set_scale_parameters_if_needed(scene_or_input, signals, signal_num, width, height)
246 if needs_scale(signals, signal_num, width, height) then
248 scene_or_input.resample_effect:choose(ResampleEffect) -- High-quality resampling.
250 scene_or_input.resample_effect:choose(ResizeEffect) -- Low-quality resampling.
252 scene_or_input.resample_effect:set_int("width", width)
253 scene_or_input.resample_effect:set_int("height", height)
255 scene_or_input.resample_effect:disable()
261 -- Returns the number of outputs in addition to the live (0) and preview (1).
262 -- Called only once, at the start of the program.
263 function num_channels()
264 return NUM_CAMERAS + 4 -- sbs, static picture, video and overlay
267 function is_plain_signal(num)
268 return (num >= INPUT0_SIGNAL_NUM and num <= INPUT5_SIGNAL_NUM) or (num == VIDEO_SIGNAL_NUM)
271 -- Helper function to write e.g. “720p60”. The difference between this
272 -- and get_channel_resolution_raw() is that this one also can say that
273 -- there's no signal.
274 function get_channel_resolution(signal_num)
275 local res = last_resolution[signal_num]
276 if (not res) or not res.is_connected then
277 return "disconnected"
279 if res.height <= 0 then
282 if not res.has_signal then
283 if res.height == 525 then
284 -- Special mode for the USB3 cards.
287 return get_channel_resolution_raw(res) .. ", no signal"
289 return get_channel_resolution_raw(res)
293 -- Helper function to write e.g. “60” or “59.94”.
294 function get_frame_rate(res)
295 local nom = res.frame_rate_nom
296 local den = res.frame_rate_den
297 if nom % den == 0 then
300 return string.format("%.2f", nom / den)
304 -- Helper function to write e.g. “720p60”.
305 function get_channel_resolution_raw(res)
306 if res.interlaced then
307 return res.height .. "i" .. get_frame_rate(res)
309 return res.height .. "p" .. get_frame_rate(res)
313 function get_futatabi_status(str)
316 for word in string.gmatch(str, '([^;]+)') do
317 table.insert(fields, word)
318 num_fields = num_fields + 1
320 if num_fields >= 4 then
328 -- Returns the name for each additional channel (starting from 2).
329 -- Called at the start of the program, and then each frame for live
330 -- channels in case they change resolution.
331 function channel_name(channel)
332 local signal_num = channel - 2
333 if signal_num == INPUT0_SIGNAL_NUM then
334 return "Main (" .. get_channel_resolution(signal_num) .. ")"
335 elseif signal_num == INPUT1_SIGNAL_NUM then
336 return "Secondary (" .. get_channel_resolution(signal_num) .. ")"
337 elseif signal_num == INPUT2_SIGNAL_NUM then
338 return "Goal L (" .. get_channel_resolution(signal_num) .. ")"
339 elseif signal_num == INPUT3_SIGNAL_NUM then
340 return "Goal R (" .. get_channel_resolution(signal_num) .. ")"
341 elseif signal_num == INPUT4_SIGNAL_NUM then
342 return "Commentators (" .. get_channel_resolution(signal_num) .. ")"
343 elseif signal_num == INPUT5_SIGNAL_NUM then
344 return "Laptop (" .. get_channel_resolution(signal_num) .. ")"
345 elseif signal_num == SBS_SIGNAL_NUM then
346 return "Side-by-side"
347 elseif signal_num == STATIC_SIGNAL_NUM then
348 return "Static picture"
349 elseif signal_num == VIDEO_SIGNAL_NUM then
350 local res = last_resolution[iptv_video:get_signal_num()]
351 if (not res) or res.last_subtitle == nil then
354 return "IPTV (" .. get_futatabi_status(res.last_subtitle) .. ")"
356 elseif signal_num == OVERLAY_SIGNAL_NUM then
362 -- Returns, given a channel number, which signal it corresponds to (starting from 0).
363 -- Should return -1 if the channel does not correspond to a simple signal.
364 -- (The information is used for whether right-click on the channel should bring up
365 -- an input selector or not.)
366 -- Called once for each channel, at the start of the program.
367 -- Will never be called for live (0) or preview (1).
368 function channel_signal(channel)
369 if channel - 2 == VIDEO_SIGNAL_NUM then
370 return iptv_video:get_signal_num()
371 elseif is_plain_signal(channel - 2) then
379 -- Called every frame. Returns the color (if any) to paint around the given
380 -- channel. Returns a CSS color (typically to mark live and preview signals);
381 -- "transparent" is allowed.
382 -- Will never be called for live (0) or preview (1).
383 function channel_color(channel)
384 if state.transition_type ~= NO_TRANSITION then
385 if channel_involved_in(channel, state.transition_src_signal) or
386 channel_involved_in(channel, state.transition_dst_signal) then
390 if channel_involved_in(channel, state.live_signal_num) then
394 if channel_involved_in(channel, state.preview_signal_num) then
400 function channel_involved_in(channel, signal_num)
401 if is_plain_signal(signal_num) then
402 return channel == (signal_num + 2)
404 if signal_num == SBS_SIGNAL_NUM then
405 return is_sbs_participating_signal(channel - 2)
407 if signal_num == STATIC_SIGNAL_NUM then
408 return (channel == NUM_CAMERAS)
410 if signal_num == VIDEO_SIGNAL_NUM then
411 return (channel == NUM_CAMERAS + 1)
417 -- Returns if a given channel supports setting white balance (starting from 2).
418 -- Called only once for each channel, at the start of the program.
419 function supports_set_wb(channel)
420 return is_plain_signal(channel - 2)
424 -- Gets called with a new gray point when the white balance is changing.
425 -- The color is in linear light (not sRGB gamma).
426 function set_wb(channel, red, green, blue)
427 if is_plain_signal(channel - 2) then
428 state.neutral_colors[channel - 2 + 1] = { red, green, blue }
432 function finish_transitions(t)
433 if state.transition_type ~= NO_TRANSITION and t >= state.transition_end then
434 state.live_signal_num = state.transition_dst_signal
435 state.transition_type = NO_TRANSITION
438 -- Disable the overlay if it is no longer visible.
439 if state.overlay_enabled and t > state.overlay_transition_end and state.overlay_alpha_dst == 0.0 then
440 state.overlay_enabled = false
441 print("Turning off overlay")
445 function in_transition(t)
446 return t >= state.transition_start and t <= state.transition_end
449 function is_sbs_participating_signal(signal_num)
450 return signal_num == INPUT0_SIGNAL_NUM or signal_num == INPUT4_SIGNAL_NUM
453 function simple_signal_has_overlay(signal_num)
454 -- The commentator output has no overlay on it.
455 return signal_num ~= INPUT4_SIGNAL_NUM
459 -- Called every frame.
460 function get_transitions(t)
461 if state.preview_signal_num == OVERLAY_SIGNAL_NUM then
462 if t < state.overlay_transition_end then
466 if state.overlay_enabled then
467 return {"Overlay off", "", "Fade ovl out"}
469 return {"Overlay on", "", "Fade ovl in"}
473 if in_transition(t) then
474 -- Transition already in progress, the only thing we can do is really
475 -- cut to the preview. (TODO: Make an “abort” and/or “finish”, too?)
479 if state.live_signal_num == state.preview_signal_num then
480 -- No transitions possible.
484 if (is_plain_signal(state.live_signal_num) and state.preview_signal_num == VIDEO_SIGNAL_NUM) or
485 (is_plain_signal(state.preview_signal_num) and state.live_signal_num == VIDEO_SIGNAL_NUM) then
486 return {"Cut", "Sting", "Fade"}
489 if (is_plain_signal(state.live_signal_num) or state.live_signal_num == STATIC_SIGNAL_NUM) and
490 (is_plain_signal(state.preview_signal_num) or state.preview_signal_num == STATIC_SIGNAL_NUM) then
491 return {"Cut", "", "Fade"}
495 if state.live_signal_num == SBS_SIGNAL_NUM and is_sbs_participating_signal(state.preview_signal_num) then
496 return {"Cut", "Zoom in"}
497 elseif is_sbs_participating_signal(state.live_signal_num) and state.preview_signal_num == SBS_SIGNAL_NUM then
498 return {"Cut", "Zoom out"}
504 function swap_preview_live()
505 local temp = state.live_signal_num
506 state.live_signal_num = state.preview_signal_num
507 state.preview_signal_num = temp
510 function start_transition(type_, t, duration)
511 state.transition_start = t
512 state.transition_end = t + duration
513 state.transition_type = type_
514 state.transition_src_signal = state.live_signal_num
515 state.transition_dst_signal = state.preview_signal_num
516 state.stinger_in_progress = false
521 -- Called when the user clicks a transition button.
522 function transition_clicked(num, t)
523 if state.preview_signal_num == OVERLAY_SIGNAL_NUM then
526 state.overlay_transition_start = -2.0
527 state.overlay_transition_end = -1.0
528 if state.overlay_enabled then
529 state.overlay_enabled = false
530 state.overlay_alpha_src = 0.0
531 state.overlay_alpha_dst = 0.0
533 state.overlay_enabled = true
534 state.overlay_alpha_src = 1.0
535 state.overlay_alpha_dst = 1.0
539 state.overlay_transition_start = t
540 state.overlay_transition_end = t + 1.0
541 if state.overlay_enabled then
542 state.overlay_alpha_src = 1.0
543 state.overlay_alpha_dst = 0.0
545 state.overlay_alpha_src = 0.0
546 state.overlay_alpha_dst = 1.0
548 state.overlay_enabled = true
555 if in_transition(t) then
556 -- Ongoing transition; finish it immediately before the cut.
557 finish_transitions(state.transition_end)
561 state.stinger_in_progress = false
564 finish_transitions(t)
566 if state.live_signal_num == state.preview_signal_num then
571 if (is_plain_signal(state.live_signal_num) and state.preview_signal_num == VIDEO_SIGNAL_NUM) or
572 (is_plain_signal(state.preview_signal_num) and state.live_signal_num == VIDEO_SIGNAL_NUM) then
574 if stinger_in_progress then
578 state.stinger_in_progress = true
579 state.stinger_frame = 0
580 state.stinger_src_signal = state.live_signal_num
581 state.stinger_dst_signal = state.preview_signal_num
585 if is_plain_signal(state.live_signal_num) and is_plain_signal(state.preview_signal_num) then
586 -- We can't zoom between these. Just make a cut.
587 io.write("Cutting from " .. state.live_signal_num .. " to " .. state.live_signal_num .. "\n")
589 state.stinger_in_progress = false
593 if (state.live_signal_num == SBS_SIGNAL_NUM and is_sbs_participating_signal(state.preview_signal_num)) or
594 (state.preview_signal_num == SBS_SIGNAL_NUM and is_sbs_participating_signal(state.live_signal_num)) then
595 start_transition(ZOOM_TRANSITION, t, 1.0)
598 finish_transitions(t)
601 if (state.live_signal_num ~= state.preview_signal_num) and
602 (is_plain_signal(state.live_signal_num) or
603 state.live_signal_num == STATIC_SIGNAL_NUM) and
604 (is_plain_signal(state.preview_signal_num) or
605 state.preview_signal_num == STATIC_SIGNAL_NUM) then
606 start_transition(FADE_TRANSITION, t, 1.0)
608 -- Fades involving SBS are ignored (we have no scene for it).
614 function channel_clicked(num)
615 state.preview_signal_num = num
618 function setup_fade_input(state, input, signals, signal_num, width, height)
619 if signal_num == STATIC_SIGNAL_NUM then
620 input.input:display(static_image)
621 input.wb_effect:disable()
623 -- We assume this is already correctly scaled at load time.
624 input.resample_effect:disable()
626 input.input:display(signal_num)
627 input.wb_effect:enable()
628 set_neutral_color(input.wb_effect, state.neutral_colors[signal_num - INPUT0_SIGNAL_NUM + 1])
630 if (signals:get_width(signal_num) ~= width or signals:get_height(signal_num) ~= height) then
631 input.resample_effect:enable()
632 input.resample_effect:set_int("width", width)
633 input.resample_effect:set_int("height", height)
635 input.resample_effect:disable()
640 function get_fade_scene(state, signals, t, width, height, input_resolution)
641 local scene = fade_scene
642 setup_fade_input(state, scene.input0, signals, state.transition_src_signal, width, height)
643 setup_fade_input(state, scene.input1, signals, state.transition_dst_signal, width, height)
645 local tt = calc_fade_progress(t, state.transition_start, state.transition_end)
646 scene.mix_effect:set_float("strength_first", 1.0 - tt)
647 scene.mix_effect:set_float("strength_second", tt)
649 -- The commentator output has no overlay on it.
650 local extra_alpha_factor = 1.0
651 if not simple_signal_has_overlay(state.transition_src_signal) and
652 not simple_signal_has_overlay(state.transition_dst_signal) then
653 extra_alpha_factor = 0.0
654 elseif not simple_signal_has_overlay(state.transition_src_signal) then
655 extra_alpha_factor = tt
656 elseif not simple_signal_has_overlay(state.transition_dst_signal) then
657 extra_alpha_factor = 1.0 - tt
660 -- If fading between two live inputs, the overlay is put on top.
661 -- If fading between the static picture and a live input,
662 -- the overlay is put on the live input.
663 scene.input0.overlay.overlay_effect:disable()
664 scene.input1.overlay.overlay_effect:disable()
665 scene.overlay.overlay_effect:disable()
666 if state.overlay_enabled then
667 local input0_live = (state.transition_src_signal ~= STATIC_SIGNAL_NUM)
668 local input1_live = (state.transition_dst_signal ~= STATIC_SIGNAL_NUM)
669 if input0_live and not input1_live then
670 scene.input0.overlay.overlay_effect:enable()
671 prepare_overlay_live(state, scene.input0, t, extra_alpha_factor)
672 elseif input1_live and not input0_live then
673 scene.input1.overlay.overlay_effect:enable()
674 prepare_overlay_live(state, scene.input1, t, extra_alpha_factor)
676 scene.overlay.overlay_effect:enable()
677 prepare_overlay_live(state, scene, t, extra_alpha_factor)
683 function fetch_input_resolution(signals, signal_num)
685 width = signals:get_width(signal_num),
686 height = signals:get_height(signal_num),
687 interlaced = signals:get_interlaced(signal_num),
688 has_signal = signals:get_has_signal(signal_num),
689 is_connected = signals:get_is_connected(signal_num),
690 frame_rate_nom = signals:get_frame_rate_nom(signal_num),
691 frame_rate_den = signals:get_frame_rate_den(signal_num),
692 last_subtitle = signals:get_last_subtitle(signal_num)
695 if res.interlaced then
696 -- Convert height from frame height to field height.
697 -- (Needed for e.g. place_rectangle.)
698 res.height = res.height * 2
700 -- Show field rate instead of frame rate; really for cosmetics only
701 -- (and actually contrary to EBU recommendations, although in line
702 -- with typical user expectations).
703 res.frame_rate_nom = res.frame_rate_nom * 2
708 local last_rate = 0.0
711 -- Called every frame. Get the scene for displaying at input <num>,
712 -- where 0 is live, 1 is preview, 2 is the first channel to display
713 -- in the bottom bar, and so on up to num_channels()+1. t is the
714 -- current time in seconds. width and height are the dimensions of
715 -- the output, although you can ignore them if you don't need them
716 -- (they're useful if you want to e.g. know what to resample by).
718 -- <signals> is basically an exposed InputState, which you can use to
719 -- query for information about the signals at the point of the current
720 -- frame. In particular, you can call get_width() and get_height()
721 -- for any signal number, and use that to e.g. assist in scene selection.
723 -- You should return scene to use, after having set any parameters you
724 -- want to set (through set_int() etc.). The parameters will be snapshot
725 -- at return time and used during rendering.
726 function get_scene(num, t, width, height, signals)
727 local input_resolution = {}
728 for signal_num=0,(NUM_CAMERAS-1) do
729 input_resolution[signal_num] = fetch_input_resolution(signals, signal_num)
731 input_resolution[iptv_video:get_signal_num()] = fetch_input_resolution(signals, iptv_video:get_signal_num())
732 last_resolution = input_resolution
734 -- Save some CPU time if we're not having SBS on live.
736 if state.live_signal_num == SBS_SIGNAL_NUM or
737 state.preview_signal_num == SBS_SIGNAL_NUM or
738 state.transition_type == ZOOM_TRANSITION then
743 if new_rate ~= last_rate then
744 -- Avoid waking up the video thread (which may be sleeping) if the rate is the same.
745 bg_video:change_rate(new_rate)
749 if num == 0 then -- Live.
750 -- See if we're in a transition.
751 finish_transitions(t)
752 if state.transition_type == ZOOM_TRANSITION then
753 -- Transition in or out of SBS.
754 sbs_scene.input0.overlay.overlay_effect:enable_if(state.overlay_enabled)
755 prepare_sbs_scene(state, sbs_scene, calc_zoom_progress(t), state.transition_type, state.transition_src_signal, state.transition_dst_signal, width, height, input_resolution)
756 prepare_overlay_live(state, sbs_scene, t, 1.0)
757 return sbs_scene.scene
758 elseif state.transition_type == NO_TRANSITION and state.live_signal_num == SBS_SIGNAL_NUM then
760 sbs_scene.input0.overlay.overlay_effect:enable_if(state.overlay_enabled)
761 prepare_sbs_scene(state, sbs_scene, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution)
762 prepare_overlay_live(state, sbs_scene, t, 1.0)
763 return sbs_scene.scene
764 elseif state.transition_type == FADE_TRANSITION then
765 return get_fade_scene(state, signals, t, width, height, input_resolution)
766 elseif is_plain_signal(state.live_signal_num) then
767 local overlay_really_enabled = state.overlay_enabled and simple_signal_has_overlay(state.live_signal_num)
768 if state.stinger_in_progress then
769 stinger_scene.overlay.overlay_effect:enable_if(overlay_really_enabled)
770 stinger_scene.input:display(state.live_signal_num)
771 set_scale_parameters_if_needed(stinger_scene, signals, state.live_signal_num, width, height)
772 set_neutral_color_from_signal(state, stinger_scene.wb_effect, state.live_signal_num)
774 stinger_scene.stinger_input:display(stinger_images[state.stinger_frame])
775 state.stinger_frame = state.stinger_frame + 1
776 if state.stinger_frame >= 25 then
777 state.stinger_in_progress = false
778 state.preview_signal_num = state.stinger_src_signal
779 state.live_signal_num = state.stinger_dst_signal
781 if state.stinger_dst_signal == VIDEO_SIGNAL_NUM then
782 -- Turn off the overlay when playing video.
783 state.stinger_save_overlay = state.overlay_enabled
784 state.overlay_enabled = false
786 -- Restore the state.
787 state.overlay_enabled = state.stinger_save_overlay
791 if overlay_really_enabled then
792 prepare_overlay_live(state, stinger_scene, t, 1.0)
794 return stinger_scene.scene
796 simple_scene.overlay.overlay_effect:enable_if(overlay_really_enabled)
797 simple_scene.input:display(state.live_signal_num)
798 set_scale_parameters_if_needed(simple_scene, signals, state.live_signal_num, width, height)
799 set_neutral_color_from_signal(state, simple_scene.wb_effect, state.live_signal_num)
800 prepare_overlay_live(state, simple_scene, t, 1.0)
801 return simple_scene.scene
803 elseif state.live_signal_num == STATIC_SIGNAL_NUM then -- Static picture.
810 -- We do not show overlays on the individual preview inputs.
811 -- The M/E preview matches what we'd put live by doing a transition, as always.
812 local show_overlay = false
813 if num == 1 then -- Preview.
814 if state.preview_signal_num == OVERLAY_SIGNAL_NUM then
815 num = state.live_signal_num + 2
816 show_overlay = not state.overlay_enabled
818 if state.transition_type ~= NO_TRANSITION then
819 num = state.transition_dst_signal + 2
822 num = state.preview_signal_num + 2
823 show_overlay = state.overlay_enabled and simple_signal_has_overlay(state.preview_signal_num)
827 -- Individual preview inputs (usually without overlay).
828 if is_plain_signal(num - 2) then
829 local signal_num = num - 2
830 simple_scene.overlay.overlay_effect:enable_if(show_overlay)
831 simple_scene.input:display(signal_num)
832 set_scale_parameters_if_needed(simple_scene, signals, signal_num, width, height)
833 set_neutral_color_from_signal(state, simple_scene.wb_effect, signal_num)
834 prepare_overlay_static(simple_scene, t)
835 return simple_scene.scene
837 if num == SBS_SIGNAL_NUM + 2 then
838 sbs_scene.input0.overlay.overlay_effect:enable_if(show_overlay)
839 prepare_sbs_scene(state, sbs_scene, 0.0, NO_TRANSITION, 0, SBS_SIGNAL_NUM, width, height, input_resolution)
840 return sbs_scene.scene
842 if num == STATIC_SIGNAL_NUM + 2 then
845 if num == OVERLAY_SIGNAL_NUM + 2 then
846 return overlay_scene_lq
850 -- This is broken, of course (even for positive numbers), but Lua doesn't give us access to real rounding.
852 return math.floor(x + 0.5)
855 function lerp(a, b, t)
856 return a + (b - a) * t
859 function lerp_pos(a, b, t)
861 x0 = lerp(a.x0, b.x0, t),
862 y0 = lerp(a.y0, b.y0, t),
863 x1 = lerp(a.x1, b.x1, t),
864 y1 = lerp(a.y1, b.y1, t)
868 function pos_from_top_left(x, y, width, height, screen_width, screen_height)
869 local xs = screen_width / 1280.0
870 local ys = screen_height / 720.0
874 x1 = round(xs * (x + width)),
875 y1 = round(ys * (y + height))
879 function prepare_sbs_scene(state, scene, t, transition_type, src_signal, dst_signal, screen_width, screen_height, input_resolution)
880 scene.input0.input:display(0)
881 scene.input1.input:display(4)
882 set_neutral_color(scene.input0.wb_effect, state.neutral_colors[1])
883 set_neutral_color(scene.input1.wb_effect, state.neutral_colors[5])
885 -- Both inputs are the same size (true side-by-side).
886 local pos0 = pos_from_top_left(1280 - 616 - 16, 186, 616, 347, screen_width, screen_height)
887 local pos1 = pos_from_top_left(16, 186, 616, 347, screen_width, screen_height)
889 local pos_fs = { x0 = 0, y0 = 0, x1 = screen_width, y1 = screen_height }
891 if transition_type == NO_TRANSITION then
893 affine_param = { sx = 1.0, sy = 1.0, tx = 0.0, ty = 0.0 } -- Identity.
895 -- Zooming to/from SBS view into or out of a single view.
896 assert(transition_type == ZOOM_TRANSITION)
898 if src_signal == SBS_SIGNAL_NUM then
902 assert(dst_signal == SBS_SIGNAL_NUM)
907 if signal == INPUT0_SIGNAL_NUM then
908 affine_param = find_affine_param(pos0, lerp_pos(pos0, pos_fs, real_t))
909 elseif signal == INPUT4_SIGNAL_NUM then
910 affine_param = find_affine_param(pos1, lerp_pos(pos1, pos_fs, real_t))
914 -- NOTE: input_resolution is not 1-indexed, unlike usual Lua arrays.
915 place_rectangle_with_affine(scene.input0, pos0, affine_param, screen_width, screen_height, input_resolution[0].width, input_resolution[0].height)
916 place_rectangle_with_affine(scene.input1, pos1, affine_param, screen_width, screen_height, input_resolution[1].width, input_resolution[1].height)
919 -- Find the transformation that changes the first rectangle to the second one.
920 function find_affine_param(a, b)
921 local sx = (b.x1 - b.x0) / (a.x1 - a.x0)
922 local sy = (b.y1 - b.y0) / (a.y1 - a.y0)
926 tx = b.x0 - a.x0 * sx,
927 ty = b.y0 - a.y0 * sy
931 function place_rectangle_with_affine(input, pos, aff, screen_width, screen_height, input_width, input_height)
932 local x0 = pos.x0 * aff.sx + aff.tx
933 local x1 = pos.x1 * aff.sx + aff.tx
934 local y0 = pos.y0 * aff.sy + aff.ty
935 local y1 = pos.y1 * aff.sy + aff.ty
937 place_rectangle(input, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height)
940 function place_rectangle(input, x0, y0, x1, y1, screen_width, screen_height, input_width, input_height, hq)
941 input.padding_effect:set_int("width", screen_width)
942 input.padding_effect:set_int("height", screen_height)
945 if x0 > screen_width or x1 < 0.0 or y0 > screen_height or y1 < 0.0 then
946 input.resample_switcher:choose(ResizeEffect) -- Low-quality resizing.
947 input.resize_effect:set_int("width", 1)
948 input.resize_effect:set_int("height", 1)
949 input.padding_effect:set_int("left", screen_width + 100)
950 input.padding_effect:set_int("top", screen_height + 100)
961 srcx0 = -x0 / (x1 - x0)
965 srcy0 = -y0 / (y1 - y0)
968 if x1 > screen_width then
969 srcx1 = (screen_width - x0) / (x1 - x0)
972 if y1 > screen_height then
973 srcy1 = (screen_height - y0) / (y1 - y0)
978 -- High-quality resampling.
979 input.resample_switcher:choose(ResampleEffect)
981 local x_subpixel_offset = x0 - math.floor(x0)
982 local y_subpixel_offset = y0 - math.floor(y0)
984 -- Resampling must be to an integral number of pixels. Round up,
985 -- and then add an extra pixel so we have some leeway for the border.
986 local width = math.ceil(x1 - x0) + 1
987 local height = math.ceil(y1 - y0) + 1
988 input.resample_effect:set_int("width", width)
989 input.resample_effect:set_int("height", height)
991 -- Correct the discrepancy with zoom. (This will leave a small
992 -- excess edge of pixels and subpixels, which we'll correct for soon.)
993 local zoom_x = (x1 - x0) / (width * (srcx1 - srcx0))
994 local zoom_y = (y1 - y0) / (height * (srcy1 - srcy0))
995 input.resample_effect:set_float("zoom_x", zoom_x)
996 input.resample_effect:set_float("zoom_y", zoom_y)
997 input.resample_effect:set_float("zoom_center_x", 0.0)
998 input.resample_effect:set_float("zoom_center_y", 0.0)
1000 -- Padding must also be to a whole-pixel offset.
1001 input.padding_effect:set_int("left", math.floor(x0))
1002 input.padding_effect:set_int("top", math.floor(y0))
1004 -- Correct _that_ discrepancy by subpixel offset in the resampling.
1005 input.resample_effect:set_float("left", srcx0 * input_width - x_subpixel_offset / zoom_x)
1006 input.resample_effect:set_float("top", srcy0 * input_height - y_subpixel_offset / zoom_y)
1008 -- Finally, adjust the border so it is exactly where we want it.
1009 input.padding_effect:set_float("border_offset_left", x_subpixel_offset)
1010 input.padding_effect:set_float("border_offset_right", x1 - (math.floor(x0) + width))
1011 input.padding_effect:set_float("border_offset_top", y_subpixel_offset)
1012 input.padding_effect:set_float("border_offset_bottom", y1 - (math.floor(y0) + height))
1014 -- Lower-quality simple resizing.
1015 input.resample_switcher:choose(ResizeEffect)
1017 local width = round(x1 - x0)
1018 local height = round(y1 - y0)
1019 input.resize_effect:set_int("width", width)
1020 input.resize_effect:set_int("height", height)
1022 -- Padding must also be to a whole-pixel offset.
1023 input.padding_effect:set_int("left", math.floor(x0))
1024 input.padding_effect:set_int("top", math.floor(y0))
1026 -- No subpixel stuff.
1027 input.padding_effect:set_float("border_offset_left", 0.0)
1028 input.padding_effect:set_float("border_offset_right", 0.0)
1029 input.padding_effect:set_float("border_offset_top", 0.0)
1030 input.padding_effect:set_float("border_offset_bottom", 0.0)
1034 function prepare_overlay_live(state, scene, t, extra_alpha_factor)
1035 if scene.overlay then
1036 local tt = calc_fade_progress(t, state.overlay_transition_start, state.overlay_transition_end)
1037 local overlay_alpha = state.overlay_alpha_src + tt * (state.overlay_alpha_dst - state.overlay_alpha_src)
1038 overlay_alpha = overlay_alpha * extra_alpha_factor
1039 --print("overlay_alpha=" .. overlay_alpha .. " [" .. state.overlay_alpha_src .. "," .. state.overlay_alpha_dst .. "]@" .. tt)
1040 if t > state.overlay_transition_end and state.overlay_alpha_dst == 0.0 then
1041 state.overlay_enabled = false -- Takes effect next frame.
1042 -- print("Turning off overlay")
1044 scene.overlay.multiply_effect:set_vec4("factor", overlay_alpha, overlay_alpha, overlay_alpha, overlay_alpha)
1048 function prepare_overlay_static(scene)
1049 if scene.overlay then
1050 scene.overlay.multiply_effect:set_vec4("factor", 1.0, 1.0, 1.0, 1.0)
1054 function set_neutral_color(effect, color)
1055 effect:set_vec3("neutral_color", color[1], color[2], color[3])
1058 function set_neutral_color_from_signal(state, effect, signal)
1059 if is_plain_signal(signal) then
1060 set_neutral_color(effect, state.neutral_colors[signal - INPUT0_SIGNAL_NUM + 1])
1064 function calc_zoom_progress(t)
1065 if t < state.transition_start then
1067 elseif t > state.transition_end then
1070 local tt = (t - state.transition_start) / (state.transition_end - state.transition_start)
1072 return math.sin(tt * 3.14159265358 * 0.5)
1076 function calc_fade_progress(t, transition_start, transition_end)
1077 local tt = (t - transition_start) / (transition_end - transition_start)
1080 elseif tt > 1.0 then
1084 -- Make the fade look maybe a tad more natural, by pumping it
1085 -- through a sigmoid function.
1086 tt = 10.0 * tt - 5.0
1087 tt = 1.0 / (1.0 + math.exp(-tt))
1093 { "Reload overlay", reload_cef },
1094 { "Disconnect IPTV", disconnect_iptv }