Media player with GStreamer

Deutsch   |   Source

Creating a media player with GStreamer 1.x

GStreamer is a multimedia framework that can be used ti show (de)code and otherwise alter media files.

/images/19_gst_player.thumbnail.png

Glade

  • display area of the media file: Gtk.DrawingArea widget
  • control elements: skip for-/backward (Gtk.Button), pause/resume playback (Gtk.Togglebutton)
  • select media: buttons to show video or image file

Python

Set up player

Elements and pipelines

GStreamer manages all kinds of media data streams. Every step in the procession chain is defined as an element connected to pipelines. A common pipeline consists of "source", "filter"/"decode" and "sink" elements.

------------------------------------------------------
|  pipeline                                          |
|                                                    |
|  -------------   ----------------   -------------- |
|  | source    |   | filter       |   | sink       | |
|  |           |->>| decoder      |->>|            | |
|  | input     |   | processing   |   | output     | |
|  -------------   ----------------   -------------- |
------------------------------------------------------

This is done by the Gst module:

#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)

Predefined pipelines

There are plenty of possibilities such like handling audio and video signals separated from each other by assigning a "videosink" and an "audiosink" and so on. On the other hand there are given pipelines for standard tasks like media playback. In this case there can be made use of the "playbin" element which also significantly reduces the code:

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)

And action!

A pipeline or playbin element can now be controled by Gst.STATE: .. code-block:: python

player.set_state(Gst.State.PLAYING) player.set_state(Gst.State.PAUSED)

Progress bar

The video progress in this example will not be visualized by a Gtk.ProgressBar but by a horizontal Gtk.Scale. This widget allows to manually set a position with the mouse instead of just showing a value using the value-changed signal. Strictly speaking the change-value signal is a much cleaner solution here which will be used in the follow-up article on relizing the media player with LibVLC.

Possibilities and limitations

Getting to know how to utilize GStreamer there appear a bunch of obstacles (read as: the incompetent author of this article tend to widely generalize based on her experiences):

There are plenty of tutorials but two circumstances make them difficult to comply with:

  1. The primary language in GStreamer is C. Good luck with your Python stuff.
  2. Many older tutorials and manuals do not work out of the box because of major version leap of both GStreamer (0.10 to 1.x) and Python (2.x auf 3.x).

In addition there are effects that are hard to understand. The example given in this article does not work if the Gtk window contains a headerbar. In theory this should be solved by using the "gtksink" but I haven't figured out yet how to assign that sink to a specific widget.

Listings

Glade

19_gst_player.glade (Source)

<?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>

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_stock(Gtk.STOCK_MEDIA_PLAY,Gtk.IconSize.BUTTON)
            widget.set_property("image",img)
            app.pause()
        else:
            img = Gtk.Image.new_from_stock(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

    #skip 20 seconds on forward/backward button
    def skip_time(self,direction=1):
        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()
Comments powered by Disqus