Mediaplayer mit GStreamer
| Anke (encarsia) | Auch verfügbar in: English
Inhalt
Mediaplayer mit GStreamer 1.x realisieren
GStreamer ist ein Multimedia-Framework, das zum Anzeigen und (De-)Kodieren von Mediendateien verwendet werden kann.
Glade
Darstellungsbereich der Mediendatei: Widget Gtk.DrawingArea
Steuerungselemente: Vor-/Zurückspulen (Gtk.utton), Pause (Gtk.Togglebutton)
Medienauswahl: Buttons, um Video- oder Bilddatei anzuzeigen
Python
Player einrichten
Elemente und Pipelines
GStreamer handhabt alle möglichen Arten von Medienflüssen. Jeder Schritt in dieser Verarbeitungskette wird per Element definiert und in Pipelines verbunden. Eine solche Pipeline besteht typischerweise aus "source"-, "filter"-/"decode"- und "sink"-Elementen.
------------------------------------------------------ | Pipeline | | | | ------------- ---------------- -------------- | | | source | | filter | | sink | | | | |->>| decoder |->>| | | | | Quelle | | Verarbeitung | | Ausgabe | | | ------------- ---------------- -------------- | ------------------------------------------------------
Nach diesem Prinzip wird dies mittels Gst-Modul umgesetzt:
# init Gst and create pipeline Gst.init() pipeline = Gst.Pipeline() # create elements src = Gst.ElementFactory.make("filesrc", "source") decode = Gst.ElementFactory.make("decodebin", "decode") sink = Gst.ElementFactory.make("xvimagesink") # configure elements src.set_property("location", file_location) # add elements to pipeline pipeline.add(src) pipeline.add(decode) pipeline.add(sink) #link elements together src.link(decode) decode.link(sink)
Fertige Pipelines
Es besteht auch beispielsweise die Möglichkeit, Audio- und Videosignale voneinander getrennt werden, indem jeweils ein "videosink" und ein "audiosink" erstellt usw. Auf der anderen Seite gibt es vorgefertigte Pipelines für Standardaufgaben wie etwa das Abspielen von Medien. Ein solches Element ist "playbin", das den Code signifikant vereinfacht:
Gst.init(None) player = Gst.ElementFactory.make("playbin", "player") sink = Gst.ElementFactory.make("xvimagesink") player.set_property("uri", uri_of_file) player.set_property("video-sink", sink)
Und los!
Eine Pipeline oder ein "playbin"-Element können nun über Gst.STATE gesteuert werden:
player.set_state(Gst.State.PLAYING) player.set_state(Gst.State.PAUSED)
Fortschrittsanzeige
Die Fortschrittsanzeige ist an dieser Stelle keine Gtk.ProgressBar sondern eine horizontale GtkScale. Mit diesem Widget lässt sich nicht nur eine Position anzeigen, sondern auch per Maus setzen. Für letzteres wird das Signal value-changed benötigt. Streng genommen ist das Signal change-value an dieser Stelle die sauberere Lösung, die im nachfolgenden Beitrag zur Umsetzung des Mediaplayers mit LibVLC verwendet wird.
Möglichkeiten und Limitierungen
Bei der Einarbeitung in GStreamer stolpert man (an dieser Stelle generalisiert die Autorin weitgehend und möglicherweise unbegründet) über diverse Hürden:
Es gibt eine Reihe von Tutorials. Die Umsetzung wird durch zwei Umstände erschwert:
Die primäre Sprache von und mit GStreamer ist C. Mit Python steht man eher auf experimentellem Boden.
Durch die Versionssprünge sowohl bei GStreamer (von 0.10 auf 1.x) als auch Python (2.x auf 3.x) funktionieren viele ältere Anleitungen nicht mehr ohne weiteres.
Es gibt weiterhin Effekte, die sich nicht erschließen. Das in diesem Artikel aufgeführte Beispiel funktioniert nicht, wenn das Fenster eine Headerbar enthält. Des Weiteren ist die Videodarstellung unter Wayland fehlerhaft (Audio läuft). Beide Probleme sind mit der Verwendung von "gtksink" lösbar.
Listings
Python
19_gst_simpleplayer.py (Source)
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import time import gi gi.require_version("Gtk", "3.0") gi.require_version("Gst", "1.0") gi.require_version("GstVideo", "1.0") from gi.repository import Gst, Gtk, GLib, GstVideo class GenericException(Exception): pass class Handler: def on_window_destroy(self, *args): Gtk.main_quit() def on_playpause_togglebutton_toggled(self, widget): if app.playpause_button.get_active(): img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PLAY, Gtk.IconSize.BUTTON) widget.set_property("image", img) app.pause() else: img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, Gtk.IconSize.BUTTON) widget.set_property("image", img) app.play() def on_forward_clicked(self, widget): app.skip_time() def on_backward_clicked(self, widget): app.skip_time(-1) def on_progress_value_changed(self, widget): app.on_slider_seek def on_vbutton_clicked(self, widget): app.clear_playbin() app.setup_player("mediaplayer.avi") if app.playpause_button.get_active() is True: app.playpause_button.set_active(False) else: app.play() def on_ibutton_clicked(self, widget): app.clear_playbin() app.setup_player("mediaplayer.jpg") app.pause() class GstPlayer: def __init__(self): # init GStreamer Gst.init(None) # setting up builder builder = Gtk.Builder() builder.add_from_file("19_gst_player.glade") builder.connect_signals(Handler()) self.movie_window = builder.get_object("play_here") self.playpause_button = builder.get_object("playpause_togglebutton") self.slider = builder.get_object("progress") self.slider_handler_id = self.slider.connect("value-changed", self.on_slider_seek) window = builder.get_object("window") window.show_all() # setting up videoplayer self.player = Gst.ElementFactory.make("playbin", "player") self.sink = Gst.ElementFactory.make("xvimagesink") self.sink.set_property("force-aspect-ratio", True) def setup_player(self,f): # file to play must be transmitted as uri uri = "file://" + os.path.abspath(f) self.player.set_property("uri", uri) # make playbin play in specified DrawingArea widget instead of # separate, GstVideo needed win_id = self.movie_window.get_property("window").get_xid() self.sink.set_window_handle(win_id) self.player.set_property("video-sink", self.sink) def play(self): self.is_playing = True self.player.set_state(Gst.State.PLAYING) #starting up a timer to check on the current playback value GLib.timeout_add(1000, self.update_slider) def pause(self): self.is_playing = False self.player.set_state(Gst.State.PAUSED) def current_position(self): status,position = self.player.query_position(Gst.Format.TIME) return position def skip_time(self,direction=1): #skip 20 seconds on forward/backward button app.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, self.current_position() + float(20) * Gst.SECOND * direction ) def update_slider(self): if not self.is_playing: return False # cancel timeout else: success, self.duration = self.player.query_duration(Gst.Format.TIME) # adjust duration and position relative to absolute scale of 100 self.mult = 100 / (self.duration / Gst.SECOND) if not success: raise GenericException("Couldn't fetch duration") # fetching the position, in nanosecs success, position = self.player.query_position(Gst.Format.TIME) if not success: raise GenericException("Couldn't fetch current position to update slider") # block seek handler so we don't seek when we set_value() self.slider.handler_block(self.slider_handler_id) self.slider.set_value(float(position) / Gst.SECOND * self.mult) self.slider.handler_unblock(self.slider_handler_id) return True # continue calling every x milliseconds def on_slider_seek(self, widget): seek_time = app.slider.get_value() self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time * Gst.SECOND / self.mult) def clear_playbin(self): try: self.player.set_state(Gst.State.NULL) except: pass def main(self): Gtk.main() app = GstPlayer() app.main()
Glade
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.0 --> <interface> <requires lib="gtk+" version="3.16"/> <object class="GtkAdjustment" id="adjustment"> <property name="upper">100</property> <property name="step_increment">1</property> <property name="page_increment">10</property> </object> <object class="GtkImage" id="image1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-rewind</property> </object> <object class="GtkImage" id="image2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-forward</property> </object> <object class="GtkImage" id="image3"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-pause</property> </object> <object class="GtkWindow" id="window"> <property name="can_focus">False</property> <property name="title" translatable="yes">GStreamer media player</property> <property name="default_width">600</property> <property name="default_height">350</property> <signal name="destroy" handler="on_window_destroy" swapped="no"/> <child> <object class="GtkBox" id="box1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkDrawingArea" id="play_here"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkSeparator" id="separator1"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkBox" id="box3"> <property name="visible">True</property> <property name="can_focus">False</property> <child> <object class="GtkButtonBox" id="buttonbox1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="layout_style">start</property> <child> <object class="GtkButton" id="backward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image1</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_backward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="forward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image2</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_forward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkToggleButton" id="playpause_togglebutton"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image3</property> <signal name="toggled" handler="on_playpause_togglebutton_toggled" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkScale" id="progress"> <property name="width_request">300</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">center</property> <property name="margin_left">5</property> <property name="margin_right">5</property> <property name="adjustment">adjustment</property> <property name="fill_level">100</property> <property name="round_digits">1</property> <property name="draw_value">False</property> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <object class="GtkBox" id="box2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="homogeneous">True</property> <child> <object class="GtkButton" id="vbutton"> <property name="label" translatable="yes">Play video</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_vbutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="ibutton"> <property name="label" translatable="yes">Show image</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_ibutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">3</property> </packing> </child> </object> </child> <child> <placeholder/> </child> </object> </interface>
Kommentare
Comments powered by Disqus