"Compositor" background showing during transition

Hi

My setup and goal
I’m on Ubuntu Server minimal install using kmssink and multiple video streams being fed into a compositor, and their audio to an audiomixer.
I need to be able to switch which video is shown, perserve sync between videos, and do this without stutter/flash.

Issue
The current issue is, when I switch which video has alpha=1.0, sometimes a checkerboard pattern appears. I have confirmed this is from the compositor, by changing compositor background to black, this results in the same experience, sometimes a black screen appears during switch.
I had another attempt, where both videos always had alpha=1.0, but I instead just changed the zorder to define which video was shown. Here, I also saw the checkerboard pattern (I’m unsure if I tried having compositor background black for this approach).

I’m unsure what the norm is on these forums, but this is my code. I have tried setting GST_DEBUG to 4, but nothing was printed when the checkerboard pattern appeared.

My “controls” import is a custom script that handles keyboard inputs, I doubt it is relevant for this issue.
I had a previous version (AI generated, decided to drop it and actually learn GStreamer) that transitioned perfectly fine, but had stuttery playback during regular playback (not on switches). The code I pasted here, has perfect playback, but checkerboard when switching.

Any help is greatly appreciated.

import os
os.environ['GST_DEBUG'] = '0'

import gi
gi.require_version("Gst", "1.0")
from gi.repository import Gst, GLib

import time
from controls import create_input_handler, InputType
import logging
import sys

logging.basicConfig(level=logging.DEBUG, format="[%(name)s] [%(levelname)8s] - %(message)s")
logger = logging.getLogger(__name__)

Gst.init(None)

# THIS FILE HAS 2 .mp4 INPUTs and 2 .wav INPUTs. IT USES AUDIOMIXER. COMPOSITOR IS USED FOR THE VIDEO
# CONTROLS HAS BEEN ADDED, THEY SWITCH SCENE AND AUDIO
# TEST SUCCESS: NO STUTTER, CLEAN SWITCH, SOMETIMES CHECKERBOARD WHEN SWITCHING

class VideoHandler:

  def __init__(self):
    self.current_index = 0
    self.v_source0 = Gst.ElementFactory.make("uridecodebin", "vsource0")
    self.a_source0 = Gst.ElementFactory.make("uridecodebin", "asource0")
    
    self.v_source1 = Gst.ElementFactory.make("uridecodebin", "vsource1")
    self.a_source1 = Gst.ElementFactory.make("uridecodebin", "asource1")

    self.v_compositor = Gst.ElementFactory.make("compositor", "compositor")
    self.v_convert = Gst.ElementFactory.make("videoconvert", "videoconvert")
    
    self.a_mixer = Gst.ElementFactory.make("audiomixer", "audiomixer")
    self.a_convert = Gst.ElementFactory.make("audioconvert", "audioconvert")
    self.a_resample = Gst.ElementFactory.make("audioresample", "audioresample")
    self.a_sink = Gst.ElementFactory.make("autoaudiosink", "audiosink")

    self.v_queue = Gst.ElementFactory.make("queue", "videoqueue")
    self.v_queue.set_property("max-size-buffers", 0)
    self.v_queue.set_property("max-size-bytes", 0)
    self.v_queue.set_property("max-size-time", 80000000)

    self.v_sink = Gst.ElementFactory.make("kmssink", "videosink")
    self.v_sink.set_property("skip-vsync", True)
    self.v_sink.set_property("sync", True)
    self.v_sink.set_property("can-scale", False)

    self.pipeline = Gst.Pipeline.new("switcher-pipeline")

    if not self.v_source0 or not self.a_source0 or not self.v_source1 or not self.a_source1 or not self.a_mixer or not self.a_convert or not self.a_resample or not self.a_sink or not self.v_compositor or not self.v_convert or not self.v_queue or not self.v_sink:
      logger.error("Failed to create one or more elements.")
      sys.exit(1)

    for element in [self.v_source0, self.a_source0, self.v_source1, self.a_source1, self.a_mixer, self.a_convert, self.a_resample, self.a_sink, self.v_compositor, self.v_convert, self.v_queue, self.v_sink]:
      self.pipeline.add(element)
      element.sync_state_with_parent()

    # audiomixer -> audio_convert -> resample -> audio_sink
    if not self.a_mixer.link(self.a_convert) or not self.a_convert.link(self.a_resample) or not self.a_resample.link(self.a_sink):
      logger.error("Not all audio elements could be linked")
      sys.exit(1)

    # compositor -> video_convert -> queue -> video_sink
    if not self.v_compositor.link(self.v_convert) or not self.v_convert.link(self.v_queue) or not self.v_queue.link(self.v_sink):
      logger.error("Failed to link video convert to sink")

    self.v_source_pad0 = self.v_compositor.get_request_pad("sink_%u")
    self.v_source_pad1 = self.v_compositor.get_request_pad("sink_%u")
    self.v_pads = [self.v_source_pad0, self.v_source_pad1]

    self.v_source0.set_property("uri", "file:///home/user/video0.mp4")
    self.v_source0.connect("pad-added", self._video_pad_added_handler)

    self.v_source1.set_property("uri", "file:///home/user/video1.mp4")
    self.v_source1.connect("pad-added", self._video_pad_added_handler)


    self.a_source_pad0 = self.a_mixer.get_request_pad("sink_%u")
    self.a_source_pad1 = self.a_mixer.get_request_pad("sink_%u")
    self.a_pads = [self.a_source_pad0, self.a_source_pad1]

    self.a_source0.set_property("uri", "file:///home/user/audio0.wav")
    self.a_source0.connect("pad-added", self._audio_pad_added_handler)

    self.a_source1.set_property("uri", "file:///home/user/audio1.wav")
    self.a_source1.connect("pad-added", self._audio_pad_added_handler)

    self.input_handler = create_input_handler(InputType.KEYBOARD, self)
    self.input_handler.start()

    logger.info("Im gonna set pipeline to playing now...")

    ret = self.pipeline.set_state(Gst.State.PLAYING)
    if ret == Gst.StateChangeReturn.FAILURE:
      logger.error("Failed to set pipeline to playing state")
      sys.exit(1)

    logger.info("Pipeline should be playing now...")

    self.loop = GLib.MainLoop()
    bus = self.pipeline.get_bus()
    bus.add_signal_watch()
    bus.connect("message", self._on_bus_message)
    
    self.switch_to(self.current_index)

    try:
        self.loop.run()
    except KeyboardInterrupt:
        logger.info("GLib Loop Interrupted")
    finally:
        self.quit()

  def _on_bus_message(self, bus, msg):
    if msg:
      if msg.type == Gst.MessageType.ERROR:
        err, debug_info = msg.parse_error()
        logger.error(f"Error received from element {msg.src.get_name()}: {err.message}")
        logger.error(f"Debugging information: {debug_info if debug_info else 'none'}")
        self.quit()
      elif msg.type == Gst.MessageType.EOS:
        logger.info("End-Of-Stream reached.")
        self.quit()
      elif msg.type == Gst.MessageType.STATE_CHANGED:
        # We are only interested in state-changed messages from the pipeline
        if msg.src == self.pipeline:
          old_state, new_state, pending_state = msg.parse_state_changed()
          logger.info(f"Pipeline state changed from {old_state} to {new_state}")
      else:
        # We should not reach here
        logger.error("Unexpected message received.")

  # 'src' is the element that owns the given pad
  # 'new_pad' is the specific pad that was added. This is usually twice per src, a pad for video and a pad for audio
  def _video_pad_added_handler(self, src, new_pad):
    
    logger.info(f"Video - Attempting to dynamically select pad: {int(src.name[-1])}")
    v_sink_pad = self.v_pads[int(src.name[-1])]
    logger.info(f"Video received new pad '{new_pad.name}' from '{src.name}'")

    if v_sink_pad.is_linked():
      logger.info("Video already linked, ignoring")
      sys.exit()

    new_pad_caps = new_pad.get_current_caps()
    new_pad_struct = new_pad_caps.get_structure(0)
    new_pad_type = new_pad_struct.get_name()

    if new_pad_type.startswith("video/x-raw"):
      logger.info("Video attempting to link to audio sink")
      ret = new_pad.link(v_sink_pad)
      if ret != Gst.PadLinkReturn.OK:
        logger.error(f"Video - Type is '{new_pad_type}' but link failed")
      else:
        logger.info(f"Video link successful of type '{new_pad_type}'")
    else:
      logger.warning(f"Video pad handler received non-video '{new_pad_type}'")
  
  def _audio_pad_added_handler(self, src, new_pad):
    
    logger.info(f"Audio - Attempting to dynamically select pad: {int(src.name[-1])}")
    a_sink_pad = self.a_pads[int(src.name[-1])]
    logger.info(f"Received new pad '{new_pad.name}' from '{src.name}'")

    if a_sink_pad.is_linked():
      logger.info("Audio already linked, ignoring")
      sys.exit()

    new_pad_caps = new_pad.get_current_caps()
    new_pad_struct = new_pad_caps.get_structure(0)
    new_pad_type = new_pad_struct.get_name()

    if new_pad_type.startswith("audio/x-raw"):
      logger.info("Attempting to link to audio sink")
      ret = new_pad.link(a_sink_pad)
      if ret != Gst.PadLinkReturn.OK:
        logger.error(f"Type is '{new_pad_type}' but link failed")
      else:
        logger.info(f"Audio link successful of type '{new_pad_type}'")
    else:
      logger.warning(f"Audio pad handler received non-audio '{new_pad_type}'")

# ------- SWITCHING --------

  def switch_to(self, index):
    self.current_index = index
    logger.info(f"Actual switch to {self.current_index}")
    self.v_pads[index].set_property("alpha", 1.0)
    self.a_pads[index].set_property("volume", 1.0)
    
    for i in range(len(self.v_pads)):
      logger.info(f"i is {i}")
      if i == self.current_index:
        logger.info(f"Skipping {i}")
        continue
      self.v_pads[i].set_property("alpha", 0.0)
      self.a_pads[i].set_property("volume", 0.0)

# -------- CONTROLS --------

  def quit(self):
    try:
      self.pipeline.set_state(Gst.State.NULL)
    except:
      logger.info("Pipeline to NULL state threw exception")

    try:
      self.loop.quit()
    except:
      logger.info("Loop quit threw exception")
      pass

    try:
      self.input_handler.stop()
    except:
      logger.info("Input handler stop threw exception")

video = VideoHandler()

To help avoid the need for gapless transition (which is simply more complex), I would suggest to try with pad properties `max-last-buffer-repeat=-1` and `repeat-after-eos=1`, this will ensure the background is never show during gaps or after EOS.

Thanks for the quick reply :slight_smile:

I have actually never reached EOS, since I quit when I notice the checkerboard pattern, so I am unsure if EOS is problematic as well, I haven’t even implemented restart logic yet :stuck_out_tongue:

I adjusted my compositor pads like this:

    self.v_source_pad0 = self.v_compositor.get_request_pad("sink_%u")
    self.v_source_pad0.set_property("max-last-buffer-repeat", 0)
    self.v_source_pad0.set_property("repeat-after-eos", 1)
    
    self.v_source_pad1 = self.v_compositor.get_request_pad("sink_%u")
    self.v_source_pad1.set_property("max-last-buffer-repeat", 0)
    self.v_source_pad1.set_property("repeat-after-eos", 1)

It seems `max-last-buffer-repeat` doesn’t support negative numbers, so I tried with “0”. I still got the checkerboard background occasionally. I’m suspecting if it might be the same issue as this guy had: Losing background after seek in using compositor - #5 by Honey_Patouceul
He sadly never posted a solution.

I know its a big ask, but if I sent the AI generated code that somehow makes it work, but has stutters in the video, would you give it a look? I have tried reading it over and copying the pipeline architecture to the best of my ability, but without improvements.

The doc is miss-leading, by -1 it mean MAXUINT64, or `GST_CLOCK_TIME_NONE`. As for the AI code, you can push it somewhere, hopefully it didn’t go too wild.

Its been a few hours, but I think I got an error from setting it at -1, but I am unsure.

I am also glad to let you know that my issue seems to have been fixed. I asked Claude (one too many times).
The answer that gave the solution, suggested me to:

  • Set the compositors background to black (which in my head shouldn’t make a huge difference)
  • Create a queue with ~80ms buffer right before each compositor input pad
  • Set compositor to “min-upstream-latency=40ms”

All these things together, seems to have solved it, though my guess is that the queue doing a big part.

I thank you very much for offering to read the AI code, but I’ll refrain from posting it since my issue has been solved. If you are interested in reading it (which I doubt, but you never know), or think it could benefit people in the future who has the same issue I had, I’ll see if I can find somewhere to post it.

Hi,

Another tip on top of using ‘background=black’.

When fading between compositor pads using alpha is to do what the industry call a video transition where each pad meets half way where both pads alpha are at 0.5 together in the timed transition. Say the transition is 1000ms each cross at exactly 500ms.

Doing it this way there is no chance the background showing through..

I’ve implemented a call back method using Glib in my repeater project to manipulate both alpha on pad inputs into compositor and audiomixer (volume).

Doing so you get a professional cross fade. Although not important for audio transition I use log maths to calculate a log transition for audio into audiomixer since human ear operates on logarithmic value where the audio transition is linear 0.0-1.0 in streamer land.

And another solution would be to use the samples-selected signal to configure all the properties atomically and for each specific output buffer consistently.

Hi

I am familiar with the crossfade. I may be missing something, but I fail to see how it would help to crossfade compared to doing an instant value-change of which pad has 1.0 alpha value. The crossfade may of course look nicer.

Thanks for the heads up on the audio.

I’m not familiar with samples-selected and I can’t really find any documentation on it.

I tried here: https://gstreamer.freedesktop.org/documentation/plugin-development/basics/signals.html?gi-language=python but the link sends me to https://library.gnome.org/devel/gobject/stable/ which is 404

See https://gstreamer.freedesktop.org/documentation/base/gstaggregator.html#GstAggregator::samples-selected