GES Basic Beginner Tutorial

Intro

GStreamer Editing Services or GES is a library built to streamline the creation of multimedia editing. 

Based on the GStreamer multimedia framework and the GNonLin set of plugins, its goals are to suit all types of

editing-related applications. GStreamer and GNonLin give the programmer all the building blocks to create Non-Linear

editors, but requires them to write a substantial amount of code to do so. GES aims to close the gap between 

GStreamer and GNonLin and the application developer by offering a series of classes to simplify the creation of many 

kinds of editing-related applications.


Figure 1. The GES Architecture


Goal

Introduce new users to the inner workings of starting a new project in GES using Python. After this tutorial, you will be

able to create a video out of a set of pictures. This includes:

  • Setting up the boilerplate and the main function

  • Initializing GStreamer and GES

  • Setting up the timeline and layer

  • Setting up the pipeline and profiles

  • Riding the bus


Before we begin

This tutorial assumes that you have the following Debian packages installed:

  • gir1.2-ges-1.0

  • gstreamer1.0-plugins-base

  • gstreamer1.0-plugins-good

  • gstreamer1.0-plugins-bad

  • gstreamer1.0-plugins-ugly

All documentation relating to GI library functions can be found here.

All documentation relating to GLib library functions can be found here.

All documentation relating to Gst library functions can be found here.

All documentation relating to GES library functions can be found here.

All documentation relating to GstPbutils library functions can be found here.

The overall documentation of GStreamer can be found here. This was used as a reference to explain the functionality 

of the functions used in this code.


Setting Up the Boilerplate

Every new GES project requires you to set up some boilerplate. This includes:

  • Importing the required libraries

    • import gi

    • from gi.repository import GLib

    • gi.require_version('Gst', '1.0')

    • from gi.repository import Gst

    • gi.require_version('GES', '1.0')

    • from gi.repository import GES

    • gi.require_version('GstPbutils', '1.0')

    • from gi.repository import GstPbutils

    • These imports are explained as follows

      • GObject Introspection (gi)  - Allows easy access to the entirety of the GNOME software platform. 

      • GLib - A low-level core library that forms the basis of GStreamer.

      • Gst - The GStreamer core.

      • GES - GStreamer Editing Services, a library based on GStreamer for creating multimedia editing

        applications.

      • GstPbutils - A collection of plugins created for GStreamer. 

    • We use gi.require_version() to choose the version of some of the libraries we import, in case the

      user’s system has multiple versions installed or a newer version has been found online. For this example,

      we will use version 1.0 for Gst, GES, and GstPbutils.

  • Choosing the framerate for the video

    • FRAME_RATE = # desired frames per second

    • Set your desired frame rate to your specific rate. The most common setting for frame rates in movies is 

      24 frames per second (FPS) (as in 24 photos are taken per second by the camera). With a higher FPS,

      the video may appear to be moving faster than intended. With a lower FPS, the video may appear to be

      in slow motion. It is important to update the FPS depending on what FPS the images being used were

      intended to be shown in.

  • Creating the to_nanoseconds function

    • def to_nanoseconds(frame_number):

    •     return Gst.util_uint64_scale_ceil(frame_number, Gst.SECOND,       FRAME_RATE)

    • This function returns the time in nanoseconds that has passed before the frame that was passed in as a

      parameter shows up in the video.

    • It is worth mentioning that an equivalent function is built into GES as Timeline.get_frame_time in

      versions 1.18 or newer.

    • Since we are using version 1.0 of GES, we do not have access to that function and therefore create 

      to_nanoseconds for ease of use.


Initializing GStreamer and GES


Setting up the main function

def main(image_filenames, output_filename):

  • image_filenames - The name of the pictures on your device

  • output_filename - The name of the video after creation


All GES video projects require the initialization of Gst and GES. In order to initialize these, call:

Gst.init(None)

      GES.init()

inside the main function.

  • Gst.init(None)- Initializes the GStreamer library, setting up internal path lists, registering built-in elements,

    and loading standard plugins.

  • GES.init() - Initialize the GStreamer Editing Service. It is important to initialize the GStreamer library before

    attempting to initialize GES.


Setting Up the Timeline and Layer

The timeline in GES is where the pictures will be stored and converted into a video. A timeline is composed as a set of

tracks and layers. A track is essentially an output source for the timeline. A layer is responsible for collecting and 

ordering clips in the timeline. Any layer within the timeline has a priority, corresponding to their index within the timeline. 

The layer with the highest priority is 0. As the indices increase, the priority becomes lesser. The function  

ges_timeline_move_layer() shall be used if you wish to change how layers are prioritized in a timeline.  For this 

tutorial, we will be working with one (1) layer. 


To set up your timeline with a single layer, simply call:

     timeline = GES.Timeline.new_audio_video()

     layer = timeline.append_layer()

  • new_audio_video()- creates a new timeline containing a single audio track and a single video track.

  • append_layer()- adds the layer to the timeline at the lowest priority by default.


Adding Assets to the Layer

Now it is time to add the pictures into the layer. To start, we must loop over every image filename, along with their 

respective frame number, and add it to our layer at the correct start and end time.

We will use a for loop in conjunction with enumerate()in order to capture each file name with its associated frame 

number. 

  • As a reminder, enumerate()returns the “count” of the element in the list that you are at, as well as the element 

    itself. By default, enumerate()starts its “count” at 0.

for frame_number, image_filename in enumerate(image_filenames):


In order to add an asset to the layer, we must first create the asset. We are creating a URI clip asset, which will allow us 

to use the media file in GES. However, before creating a URI clip asset, we must first use  

Gst.filename_to_uri(filename)in order to convert the filename to a valid URI string.

  • On a Windows system, filename should be in UTF-8 encoding

  • It may also be possible to use Glib.filename_to_uri(filename); however, the function we are using 

    attempts to handle relative file paths as well.


Now, to actually create the URI clip asset, we will use the function request_sync()in conjunction with  

filename_to_uri(). We will pass the return value from filename_to_uri() to request_sync()which will 

synchronously create a URI clip asset for the URI we passed as a parameter.

  • In larger applications, it could potentially be dangerous to create URI clips synchronously. It is recommended as 

    best practice to use asset_new()in those cases.

asset = GES.UriClipAsset.request_sync(

           Gst.filename_to_uri(image_filename),

        )

The URI clip asset is now created and stored in the variable called asset.


Remember to_nanoseconds()from earlier? Now is the time where it comes in handy. We would like to get the time, 

in nanoseconds, between the current frame and the next frame. We will store the current frame’s time in the variable 

start_time and the next frame’s time in the variable end_time 

      start_time = to_nanoseconds(frame_number)

      end_time = to_nanoseconds(frame_number + 1)


We now have all of the necessary parameters to add our asset to our layer. The function that we will use to add the 

asset to the layer is GES.Layer.add_asset(). When using add_asset()it requires 6 parameters.

  • The asset to extract the new clip from; In our case, this is the variable asset.

  • The start; In our case, this is the variable start_time.

  • The inpoint; In our case, we want all of our inpoints to be 0.

  • The duration; In our case, this is the start_time subtracted from the end_time.

  • The track types; In our case, we will want to get all of the supported formats for our asset.

    • We will use the function get_supported_formats() on the asset variable in order to get the 

      supported formats.

    • You could also use GES.TrackType.UNKNOWN for only the default formats.

layer.add_asset(

        asset,

        start_time,

        0,

        end_time - start_time,

        asset.get_supported_formats(),

    )


After all of the pictures have been converted to URI clip assets and added to our layer in the correct order, we are ready 

to move on to setting up the pipeline.


Creating the Pipeline

Before creating the pipeline, it is important to understand the job of the pipeline. The pipeline is in charge of managing 

the global clock, as well as providing a “bus” to the application. It is important to know that the pipeline has a variety of 

states that it can be in at any given time. The state can be NULL, READY, PAUSED, and PLAYING. Usually, the clock 

does not need to be manually changed. The clock selection algorithm by default will select a clock by an element that 

is closest to the source. If no clock is provided by an element, Gst.SystemClock is used.


Now onto creating the pipeline. GES makes creating the pipeline itself extremely straightforward, the function 

GES.Pipeline.new() creates a new pipeline and returns it.

pipeline = GES.Pipeline.new()


In order to place our timeline into the pipeline, we will use the function set_timeline(), which has 1 parameter of the 

timeline you want to add to the pipeline. In our case, it is the variable timeline.

  • You should only call this function 1 time on any given pipeline, as a pipeline can only have its timeline set once.

       pipeline.set_timeline(timeline)


Creating the Encoding Profile

Encoding profiles describe the media types and settings that you want to use for a certain encoding process. Typically, 

the top-level encoding profiles are GstPbutils.EncodingContainerProfile which have a readable name as well 

as a description of the container format to use. These profiles reference one or more GstPbutils.EncodingProfile 

which describe what encoding format should be used for each stream. 


It is now time to create our container profile. We will be utilizing the function EncodingContainerProfile.new()to 

create our Encoding Container Profile. 

EncodingContainerProfile.new()has 4 parameters:

  • Name; The name of the profile, in our case we will have it be None.

  • Description; The description of the profile, in our case we will have it be None.

  • Format; The format to use for this profile, in our case we will want to get the capabilities (caps) from the string 

    ‘video/ogg’ using the function from_string().

  • Preset; The preset to use for this profile, in our case we will have it be None.

We are only working on a single video source, therefore we do not need to worry about profile naming.

container_profile = GstPbutils.EncodingContainerProfile.new(

        None,

        None,

        Gst.Caps.from_string('video/ogg'),

        None,

    )


Now that the container profile is created, it is time to add our video profile to the container. We will use the function 

add_profile() to add the new profile to the container profile. We will create the new profile using  

GstPbutils.EncodingVideoProfile.new() which has 4 parameters:

  • Format: The capabilities (caps) of the profile.

    • We will use caps.from_string()to define our caps for the profile. The caps we will define are 

      “video/x-theora”, a width of 1920 (pixels), a height of 1080 (pixels), and a framerate as a function of 

      the FRAME_RATE variable we created earlier.

  • Preset: We do not have a preset, in our case None.

  • Restriction: We do not need to restrict any capabilities, in our case None.

  • Presence: How many times this stream must be used, 0 is any number of times so we will use that in our case.

     container_profile.add_profile(GstPbutils.EncodingVideoProfile.new(

        Gst.Caps.from_string(', '.join([

            'video/x-theora',

            'width=(int)1920',

            'height=(int)1080',

            f'framerate=(fraction){FRAME_RATE}/1',

        ])),

        None,

        None,

        0,

    ))


Now that the capabilities are defined, added to the new profile, and then the profile is added to our container profile, it 

is time to set our render settings for our pipeline. In order to set our render settings, we will use the function 

set_render_settings()which has 2 parameters:

  • Output URI: The URI to save the pipeline’s rendering result to. In our case, we will use the user’s  

    output_filename in conjunction with the function filename_to_uri() to convert the filename to a URI.

  • Profile: The encoding profile to use for rendering the pipeline’s timeline. In our case we will use our container 

    profile that we created during the previous steps.

pipeline.set_render_settings(

        Gst.filename_to_uri(output_filename),

        container_profile,

    )


Riding the Bus

Now we will work on initializing and connecting the bus for the pipeline. The bus is an object that delivers the 

information (messages) from the streaming threads to the application in a first-in first-out (FIFO) method. Prior to 

initializing the bus for the pipeline, we will use the function GLib.MainLoop()to create the main event loop for 

our application.

    loop = GLib.MainLoop()

Next, we will use the function get_bus()to get the bus of the pipeline and save it into a variable named bus. We will 

use the function add_signal_watch() on our bus in order to make the bus emit the signal “message” every time 

a message is posted on the bus.

    bus = pipeline.get_bus()

    bus.add_signal_watch()

We will then use the function connect()to connect the “message” signal to a function, in this case it is a lambda 

function that will quit the main loop if the message is of type Gst.MessageType.EOS which means “end of stream”. 

This connection will allow for our application to terminate, instead of looping forever, when our bus runs out of things 

to do, A.K.A. “end of stream”.

    bus.connect(

        'message',

        lambda unused_bus, message:

            loop.quit() if message.type == Gst.MessageType.EOS else None,

    )


Setting Pipeline Mode and State

Next we will set the mode for our pipeline. In order to set the mode for our pipeline, we will use the function  

set_mode(). It has 1 parameter, which is the mode for the pipeline, in our case we will use the mode 

GES.PipelineFlags.RENDER. This mode will render the pipeline with forced decoding.

    pipeline.set_mode(GES.PipelineFlags.RENDER)

It is also important to set the state of the pipeline prior to starting the main loop of the application. We will use the 

function set_state() to set the state of our pipeline to Gst.State.PLAYING. This state means that the pipeline 

clock is running and data is flowing.

    pipeline.set_state(Gst.State.PLAYING)


Rendering the Video

We will now invoke the function run()on the loop variable we created earlier. This loop will run until the 

“end of stream” type message is received in the loop we created earlier. While this loop is running, the video is being 

rendered by the application.

    loop.run()

Once the loop has terminated, we will want to remove the signal watching from the pipeline’s bus. It is important to 

remove any signal watching, as the functions can be called many times.

    bus.remove_signal_watch()

Finally, we will set the pipeline’s state to Gst.State.NULL using the same function set_state() from before. It is 

important to set the pipeline to the Gst.State.NULL state, because the pipeline then automatically flushes the bus 

to ensure no circular references exist.

    pipeline.set_state(Gst.State.NULL)

Source Code

A github gist of the entire program can be found below. The code comes courtesy of our professor Brady James Garvin.

https://gist.github.com/wblazer/c4e931418f24133ab5a10977084af74d

Comments