1

Background

I'm trying to create a media player that can run even if it has no window handle to render the video into. (So you can't see the video, but can still hear sound.)

I thought I could achieve this with an output-selector: When no window handle is available, the output is redirected to a fakesink.

Code

(In order to reduce the amount of code, there is no audio playback.)

import sys

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gst', '1.0')
gi.require_version('GstVideo', '1.0')
from gi.repository import Gtk, GLib, Gst, GstVideo
Gst.init(None)


if sys.platform == 'win32':
    import ctypes

    PyCapsule_GetPointer = ctypes.pythonapi.PyCapsule_GetPointer
    PyCapsule_GetPointer.restype = ctypes.c_void_p
    PyCapsule_GetPointer.argtypes = [ctypes.py_object]

    gdkdll = ctypes.CDLL('libgdk-3-0.dll')
    gdkdll.gdk_win32_window_get_handle.argtypes = [ctypes.c_void_p]
    
    def get_window_handle(widget):
        window = widget.get_window()
        if not window.ensure_native():
            raise Exception('video playback requires a native window')
        
        window_gpointer = PyCapsule_GetPointer(window.__gpointer__, None)
        handle = gdkdll.gdk_win32_window_get_handle(window_gpointer)
        
        return handle
else:
    def get_window_handle(widget):
        return widget.get_window().get_xid()


class VideoPlayer:
    def __init__(self, canvas):
        self._canvas = canvas
        self._setup_pipeline()
    
    def _setup_pipeline(self):
        # The element with the set_window_handle function will be stored here
        self._video_overlay = None
        
        self._pipeline = Gst.ElementFactory.make('pipeline', 'pipeline')
        src = Gst.ElementFactory.make('videotestsrc', 'src')
        output_selector = Gst.ElementFactory.make('output-selector', 'output-selector')
        fake_video_sink = Gst.ElementFactory.make('fakevideosink', 'fakevideosink')
        video_convert = Gst.ElementFactory.make('videoconvert', 'videoconvert')
        auto_video_sink = Gst.ElementFactory.make('autovideosink', 'autovideosink')

        output_selector.props.resend_latest = True
        
        for element in [src, output_selector, fake_video_sink, video_convert, auto_video_sink]:
            self._pipeline.add(element)
        
        src.link(output_selector)
        
        # Connect the output-selector to the fakesink and the videoconvert.
        # By default, the fakesink is selected.
        self._fake_video_sink_source_pad = output_selector.get_request_pad("src_%u")
        sink_pad = fake_video_sink.get_static_pad('sink')
        self._fake_video_sink_source_pad.link(sink_pad)
        output_selector.props.active_pad = self._fake_video_sink_source_pad

        self._video_convert_source_pad = output_selector.get_request_pad("src_%u")
        sink_pad = video_convert.get_static_pad('sink')
        self._video_convert_source_pad.link(sink_pad)

        video_convert.link(auto_video_sink)
        
        self._setup_signal_handlers()
    
    def _setup_signal_handlers(self):
        self._canvas.connect('realize', self._on_canvas_realize)
        self._canvas.connect('unrealize', self._on_canvas_unrealize)
        
        bus = self._pipeline.get_bus()
        bus.enable_sync_message_emission()
        bus.connect('sync-message::element', self._on_sync_element_message)
    
    def _on_sync_element_message(self, bus, message):
        if message.get_structure().get_name() == 'prepare-window-handle':
            self._video_overlay = message.src
            self._video_overlay.set_window_handle(self._canvas_window_handle)
    
    def _on_canvas_realize(self, canvas):
        self._canvas_window_handle = get_window_handle(self._canvas)
        output_selector = self._pipeline.get_by_name('output-selector')
        
        # If the fakesink is currently selected, activate the videoconvert
        # so the video will start rendering.
        # If the videoconvert is currently selected, update its window handle.
        if output_selector.props.active_pad is self._fake_video_sink_source_pad:
            output_selector.props.active_pad = self._video_convert_source_pad
        elif self._video_overlay is not None:
            self._video_overlay.set_window_handle(self._canvas_window_handle)
        
    def _on_canvas_unrealize(self, canvas):
        # Stop rendering; redirect output to the fakesink
        output_selector = self._pipeline.get_by_name('output-selector')
        output_selector.props.active_pad = self._fake_video_sink_source_pad
    
    def start(self):
        self._pipeline.set_state(Gst.State.PLAYING)
    

window = Gtk.Window()
canvas = Gtk.DrawingArea()
window.add(canvas)

player = VideoPlayer(canvas)
canvas.connect('realize', lambda *_: player.start())

window.connect('destroy', Gtk.main_quit)
window.show_all()
Gtk.main()

Problem

The video doesn't play. All I can see is a static image. For some reason, the pipeline never reaches the PLAYING state.

Please tell me what's wrong with my code, or suggest an alternative solution.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • I found [this code](https://github.com/GStreamer/gst-plugins-base/blob/ca7a964fb120f680b6fc89b68960287abe2bf2de/tests/icles/output-selector-test.c#L69-L73) where they set `sync=False` and `async=False` on every element added to the autovideosink, but doing that didn't make a difference for me. – Aran-Fey Dec 28 '21 at 14:42
  • I realized that an `output-selector` is a bad solution, because the program would waste resources decoding the video stream even when the video won't be displayed. Instead, I'm now trying to dynamically alter the pipeline - when a window handle is obtained, the source is linked to the `videoconvert`, and when the handle becomes invalid, they're unlinked again. I've figured out the linking part, but the unlinking is giving me trouble... – Aran-Fey Dec 28 '21 at 18:06

0 Answers0