Python GStreamer Tutorial
Table of Contents
This tutorial aims at giving a brief introduction to the GStreamer 1.0 multimedia framework using its Python bindings.
1 Meta
A GStreamer application is built as a directed, acyclic graph. In the figures these graphs are illustrated with the convention:
- right solid line rectangles indicate basic GStreamer elements
- rounded solid line rectangles to indicate GStreamer bins (and pipelines) subclasses of elements.
- rounded dashed line rectangles to indicate pads
2 Introduction
This tutorial is meant to be a quick way to get to know more about GStreamer but it'll take some time to write it though because we don't know it ourselves … yet. We're usually using GNU/Linux and GTK in the examples but we try to keep the GUI code to an absolute minimum so it should not get in the way. Just remember that GStreamer depends heavily on Glib so you must make sure that the Glib Mainloop is running if you want to catch events on the bus. We take for granted that you are at least a fairly descent Python coder. For problems related to the Python language we redirect you over to Online Python docs.
There are also some example coding distributed with the PyGST source which you may browse at the gst-python git repository. Reference documents for GStreamer and the rest of the ecosystem it relies on are available at lazka's GitHub site. The main GStreamer site has Reference Manual, FAQ, Applications Development Manual and Plugin Writer's Guide. This tutorial targets the GStreamer 1.0 API which all v1.x releases should follow. The Novacut project has a guide to porting Python applications from the prior 0.1 API to 1.0. Finally, GStreamer provides the GstSDK documentation which includes substantial C programming tutorials.
As you may see this tutorial is far from done and we are always looking for new people to join this project. If you want to write a chapter, fix the examples, provide alternatives or otherwise improve this document please do so. It is suggested to clone the repository on GitHub and issue a pull request. Note, the original source of this document was here but is now dead.
2.1 Command Line
Before getting started with Python some of the command line interface (CLI) programs that come with GStreamer are explored. Besides being generally useful they can help you find and try out what you need in a very fast and convenient way without writing a bit of code. With gst-inspect
you can track highlevel elements which are shipped with the various plugins packages.
man gst-inspect-1.0
If you are looking for an element but you don't know its name you can use it with grep. Getting the elements that handles ie mp3 is done like this:
gst-inspect-1.0 | grep mp3 | sort | head -3
flump3dec: flump3dec: Fluendo MP3 Decoder (liboil build) lame: lamemp3enc: L.A.M.E. mp3 encoder libav: avdec_mp3adufloat: libav ADU (Application Data Unit) MP3 (MPEG audio layer 3) decoder
The playbin
element is an autoplugger which usually plays anything you throw at it, if you have the appropriate plugins installed.
gst-inspect-1.0 playbin > gst-inspect-playbin.txt
Browse example output here.
You can also run pipelines directly in a terminal with gst-launch
:
man gst-launch-1.0
For playing a file with playbin:
gst-launch-1.0 playbin \ uri=http://docs.gstreamer.com/media/sintel_trailer-480p.webm
It's also possible to link elements together with "!":
gst-launch-1.0 audiotestsrc ! alsasink
You may also make different streams in the pipeline:
gst-launch-1.0 audiotestsrc ! alsasink videotestsrc ! xvimagesink
Or, you can make a single frame JPEG
gst-launch-1.0 videotestsrc num-buffers=1 ! jpegenc ! \ filesink location=videotestsrc-frame.jpg
If you are using the "name" property you may use the same element more than once. Just put a "." after its name, eg with oggmux here.
gst-launch-1.0 audiotestsrc ! vorbisenc ! oggmux name=mux ! \ filesink location=file.ogg videotestsrc ! theoraenc ! mux.
In the next chapter we will show you more examples with Playbin.
3 Playbin
The playbin
element was exercised from the command line in section 2.1 and in this section it will be used from Python. It is a high-level, automatic audio and video player. You create a playbin
object with:
import gi gi.require_version('Gst', '1.0') from gi.repository import Gst Gst.init(None) # ... my_playbin = Gst.ElementFactory.make("playbin", None) assert my_playbin print my_playbin
<__main__.GstPlayBin object at 0x7f2604238aa0 (GstPlayBin at 0x17e6480)>
To get information about a playbin run:
gst-inspect-0.10 playbin
This figure shows how playbin is built internally. The "optional stuff" are things that could be platform specific or things that you may set with properties.
The "uri" property should take any possible protocol supported by your GStreamer plugins. One nice feature is that you may switch the sinks out for your own bins as shown below. Playbin always tries to set up the best possible pipeline for your specific environment so if you don't need any special features that are not implemented in playbin, it should in most cases just work "out of the box". Ok, time for a few examples.
3.1 Audio with Playbin
This first example is just a simple audio player, insert a file with absolute path and it'll play. It's code is listed below. You can run it like:
python playbin-example-audio.py
It will open a small window with a text entry. Enter the full path to some audio file and click "Start".
playbin-example-audio.py#!/usr/bin/env python import os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk class GTK_Main(object): def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Audio-Player") window.set_default_size(300, -1) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.entry = Gtk.Entry() vbox.pack_start(self.entry, False, True, 0) self.button = Gtk.Button("Start") self.button.connect("clicked", self.start_stop) vbox.add(self.button) window.show_all() self.player = Gst.ElementFactory.make("playbin", "player") fakesink = Gst.ElementFactory.make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.set_property("uri", "file://" + filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() print "Error: %s" % err, debug self.button.set_label("Start") Gst.init(None) GTK_Main() GObject.threads_init() Gtk.main()
3.2 Adding Video
A playbin
plugs both audio and video streams automagically and the videosink
has been switched out to a fakesink
element which is GStreamer's answer to directing output to /dev/null
. If you want to enable video playback just comment out the following lines:
fakesink = Gst.ElementFactory.make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink)
If you want to show the video output in a specified window you'll have to use the enable_sync_message_emission()
method on the bus. Here is an example with the video window embedded in the program.
#!/usr/bin/env python import sys, os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk # Needed for window.get_xid(), xvimagesink.set_window_handle(), respectively: from gi.repository import GdkX11, GstVideo class GTK_Main(object): def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Video-Player") window.set_default_size(500, 400) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) hbox = Gtk.HBox() vbox.pack_start(hbox, False, False, 0) self.entry = Gtk.Entry() hbox.add(self.entry) self.button = Gtk.Button("Start") hbox.pack_start(self.button, False, False, 0) self.button.connect("clicked", self.start_stop) self.movie_window = Gtk.DrawingArea() vbox.add(self.movie_window) window.show_all() self.player = Gst.ElementFactory.make("playbin", "player") bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) bus.connect("sync-message::element", self.on_sync_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.set_property("uri", "file://" + filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() print "Error: %s" % err, debug self.button.set_label("Start") def on_sync_message(self, bus, message): if message.get_structure().get_name() == 'prepare-window-handle': imagesink = message.src imagesink.set_property("force-aspect-ratio", True) imagesink.set_window_handle(self.movie_window.get_property('window').get_xid()) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
And just to make things a little more complicated you can switch the playbin
's video sink to a Gst.Bin
with a Gst.GhostPad
on it. Here's an example with a timeoverlay
.
bin = Gst.Bin.new("my-bin") timeoverlay = Gst.ElementFactory.make("timeoverlay") bin.add(timeoverlay) pad = timeoverlay.get_static_pad("video_sink") ghostpad = Gst.GhostPad.new("sink", pad) bin.add_pad(ghostpad) videosink = Gst.ElementFactory.make("autovideosink") bin.add(videosink) timeoverlay.link(videosink) self.player.set_property("video-sink", bin)
Add that code to the example above and you'll get a timeoverlay
too. We'll talk more about "ghost pads" later.
Here now adds a CLI example which plays music. It can be run it with:
python cliplayer.py /path/to/file1.mp3 /path/to/file2.ogg
#!/usr/bin/env python import sys, os, time, thread import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GLib, GObject class CLI_Main(object): def __init__(self): self.player = Gst.ElementFactory.make("playbin", "player") fakesink = Gst.ElementFactory.make("fakesink", "fakesink") self.player.set_property("video-sink", fakesink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.playmode = False elif t == Gst.MessageType.ERROR: self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() print "Error: %s" % err, debug self.playmode = False def start(self): for filepath in sys.argv[1:]: if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.playmode = True self.player.set_property("uri", "file://" + filepath) self.player.set_state(Gst.State.PLAYING) while self.playmode: time.sleep(1) time.sleep(1) loop.quit() GObject.threads_init() Gst.init(None) mainclass = CLI_Main() thread.start_new_thread(mainclass.start, ()) loop = GLib.MainLoop() loop.run()
A playbin
implements a Gst.Pipeline
element and that's what the next chapter is going to tell you more about.
4 Pipeline
A Gst.Pipeline
is a top-level bin with its own bus and clock. If your program only contains one bin-like object, this is what you're looking for. You create a pipeline object with:
my_pipeline = Gst.Pipeline.new("my-pipeline")
A pipeline is a "container" where you can put other objects and when everything is in place and the file to play is specified you set the pipeline's state to Gst.State.PLAYING
and there should be multimedia coming out of it.
4.1 Pipeline Audio Player
The first example here starts with the audio player from section 3 and switches the playbin
for the mad
decoding pipeline that is capable of handling MP3 streams. Before coding it in Python it can be tested using gst-launch
.
gst-launch-1.0 filesrc location=file.mp3 ! mad ! audioconvert ! alsasink
Conceptually this pipeline looks like:
Done in Python this would look like pipeline-example.py
:
#!/usr/bin/env python import sys, os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk class GTK_Main(object): def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("MP3-Player") window.set_default_size(400, 200) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.entry = Gtk.Entry() vbox.pack_start(self.entry, False, True, 0) self.button = Gtk.Button("Start") self.button.connect("clicked", self.start_stop) vbox.add(self.button) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") decoder = Gst.ElementFactory.make("mad", "mp3-decoder") conv = Gst.ElementFactory.make("audioconvert", "converter") sink = Gst.ElementFactory.make("alsasink", "alsa-output") self.player.add(source) self.player.add(decoder) self.player.add(conv) self.player.add(sink) source.link(decoder) decoder.link(conv) conv.link(sink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") err, debug = message.parse_error() print "Error: %s" % err, debug Gst.init(None) GTK_Main() GObject.threads_init() Gtk.main()
4.2 Adding Video to the Pipeline
The next example is playing MPEG2 videos. Some demuxers, such as mpegdemux
, uses dynamic pads which are created at runtime and therefor you can't link between the demuxer and the next element in the pipeline before the pad has been created at runtime. Watch out for the demuxer_callback()
method below.
#!/usr/bin/env python import os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk class GTK_Main(object): def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Mpeg2-Player") window.set_default_size(500, 400) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) hbox = Gtk.HBox() vbox.pack_start(hbox, False, False, 0) self.entry = Gtk.Entry() hbox.add(self.entry) self.button = Gtk.Button("Start") hbox.pack_start(self.button, False, False, 0) self.button.connect("clicked", self.start_stop) self.movie_window = Gtk.DrawingArea() vbox.add(self.movie_window) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") demuxer = Gst.ElementFactory.make("mpegpsdemux", "demuxer") demuxer.connect("pad-added", self.demuxer_callback) self.video_decoder = Gst.ElementFactory.make("mpeg2dec", "video-decoder") self.audio_decoder = Gst.ElementFactory.make("mad", "audio-decoder") audioconv = Gst.ElementFactory.make("audioconvert", "converter") audiosink = Gst.ElementFactory.make("autoaudiosink", "audio-output") videosink = Gst.ElementFactory.make("autovideosink", "video-output") self.queuea = Gst.ElementFactory.make("queue", "queuea") self.queuev = Gst.ElementFactory.make("queue", "queuev") colorspace = Gst.ElementFactory.make("videoconvert", "colorspace") self.player.add(source) self.player.add(demuxer) self.player.add(self.video_decoder) self.player.add(self.audio_decoder) self.player.add(audioconv) self.player.add(audiosink) self.player.add(videosink) self.player.add(self.queuea) self.player.add(self.queuev) self.player.add(colorspace) source.link(demuxer) self.queuev.link(self.video_decoder) self.video_decoder.link(colorspace) colorspace.link(videosink) self.queuea.link(self.audio_decoder) self.audio_decoder.link(audioconv) audioconv.link(audiosink) bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) bus.connect("sync-message::element", self.on_sync_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_sync_message(self, bus, message): if message.get_structure().get_name() == 'prepare-window-handle': imagesink = message.src imagesink.set_property("force-aspect-ratio", True) xid = self.movie_window.get_property('window').get_xid() imagesink.set_window_handle(xid) def demuxer_callback(self, demuxer, pad): if pad.get_property("template").name_template == "video_%02d": qv_pad = self.queuev.get_pad("sink") pad.link(qv_pad) elif pad.get_property("template").name_template == "audio_%02d": qa_pad = self.queuea.get_pad("sink") pad.link(qa_pad) Gst.init(None) GTK_Main() GObject.threads_init() Gtk.main()
The elements in a pipeline connects to each other with pads and that's what the next chapter will tell you more about.
5 Src, sink, pad … oh my!
As their names imply, a src is an object that is "sending" data and a sink is an object that is "receiving" data. These objects connect to each other with pads. Pads could be either src or sink. Most elements have both src and sink pads. For example, the mad
MP3 decoder element looks something like the figure below:
And as always if you want to know more about highlevel elements gst-inspect is your friend:
gst-inspect-1.0 mad
In particular, the inheritance diagram shows that mad
is an element:
GObject +----GInitiallyUnowned +----GstObject +----GstElement +----GstAudioDecoder +----GstMad
There are many different ways to link elements together. In pipeline-example.py
we used the Gst.Pipeline.add()
and the .link()
method of the produced elements. You can also make a completely ready-to-go pipeline with the parse_launch()
function. The many .add()
calls in that example can be rewritten as:
mp3_pipeline = Gst.parse_launch("filesrc name=source ! mad name=decoder ! " \ "audioconvert name=conv ! alsasink name=sink")
The micro-language used in this function call is that of the gst-launch
command line program.
When you do manually link pads with the .link()
method make sure that you link a src-pad to a sink-pad. No rule though without exceptions. A Gst.GhostPad
should be linked to a pad of the same kind as itself. We have already showed how a ghost pad works in the addition to example 2.2. A Gst.Bin
can't link to other objects if you don't link a Gst.GhostPad
to an element inside the bin. The playbin-example-video.py
example in section 3 should look something like this:
And the ghostpad above should be created as type "sink"!!!
Some pads are not always available and are only created when they are in use. Such pads are called "dynamical pads". The next example will show how to use dynamically created pads with an oggdemux
. The link between the demuxer and the decoder is created with the demuxer_callback()
method, which is called whenever a pad is created in the demuxer using the "pad-added
" signal.
#!/usr/bin/env python import sys, os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk class GTK_Main(object): def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Vorbis-Player") window.set_default_size(500, 200) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.entry = Gtk.Entry() vbox.pack_start(self.entry, False, False, 0) self.button = Gtk.Button("Start") vbox.add(self.button) self.button.connect("clicked", self.start_stop) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") demuxer = Gst.ElementFactory.make("oggdemux", "demuxer") demuxer.connect("pad-added", self.demuxer_callback) self.audio_decoder = Gst.ElementFactory.make("vorbisdec", "vorbis-decoder") audioconv = Gst.ElementFactory.make("audioconvert", "converter") audiosink = Gst.ElementFactory.make("autoaudiosink", "audio-output") self.player.add(source) self.player.add(demuxer) self.player.add(self.audio_decoder) self.player.add(audioconv) self.player.add(audiosink) source.link(demuxer) self.audio_decoder.link(audioconv) audioconv.link(audiosink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def demuxer_callback(self, demuxer, pad): adec_pad = self.audio_decoder.get_static_pad("sink") pad.link(adec_pad) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
Now after reading through these four chapters you could need a break. Happy hacking and stay tuned for more interesting chapters to come.
6 Seeking
Seeking in GStreamer is done with the seek()
and seek_simple()
methods of Gst.Element
.
To be able to seek you will also need to tell GStreamer what kind of seek it should do. In the following example we will use a TIME
value (of Gst.Format
enum) format constant which will, as you may guess, request a time seek. We will also use the query_duration()
and query_position()
methods to get the file length and how long the file has currently played. GStreamer uses nanoseconds by default so you have to adjust to that.
In this next example we take the Vorbis-Player from example 4.1 and update it with some more stuff so it's able to seek and show duration and position.
seeking-example.py#!/usr/bin/env python import os, thread, time import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GObject, Gtk, Gdk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Vorbis-Player") window.set_default_size(500, -1) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.entry = Gtk.Entry() vbox.pack_start(self.entry, False, False, 0) hbox = Gtk.HBox() vbox.add(hbox) buttonbox = Gtk.HButtonBox() hbox.pack_start(buttonbox, False, False, 0) rewind_button = Gtk.Button("Rewind") rewind_button.connect("clicked", self.rewind_callback) buttonbox.add(rewind_button) self.button = Gtk.Button("Start") self.button.connect("clicked", self.start_stop) buttonbox.add(self.button) forward_button = Gtk.Button("Forward") forward_button.connect("clicked", self.forward_callback) buttonbox.add(forward_button) self.time_label = Gtk.Label() self.time_label.set_text("00:00 / 00:00") hbox.add(self.time_label) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") demuxer = Gst.ElementFactory.make("oggdemux", "demuxer") demuxer.connect("pad-added", self.demuxer_callback) self.audio_decoder = Gst.ElementFactory.make("vorbisdec", "vorbis-decoder") audioconv = Gst.ElementFactory.make("audioconvert", "converter") audiosink = Gst.ElementFactory.make("autoaudiosink", "audio-output") for ele in [source, demuxer, self.audio_decoder, audioconv, audiosink]: self.player.add(ele) source.link(demuxer) self.audio_decoder.link(audioconv) audioconv.link(audiosink) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PLAYING) self.play_thread_id = thread.start_new_thread(self.play_thread, ()) else: self.play_thread_id = None self.player.set_state(Gst.State.NULL) self.button.set_label("Start") self.time_label.set_text("00:00 / 00:00") def play_thread(self): play_thread_id = self.play_thread_id Gdk.threads_enter() self.time_label.set_text("00:00 / 00:00") Gdk.threads_leave() while play_thread_id == self.play_thread_id: try: time.sleep(0.2) dur_int = self.player.query_duration(Gst.Format.TIME, None)[0] if dur_int == -1: continue dur_str = self.convert_ns(dur_int) Gdk.threads_enter() self.time_label.set_text("00:00 / " + dur_str) Gdk.threads_leave() break except: pass time.sleep(0.2) while play_thread_id == self.play_thread_id: pos_int = self.player.query_position(Gst.Format.TIME, None)[0] pos_str = self.convert_ns(pos_int) if play_thread_id == self.play_thread_id: Gdk.threads_enter() self.time_label.set_text(pos_str + " / " + dur_str) Gdk.threads_leave() time.sleep(1) def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.play_thread_id = None self.player.set_state(Gst.State.NULL) self.button.set_label("Start") self.time_label.set_text("00:00 / 00:00") elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.play_thread_id = None self.player.set_state(Gst.State.NULL) self.button.set_label("Start") self.time_label.set_text("00:00 / 00:00") def demuxer_callback(self, demuxer, pad): adec_pad = self.audio_decoder.get_static_pad("sink") pad.link(adec_pad) def rewind_callback(self, w): rc, pos_int = self.player.query_position(Gst.Format.TIME) seek_ns = pos_int - 10 * 1000000000 if seek_ns < 0: seek_ns = 0 print 'Backward: %d ns -> %d ns' % (pos_int, seek_ns) self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_ns) def forward_callback(self, w): rc, pos_int = self.player.query_position(Gst.Format.TIME) seek_ns = pos_int + 10 * 1000000000 print 'Forward: %d ns -> %d ns' % (pos_int, seek_ns) self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, seek_ns) def convert_ns(self, t): # This method was submitted by Sam Mason. # It's much shorter than the original one. s,ns = divmod(t, 1000000000) m,s = divmod(s, 60) if m < 60: return "%02i:%02i" %(m,s) else: h,m = divmod(m, 60) return "%i:%02i:%02i" %(h,m,s) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
7 Capabilities
Capabilities, Gst.Caps
, is a container where you may store information that you may pass on to a Gst.PadTemplate
. When you set the pipeline state to either playing or paused the elements pads negotiates what caps to use for the stream. Now the following pipeline works perfectly:
gst-launch-1.0 videotestsrc ! video/x-raw, width=320, height=240 ! \ xvimagesink
But if you try to switch out the xvimagesink
for an ximagesink
you will notice that it wouldn't work. That's because ximagesink
can't handle video/x-raw
so you must put in an element BEFORE in the pipeline that does.
gst-launch-1.0 videotestsrc ! video/x-raw, width=320, height=240 ! \ videoconvert ! ximagesink
And as ximagesink
does not support hardware scaling you have to throw in a videoscale
element too if you want software scaling.
gst-launch-1.0 videotestsrc ! video/x-raw, width=320, height=240 ! \ videoscale ! videoconvert ! ximagesink
To put the above examples in code you have to put the caps in a capsfilter element.
capabilities-example.py#!/usr/bin/env python import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GObject, Gtk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Videotestsrc-Player") window.set_default_size(300, -1) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.button = Gtk.Button("Start") self.button.connect("clicked", self.start_stop) vbox.add(self.button) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("videotestsrc", "video-source") sink = Gst.ElementFactory.make("xvimagesink", "video-output") caps = Gst.Caps.from_string("video/x-raw, width=320, height=230") filter = Gst.ElementFactory.make("capsfilter", "filter") filter.set_property("caps", caps) self.player.add(source) self.player.add(filter) self.player.add(sink) source.link(filter) filter.link(sink) def start_stop(self, w): if self.button.get_label() == "Start": self.button.set_label("Stop") self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
A frequently asked question is how to find out what resolution a file has and one way to do it is to check the caps on a decodebin element in paused state.
capabilities-resolution-example.py#!/usr/bin/env python import os import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GObject, Gtk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Resolutionchecker") window.set_default_size(300, -1) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.entry = Gtk.Entry() vbox.pack_start(self.entry, False, True, 0) self.button = Gtk.Button("Check") self.button.connect("clicked", self.start_stop) vbox.add(self.button) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") decoder = Gst.ElementFactory.make("decodebin", "decoder") decoder.connect("pad-added", self.decoder_callback) self.fakea = Gst.ElementFactory.make("fakesink", "fakea") self.fakev = Gst.ElementFactory.make("fakesink", "fakev") self.player.add(source) self.player.add(decoder) self.player.add(self.fakea) self.player.add(self.fakev) source.link(decoder) bus = self.player.get_bus() bus.add_signal_watch() bus.connect("message", self.on_message) def start_stop(self, w): filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.player.set_state(Gst.State.NULL) self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PAUSED) def on_message(self, bus, message): typ = message.type if typ == Gst.MessageType.STATE_CHANGED: if message.parse_state_changed()[1] == Gst.State.PAUSED: decoder = self.player.get_by_name("decoder") for pad in decoder.srcpads: caps = pad.query_caps(None) structure_name = caps.to_string() width = caps.get_structure(0).get_int('width') height = caps.get_structure(0).get_int('height') if structure_name.startswith("video") and len(str(width)) < 6: print "Width:%d, Height:%d" %(width, height) self.player.set_state(Gst.State.NULL) break elif typ == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.player.set_state(Gst.State.NULL) def decoder_callback(self, decoder, pad): caps = pad.query_caps(None) structure_name = caps.to_string() if structure_name.startswith("video"): fv_pad = self.fakev.get_static_pad("sink") pad.link(fv_pad) elif structure_name.startswith("audio"): fa_pad = self.fakea.get_static_pad("sink") pad.link(fa_pad) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
In the next example we will use the playbin
from section 3.2 and switch its video-sink out for our own homemade bin filled with some elements. Now, let's say that you run a tv-station and you want to have your logo in the top right corner of the screen. For that you can use a textoverlay
but for the fonts to be the exact same size on the screen no matter what kind of resolution the source has you have to specify a width so everything is scaled according to that.
#!/usr/bin/env python import os import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GObject, Gtk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Video-Player") window.set_default_size(500, 400) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) hbox = Gtk.HBox() vbox.pack_start(hbox, False, False, 0) self.entry = Gtk.Entry() hbox.add(self.entry) self.button = Gtk.Button("Start") hbox.pack_start(self.button, False, False, 0) self.button.connect("clicked", self.start_stop) self.movie_window = Gtk.DrawingArea() vbox.add(self.movie_window) window.show_all() self.player = Gst.ElementFactory.make("playbin", "player") self.bin = Gst.Bin.new("my-bin") videoscale = Gst.ElementFactory.make("videoscale") videoscale.set_property("method", 1) pad = videoscale.get_static_pad("sink") ghostpad = Gst.GhostPad.new("sink", pad) self.bin.add_pad(ghostpad) caps = Gst.Caps.from_string("video/x-raw, width=720") filter = Gst.ElementFactory.make("capsfilter", "filter") filter.set_property("caps", caps) textoverlay = Gst.ElementFactory.make('textoverlay') textoverlay.set_property("text", "GNUTV") textoverlay.set_property("font-desc", "normal 14") # TypeError: object of type `GstTextOverlay' does not have property `halign' #textoverlay.set_property("halign", "right") # TypeError: object of type `GstTextOverlay' does not have property `valign' #textoverlay.set_property("valign", "top") conv = Gst.ElementFactory.make ("videoconvert", "conv") videosink = Gst.ElementFactory.make("autovideosink") self.bin.add(videoscale) self.bin.add(filter) self.bin.add(textoverlay) self.bin.add(conv) self.bin.add(videosink) videoscale.link(filter) filter.link(textoverlay) textoverlay.link(conv) conv.link(videosink) self.player.set_property("video-sink", self.bin) bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) bus.connect("sync-message::element", self.on_sync_message) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.exists(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.set_property("uri", "file://" + filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): typ = message.type if typ == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif typ == Gst.MessageType.ERROR: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") err, debug = message.parse_error() print "Error: %s" % err, debug def on_sync_message(self, bus, message): if message.structure is None: return message_name = message.structure.get_name() if message_name == "prepare-xwindow-id": imagesink = message.src imagesink.set_property("force-aspect-ratio", True) imagesink.set_xwindow_id(self.movie_window.window.xid) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
8 Videomixer
The videomixer
element makes it possible to mix different video streams together. Here is a CLI example:
gst-launch-1.0 filesrc location=tvlogo.png ! pngdec ! alphacolor ! \ videoconvert ! videobox border-alpha=0 alpha=0.5 top=-20 left=-200 ! \ videomixer name=mix ! videoconvert ! autovideosink videotestsrc ! \ video/x-raw, width=320, height=240 ! mix.
Fixme: this fails with:
Setting pipeline to PAUSED ... Pipeline is PREROLLING ... ERROR: from element /GstPipeline:pipeline0/GstFileSrc:filesrc0: Internal data flow error. Additional debug info: gstbasesrc.c(2865): gst_base_src_loop (): /GstPipeline:pipeline0/GstFileSrc:filesrc0: streaming task paused, reason not-negotiated (-4) ERROR: pipeline doesn't want to preroll. Setting pipeline to NULL ... Freeing pipeline ...
You have to make a tvlogo.png
image (100x100 px) to be able to run it. With the videobox
element you can move the image around and add more alpha channels.
In the next example we take the now working Mpeg2-Player from section 4 and add the elements shown above.
videomixer-example.py#!/usr/bin/env python import os import gi gi.require_version("Gst", "1.0") from gi.repository import Gst, GObject, Gtk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Mpeg2-Player") window.set_default_size(500, 400) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) hbox = Gtk.HBox() vbox.pack_start(hbox, False, False, 0) self.entry = Gtk.Entry() hbox.add(self.entry) self.button = Gtk.Button("Start") hbox.pack_start(self.button, False, False, 0) self.button.connect("clicked", self.start_stop) self.movie_window = Gtk.DrawingArea() vbox.add(self.movie_window) window.show_all() self.player = Gst.Pipeline.new("player") source = Gst.ElementFactory.make("filesrc", "file-source") demuxer = Gst.ElementFactory.make("mpegpsdemux", "demuxer") demuxer.connect("pad-added", self.demuxer_callback) self.video_decoder = Gst.ElementFactory.make("mpeg2dec", "video-decoder") png_decoder = Gst.ElementFactory.make("pngdec", "png-decoder") png_source = Gst.ElementFactory.make("filesrc", "png-source") png_source.set_property("location", os.path.realpath("tvlogo.png")) mixer = Gst.ElementFactory.make("videomixer", "mixer") self.audio_decoder = Gst.ElementFactory.make("mad", "audio-decoder") audioconv = Gst.ElementFactory.make("audioconvert", "converter") audiosink = Gst.ElementFactory.make("autoaudiosink", "audio-output") videosink = Gst.ElementFactory.make("autovideosink", "video-output") self.queuea = Gst.ElementFactory.make("queue", "queuea") self.queuev = Gst.ElementFactory.make("queue", "queuev") ffmpeg1 = Gst.ElementFactory.make("videoconvert", "ffmpeg1") ffmpeg2 = Gst.ElementFactory.make("videoconvert", "ffmpeg2") ffmpeg3 = Gst.ElementFactory.make("videoconvert", "ffmpeg3") videobox = Gst.ElementFactory.make("videobox", "videobox") alphacolor = Gst.ElementFactory.make("alphacolor", "alphacolor") for ele in (source, demuxer, self.video_decoder, png_decoder, png_source, mixer, self.audio_decoder, audioconv, audiosink, videosink, self.queuea, self.queuev, ffmpeg1, ffmpeg2, ffmpeg3, videobox, alphacolor): self.player.add(ele) source.link(demuxer) self.queuev.link(self.video_decoder) self.video_decoder.link(ffmpeg1) ffmpeg1.link(mixer) mixer.link(ffmpeg2) ffmpeg2.link(videosink) png_source.link(png_decoder) png_decoder.link(alphacolor) alphacolor.link(ffmpeg3) ffmpeg3.link(videobox) videobox.link(mixer) self.queuea.link(self.audio_decoder) self.audio_decoder.link(audioconv) audioconv.link(audiosink) bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) bus.connect("sync-message::element", self.on_sync_message) videobox.set_property("border-alpha", 0) videobox.set_property("alpha", 0.5) videobox.set_property("left", -10) videobox.set_property("top", -10) def start_stop(self, w): if self.button.get_label() == "Start": filepath = self.entry.get_text().strip() if os.path.isfile(filepath): filepath = os.path.realpath(filepath) self.button.set_label("Stop") self.player.get_by_name("file-source").set_property("location", filepath) self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_sync_message(self, bus, message): if message.structure is None: return message_name = message.structure.get_name() if message_name == "prepare-xwindow-id": imagesink = message.src imagesink.set_property("force-aspect-ratio", True) imagesink.set_xwindow_id(self.movie_window.window.xid) def demuxer_callback(self, demuxer, pad): tpl_property = pad.get_property("template") tpl_name = tpl_property.name_template print 'demuxer_callback: template name template: "%s"' % tpl_name if tpl_name == "video_%02d": queuev_pad = self.queuev.get_pad("sink") pad.link(queuev_pad) elif tpl_name == "audio_%02d": queuea_pad = self.queuea.get_pad("sink") pad.link(queuea_pad) GObject.threads_init() Gst.init(None) GTK_Main() Gtk.main()
Fixme: This fails with:
Error: Internal data flow error. gstbasesrc.c(2865): gst_base_src_loop (): /GstPipeline:player/GstFileSrc:png-source: streaming task paused, reason not-negotiated (-4)
9 Webcam Viewer
Remember to peal the tape off your web cam lens before testing this.
webcam-example.py#!/usr/bin/env python import sys, os import gi gi.require_version('Gst', '1.0') from gi.repository import Gst, GObject, Gtk class GTK_Main: def __init__(self): window = Gtk.Window(Gtk.WindowType.TOPLEVEL) window.set_title("Webcam-Viewer") window.set_default_size(500, 400) window.connect("destroy", Gtk.main_quit, "WM destroy") vbox = Gtk.VBox() window.add(vbox) self.movie_window = Gtk.DrawingArea() vbox.add(self.movie_window) hbox = Gtk.HBox() vbox.pack_start(hbox, False, False, 0) hbox.set_border_width(10) hbox.pack_start(Gtk.Label(), False, False, 0) self.button = Gtk.Button("Start") self.button.connect("clicked", self.start_stop) hbox.pack_start(self.button, False, False, 0) self.button2 = Gtk.Button("Quit") self.button2.connect("clicked", self.exit) hbox.pack_start(self.button2, False, False, 0) hbox.add(Gtk.Label()) window.show_all() # Set up the gstreamer pipeline self.player = Gst.parse_launch ("v4l2src ! autovideosink") bus = self.player.get_bus() bus.add_signal_watch() bus.enable_sync_message_emission() bus.connect("message", self.on_message) bus.connect("sync-message::element", self.on_sync_message) def start_stop(self, w): if self.button.get_label() == "Start": self.button.set_label("Stop") self.player.set_state(Gst.State.PLAYING) else: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def exit(self, widget, data=None): Gtk.main_quit() def on_message(self, bus, message): t = message.type if t == Gst.MessageType.EOS: self.player.set_state(Gst.State.NULL) self.button.set_label("Start") elif t == Gst.MessageType.ERROR: err, debug = message.parse_error() print "Error: %s" % err, debug self.player.set_state(Gst.State.NULL) self.button.set_label("Start") def on_sync_message(self, bus, message): struct = message.get_structure() if not struct: return message_name = struct.get_name() if message_name == "prepare-xwindow-id": # Assign the viewport imagesink = message.src imagesink.set_property("force-aspect-ratio", True) imagesink.set_xwindow_id(self.movie_window.window.xid) Gst.init(None) GTK_Main() GObject.threads_init() Gtk.main()