gkralik / python-gst-tutorial

Python version of GStreamer tutorials (http://docs.gstreamer.com/display/GstSDK/Tutorials)
113 stars 42 forks source link

code update proposal #2

Open spiderdab opened 4 years ago

spiderdab commented 4 years ago

Hi, Here there is the same code with some update to make it work at first run, to be precise the video url was wrong, and some Gtk functions where deprecated.

here's my working code: `#!/usr/bin/env python3

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

class Player(object):

def __init__(self):
    # initialize GTK
    Gtk.init(sys.argv)

    # initialize GStreamer
    Gst.init(sys.argv)

    self.state = Gst.State.NULL
    self.duration = Gst.CLOCK_TIME_NONE
    self.playbin = Gst.ElementFactory.make("playbin", "playbin")
    if not self.playbin:
        print("ERROR: Could not create playbin.")
        sys.exit(1)

    # set up URI
    self.playbin.set_property(
        "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")

    # connect to interesting signals in playbin
    self.playbin.connect("video-tags-changed", self.on_tags_changed)
    self.playbin.connect("audio-tags-changed", self.on_tags_changed)
    self.playbin.connect("text-tags-changed", self.on_tags_changed)

    # create the GUI
    self.build_ui()

    # instruct the bus to emit signals for each received message
    # and connect to the interesting signals
    bus = self.playbin.get_bus()
    bus.add_signal_watch()
    bus.connect("message::error", self.on_error)
    bus.connect("message::eos", self.on_eos)
    bus.connect("message::state-changed", self.on_state_changed)
    bus.connect("message::application", self.on_application_message)

# set the playbin to PLAYING (start playback), register refresh callback
# and start the GTK main loop
def start(self):
    # start playing
    ret = self.playbin.set_state(Gst.State.PLAYING)
    if ret == Gst.StateChangeReturn.FAILURE:
        print("ERROR: Unable to set the pipeline to the playing state")
        sys.exit(1)

    # register a function that GLib will call every second
    GLib.timeout_add_seconds(1, self.refresh_ui)

    # start the GTK main loop. we will not regain control until
    # Gtk.main_quit() is called
    Gtk.main()

    # free resources
    self.cleanup()

# set the playbin state to NULL and remove the reference to it
def cleanup(self):
    if self.playbin:
        self.playbin.set_state(Gst.State.NULL)
        self.playbin = None

def build_ui(self):
    main_window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
    main_window.connect("delete-event", self.on_delete_event)

    video_window = Gtk.DrawingArea.new()
    video_window.connect("realize", self.on_realize)
    video_window.connect("draw", self.on_draw)

    play_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PLAY, 12))
    play_button.connect("clicked", self.on_play)

    pause_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, 12))
    pause_button.connect("clicked", self.on_pause)

    stop_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_STOP, 12))
    stop_button.connect("clicked", self.on_stop)

    self.slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
    self.slider.set_draw_value(False)
    self.slider_update_signal_id = self.slider.connect(
        "value-changed", self.on_slider_changed)

    self.streams_list = Gtk.TextView.new()
    self.streams_list.set_editable(False)

    controls = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
    controls.pack_start(play_button, False, False, 2)
    controls.pack_start(pause_button, False, False, 2)
    controls.pack_start(stop_button, False, False, 2)
    controls.pack_start(self.slider, True, True, 0)

    main_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
    main_hbox.pack_start(video_window, True, True, 0)
    main_hbox.pack_start(self.streams_list, False, False, 2)

    main_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
    main_box.pack_start(main_hbox, True, True, 0)
    main_box.pack_start(controls, False, False, 0)

    main_window.add(main_box)
    main_window.set_default_size(640, 480)
    main_window.show_all()

# this function is called when the GUI toolkit creates the physical window
# that will hold the video
# at this point we can retrieve its handler and pass it to GStreamer
# through the XOverlay interface
def on_realize(self, widget):
    window = widget.get_window()
    window_handle = window.get_xid()

    # pass it to playbin, which implements XOverlay and will forward
    # it to the video sink
    self.playbin.set_window_handle(window_handle)
    # self.playbin.set_xwindow_id(window_handle)

# this function is called when the PLAY button is clicked
def on_play(self, button):
    self.playbin.set_state(Gst.State.PLAYING)
    pass

# this function is called when the PAUSE button is clicked
def on_pause(self, button):
    self.playbin.set_state(Gst.State.PAUSED)
    pass

# this function is called when the STOP button is clicked
def on_stop(self, button):
    self.playbin.set_state(Gst.State.READY)
    pass

# this function is called when the main window is closed
def on_delete_event(self, widget, event):
    self.on_stop(None)
    Gtk.main_quit()

# this function is called every time the video window needs to be
# redrawn. GStreamer takes care of this in the PAUSED and PLAYING states.
# in the other states we simply draw a black rectangle to avoid
# any garbage showing up
def on_draw(self, widget, cr):
    if self.state < Gst.State.PAUSED:
        allocation = widget.get_allocation()

        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, allocation.width, allocation.height)
        cr.fill()

    return False

# this function is called when the slider changes its position.
# we perform a seek to the new position here
def on_slider_changed(self, range):
    value = self.slider.get_value()
    self.playbin.seek_simple(Gst.Format.TIME,
                             Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                             value * Gst.SECOND)

# this function is called periodically to refresh the GUI
def refresh_ui(self):
    current = -1

    # we do not want to update anything unless we are in the PAUSED
    # or PLAYING states
    if self.state < Gst.State.PAUSED:
        return True

    # if we don't know it yet, query the stream duration
    if self.duration == Gst.CLOCK_TIME_NONE:
        ret, self.duration = self.playbin.query_duration(Gst.Format.TIME)
        if not ret:
            print("ERROR: Could not query current duration")
        else:
            # set the range of the slider to the clip duration (in seconds)
            self.slider.set_range(0, self.duration / Gst.SECOND)

    ret, current = self.playbin.query_position(Gst.Format.TIME)
    if ret:
        # block the "value-changed" signal, so the on_slider_changed
        # callback is not called (which would trigger a seek the user
        # has not requested)
        self.slider.handler_block(self.slider_update_signal_id)

        # set the position of the slider to the current pipeline position
        # (in seconds)
        self.slider.set_value(current / Gst.SECOND)

        # enable the signal again
        self.slider.handler_unblock(self.slider_update_signal_id)

    return True

# this function is called when new metadata is discovered in the stream
def on_tags_changed(self, playbin, stream):
    # we are possibly in a GStreamer working thread, so we notify
    # the main thread of this event through a message in the bus
    self.playbin.post_message(
        Gst.Message.new_application(
            self.playbin,
            Gst.Structure.new_empty("tags-changed")))

# this function is called when an error message is posted on the bus
def on_error(self, bus, msg):
    err, dbg = msg.parse_error()
    print("ERROR:", msg.src.get_name(), ":", err.message)
    if dbg:
        print("Debug info:", dbg)

# this function is called when an End-Of-Stream message is posted on the bus
# we just set the pipeline to READY (which stops playback)
def on_eos(self, bus, msg):
    print("End-Of-Stream reached")
    self.playbin.set_state(Gst.State.READY)

# this function is called when the pipeline changes states.
# we use it to keep track of the current state
def on_state_changed(self, bus, msg):
    old, new, pending = msg.parse_state_changed()
    if not msg.src == self.playbin:
        # not from the playbin, ignore
        return

    self.state = new
    print("State changed from {0} to {1}".format(
        Gst.Element.state_get_name(old), Gst.Element.state_get_name(new)))

    if old == Gst.State.READY and new == Gst.State.PAUSED:
        # for extra responsiveness we refresh the GUI as soons as
        # we reach the PAUSED state
        self.refresh_ui()

# extract metadata from all the streams and write it to the text widget
# in the GUI
def analyze_streams(self):
    # clear current contents of the widget
    buffer = self.streams_list.get_buffer()
    buffer.set_text("")

    # read some properties
    nr_video = self.playbin.get_property("n-video")
    nr_audio = self.playbin.get_property("n-audio")
    nr_text = self.playbin.get_property("n-text")

    for i in range(nr_video):
        tags = None
        # retrieve the stream's video tags
        tags = self.playbin.emit("get-video-tags", i)
        if tags:
            buffer.insert_at_cursor("video stream {0}\n".format(i))
            _, str = tags.get_string(Gst.TAG_VIDEO_CODEC)
            buffer.insert_at_cursor(
                "  codec: {0}\n".format(
                    str or "unknown"))

    for i in range(nr_audio):
        tags = None
        # retrieve the stream's audio tags
        tags = self.playbin.emit("get-audio-tags", i)
        if tags:
            buffer.insert_at_cursor("\naudio stream {0}\n".format(i))
            ret, str = tags.get_string(Gst.TAG_AUDIO_CODEC)
            if ret:
                buffer.insert_at_cursor(
                    "  codec: {0}\n".format(
                        str or "unknown"))

            ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
            if ret:
                buffer.insert_at_cursor(
                    "  language: {0}\n".format(
                        str or "unknown"))

            ret, str = tags.get_uint(Gst.TAG_BITRATE)
            if ret:
                buffer.insert_at_cursor(
                    "  bitrate: {0}\n".format(
                        str or "unknown"))

    for i in range(nr_text):
        tags = None
        # retrieve the stream's subtitle tags
        tags = self.playbin.emit("get-text-tags", i)
        if tags:
            buffer.insert_at_cursor("\nsubtitle stream {0}\n".format(i))
            ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
            if ret:
                buffer.insert_at_cursor(
                    "  language: {0}\n".format(
                        str or "unknown"))

# this function is called when an "application" message is posted on the bus
# here we retrieve the message posted by the on_tags_changed callback
def on_application_message(self, bus, msg):
    if msg.get_structure().get_name() == "tags-changed":
        # if the message is the "tags-changed", update the stream info in
        # the GUI
        self.analyze_streams()

if name == 'main': p = Player() p.start()`

ousia commented 2 years ago

@spiderdab, many thanks for your code.

I needed two minor tweaks to make it work.

I post the new version here, just in case it might help.

#!/usr/bin/env python3

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

class Player(object):

    def __init__(self):
        # initialize GTK
        Gtk.init(sys.argv)

        # initialize GStreamer
        Gst.init(sys.argv)

        self.state = Gst.State.NULL
        self.duration = Gst.CLOCK_TIME_NONE
        self.playbin = Gst.ElementFactory.make("playbin", "playbin")
        if not self.playbin:
            print("ERROR: Could not create playbin.")
            sys.exit(1)

        # set up URI
        self.playbin.set_property(
            "uri", "https://www.freedesktop.org/software/gstreamer-sdk/data/media/sintel_trailer-480p.webm")

        # connect to interesting signals in playbin
        self.playbin.connect("video-tags-changed", self.on_tags_changed)
        self.playbin.connect("audio-tags-changed", self.on_tags_changed)
        self.playbin.connect("text-tags-changed", self.on_tags_changed)

        # create the GUI
        self.build_ui()

        # instruct the bus to emit signals for each received message
        # and connect to the interesting signals
        bus = self.playbin.get_bus()
        bus.add_signal_watch()
        bus.connect("message::error", self.on_error)
        bus.connect("message::eos", self.on_eos)
        bus.connect("message::state-changed", self.on_state_changed)
        bus.connect("message::application", self.on_application_message)

    # set the playbin to PLAYING (start playback), register refresh callback
    # and start the GTK main loop
    def start(self):
        # start playing
        ret = self.playbin.set_state(Gst.State.PLAYING)
        if ret == Gst.StateChangeReturn.FAILURE:
            print("ERROR: Unable to set the pipeline to the playing state")
            sys.exit(1)

        # register a function that GLib will call every second
        GLib.timeout_add_seconds(1, self.refresh_ui)

        # start the GTK main loop. we will not regain control until
        # Gtk.main_quit() is called
        Gtk.main()

        # free resources
        self.cleanup()

    # set the playbin state to NULL and remove the reference to it
    def cleanup(self):
        if self.playbin:
            self.playbin.set_state(Gst.State.NULL)
            self.playbin = None

    def build_ui(self):
        main_window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
        main_window.connect("delete-event", self.on_delete_event)

        video_window = Gtk.DrawingArea.new()
        video_window.connect("realize", self.on_realize)
        video_window.connect("draw", self.on_draw)

        play_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PLAY, 12))
        play_button.connect("clicked", self.on_play)

        pause_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, 12))
        pause_button.connect("clicked", self.on_pause)

        stop_button = Gtk.ToolButton.new(Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_STOP, 12))
        stop_button.connect("clicked", self.on_stop)

        self.slider = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 0, 100, 1)
        self.slider.set_draw_value(False)
        self.slider_update_signal_id = self.slider.connect(
            "value-changed", self.on_slider_changed)

        self.streams_list = Gtk.TextView.new()
        self.streams_list.set_editable(False)

        controls = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
        controls.pack_start(play_button, False, False, 2)
        controls.pack_start(pause_button, False, False, 2)
        controls.pack_start(stop_button, False, False, 2)
        controls.pack_start(self.slider, True, True, 0)

        main_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0)
        main_hbox.pack_start(video_window, True, True, 0)
        main_hbox.pack_start(self.streams_list, False, False, 2)

        main_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
        main_box.pack_start(main_hbox, True, True, 0)
        main_box.pack_start(controls, False, False, 0)

        main_window.add(main_box)
        main_window.set_default_size(640, 480)
        main_window.show_all()

    # this function is called when the GUI toolkit creates the physical window
    # that will hold the video
    # at this point we can retrieve its handler and pass it to GStreamer
    # through the XOverlay interface
    def on_realize(self, widget):
        window = widget.get_window()
        window_handle = window.get_xid()

        # pass it to playbin, which implements XOverlay and will forward
        # it to the video sink
        self.playbin.set_window_handle(window_handle)
        # self.playbin.set_xwindow_id(window_handle)

    # this function is called when the PLAY button is clicked
    def on_play(self, button):
        self.playbin.set_state(Gst.State.PLAYING)
        pass

    # this function is called when the PAUSE button is clicked
    def on_pause(self, button):
        self.playbin.set_state(Gst.State.PAUSED)
        pass

    # this function is called when the STOP button is clicked
    def on_stop(self, button):
        self.playbin.set_state(Gst.State.READY)
        pass

    # this function is called when the main window is closed
    def on_delete_event(self, widget, event):
        self.on_stop(None)
        Gtk.main_quit()

    # this function is called every time the video window needs to be
    # redrawn. GStreamer takes care of this in the PAUSED and PLAYING states.
    # in the other states we simply draw a black rectangle to avoid
    # any garbage showing up
    def on_draw(self, widget, cr):
        if self.state < Gst.State.PAUSED:
            allocation = widget.get_allocation()

            cr.set_source_rgb(0, 0, 0)
            cr.rectangle(0, 0, allocation.width, allocation.height)
            cr.fill()

        return False

    # this function is called when the slider changes its position.
    # we perform a seek to the new position here
    def on_slider_changed(self, range):
        value = self.slider.get_value()
        self.playbin.seek_simple(Gst.Format.TIME,
                                 Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
                                 value * Gst.SECOND)

    # this function is called periodically to refresh the GUI
    def refresh_ui(self):
        current = -1

        # we do not want to update anything unless we are in the PAUSED
        # or PLAYING states
        if self.state < Gst.State.PAUSED:
            return True

        # if we don't know it yet, query the stream duration
        if self.duration == Gst.CLOCK_TIME_NONE:
            ret, self.duration = self.playbin.query_duration(Gst.Format.TIME)
            if not ret:
                print("ERROR: Could not query current duration")
            else:
                # set the range of the slider to the clip duration (in seconds)
                self.slider.set_range(0, self.duration / Gst.SECOND)

        ret, current = self.playbin.query_position(Gst.Format.TIME)
        if ret:
            # block the "value-changed" signal, so the on_slider_changed
            # callback is not called (which would trigger a seek the user
            # has not requested)
            self.slider.handler_block(self.slider_update_signal_id)

            # set the position of the slider to the current pipeline position
            # (in seconds)
            self.slider.set_value(current / Gst.SECOND)

            # enable the signal again
            self.slider.handler_unblock(self.slider_update_signal_id)

        return True

    # this function is called when new metadata is discovered in the stream
    def on_tags_changed(self, playbin, stream):
        # we are possibly in a GStreamer working thread, so we notify
        # the main thread of this event through a message in the bus
        self.playbin.post_message(
            Gst.Message.new_application(
                self.playbin,
                Gst.Structure.new_empty("tags-changed")))

    # this function is called when an error message is posted on the bus
    def on_error(self, bus, msg):
        err, dbg = msg.parse_error()
        print("ERROR:", msg.src.get_name(), ":", err.message)
        if dbg:
            print("Debug info:", dbg)

    # this function is called when an End-Of-Stream message is posted on the bus
    # we just set the pipeline to READY (which stops playback)
    def on_eos(self, bus, msg):
        print("End-Of-Stream reached")
        self.playbin.set_state(Gst.State.READY)

    # this function is called when the pipeline changes states.
    # we use it to keep track of the current state
    def on_state_changed(self, bus, msg):
        old, new, pending = msg.parse_state_changed()
        if not msg.src == self.playbin:
            # not from the playbin, ignore
            return

        self.state = new
        print("State changed from {0} to {1}".format(
            Gst.Element.state_get_name(old), Gst.Element.state_get_name(new)))

        if old == Gst.State.READY and new == Gst.State.PAUSED:
            # for extra responsiveness we refresh the GUI as soons as
            # we reach the PAUSED state
            self.refresh_ui()

    # extract metadata from all the streams and write it to the text widget
    # in the GUI
    def analyze_streams(self):
        # clear current contents of the widget
        buffer = self.streams_list.get_buffer()
        buffer.set_text("")

        # read some properties
        nr_video = self.playbin.get_property("n-video")
        nr_audio = self.playbin.get_property("n-audio")
        nr_text = self.playbin.get_property("n-text")

        for i in range(nr_video):
            tags = None
            # retrieve the stream's video tags
            tags = self.playbin.emit("get-video-tags", i)
            if tags:
                buffer.insert_at_cursor("video stream {0}\n".format(i))
                _, str = tags.get_string(Gst.TAG_VIDEO_CODEC)
                buffer.insert_at_cursor(
                    "  codec: {0}\n".format(
                        str or "unknown"))

        for i in range(nr_audio):
            tags = None
            # retrieve the stream's audio tags
            tags = self.playbin.emit("get-audio-tags", i)
            if tags:
                buffer.insert_at_cursor("\naudio stream {0}\n".format(i))
                ret, str = tags.get_string(Gst.TAG_AUDIO_CODEC)
                if ret:
                    buffer.insert_at_cursor(
                        "  codec: {0}\n".format(
                            str or "unknown"))

                ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
                if ret:
                    buffer.insert_at_cursor(
                        "  language: {0}\n".format(
                            str or "unknown"))

                ret, str = tags.get_uint(Gst.TAG_BITRATE)
                if ret:
                    buffer.insert_at_cursor(
                        "  bitrate: {0}\n".format(
                            str or "unknown"))

        for i in range(nr_text):
            tags = None
            # retrieve the stream's subtitle tags
            tags = self.playbin.emit("get-text-tags", i)
            if tags:
                buffer.insert_at_cursor("\nsubtitle stream {0}\n".format(i))
                ret, str = tags.get_string(Gst.TAG_LANGUAGE_CODE)
                if ret:
                    buffer.insert_at_cursor(
                        "  language: {0}\n".format(
                            str or "unknown"))

    # this function is called when an "application" message is posted on the bus
    # here we retrieve the message posted by the on_tags_changed callback
    def on_application_message(self, bus, msg):
        if msg.get_structure().get_name() == "tags-changed":
            # if the message is the "tags-changed", update the stream info in
            # the GUI
            self.analyze_streams()

p = Player()
p.start()