Skip to main content

Implementing Adaptive Streaming with GStreamer

Overview

Adaptive streaming is a technique that allows a streaming application to adjust its bitrate, and possibly other parameters like framerate, in real-time based on network conditions and system performance. This helps maintain a balance between video quality and stability, ensuring a smooth viewing experience even under varying conditions.

In this guide, you will learn how to implement adaptive streaming using GStreamer, focusing on dynamically adjusting the video bitrate based on the buffer levels in the pipeline.

Prerequisites

Before getting started, ensure you have the following:

  • GStreamer: Installed on your system. You can install GStreamer using the package manager of your choice:

    • On Ubuntu:

      sudo apt-get install gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
    • On macOS:

      brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav
  • Python 3: Along with the GStreamer Python bindings (PyGObject). You can install them via pip:

    pip install PyGObject

Step 1: Understanding the Pipeline Structure

GStreamer operates by linking together different "elements" into a "pipeline." Each element performs a specific function, such as capturing video, encoding, muxing, and streaming.

Basic Pipeline Structure

Here's the basic structure of a GStreamer pipeline for streaming to an RTMP server:

  • Source: Captures video input (e.g., videotestsrc for testing).
  • Conversion: Converts the video to a format suitable for encoding (e.g., videoconvert).
  • Encoding: Encodes the video into H.264 (e.g., x264enc).
  • Muxing: Packages the video into a suitable container format (e.g., flvmux).
  • Streaming: Streams the packaged video to an RTMP server (e.g., rtmpsink).

Step 2: Adding a Queue for Buffer Management

To enable adaptive streaming, you need to monitor the amount of data waiting in the pipeline. This is done by inserting a queue element, which can buffer data temporarily.

Adding the Queue

The queue will be inserted after the muxer but before the RTMP sink:

after_muxer_queue = Gst.ElementFactory.make("queue", "after_muxer_queue")
after_muxer_queue.set_property("max-size-buffers", 0)
after_muxer_queue.set_property("max-size-bytes", 0)
after_muxer_queue.set_property("max-size-time", 1000000000)

This queue is configured to buffer up to 1 second of data.

Step 3: Implementing Dynamic Bitrate Adjustment

To adjust the bitrate dynamically, you will periodically check the level of the queue and adjust the encoder's bitrate accordingly.

Bitrate Adjustment Logic

The bitrate adjustment logic works as follows:

  • Check the queue level: If the queue level is too high, it indicates that data is being produced faster than it can be sent out, so the bitrate should be reduced.
  • Adjust the bitrate: Decrease the bitrate if the queue is filling up, or increase it if the queue is emptying out.
def adjust_bitrate():
current_level = after_muxer_queue.get_property("current-level-buffers")
queue_level_threshold = 10
adjusting_step = 100
current_bitrate = encoder.get_property("bitrate")

if current_level > queue_level_threshold and current_bitrate > min_bitrate:
new_bitrate = current_bitrate - adjusting_step
encoder.set_property("bitrate", new_bitrate)
print(f"Reduced bitrate to {new_bitrate}")
elif current_level <= queue_level_threshold and current_bitrate < max_bitrate:
new_bitrate = current_bitrate + adjusting_step
encoder.set_property("bitrate", new_bitrate)
print(f"Increased bitrate to {new_bitrate}")

return True

This function is registered to run periodically using GLib.timeout_add_seconds.

Step 4: Creating the GStreamer Pipeline

With all the components in place, you can now create the full pipeline:

def create_pipeline(self):
Gst.init(None)
pipeline = Gst.Pipeline.new("adaptive-pipeline")
source = Gst.ElementFactory.make("videotestsrc", "source")
source.set_property("is-live", True)

video_convert = Gst.ElementFactory.make("videoconvert", "video_convert")
x264enc = Gst.ElementFactory.make("x264enc", "x264enc")
flvmux = Gst.ElementFactory.make("flvmux", "flvmux")
rtmpsink = Gst.ElementFactory.make("rtmpsink", "rtmp_sink")
rtmpsink.set_property("location", self.rtmp_url)

after_muxer_queue = self.create_after_muxer_queue(x264enc, "1080p", True)

pipeline.add(source)
pipeline.add(video_convert)
pipeline.add(x264enc)
pipeline.add(after_muxer_queue)
pipeline.add(flvmux)
pipeline.add(rtmpsink)

if not Gst.Element.link(source, video_convert):
print("Elements could not be linked: source -> video_convert.")
if not Gst.Element.link(video_convert, x264enc):
print("Elements could not be linked: video_convert -> x264enc.")
if not Gst.Element.link(x264enc, after_muxer_queue):
print("Elements could not be linked: x264enc -> after_muxer_queue.")
if not Gst.Element.link(after_muxer_queue, flvmux):
print("Elements could not be linked: after_muxer_queue -> flvmux.")
if not Gst.Element.link(flvmux, rtmpsink):
print("Elements could not be linked: flvmux -> rtmpsink.")

return pipeline

Pipeline Execution

To start and manage the pipeline:

def start_pipeline(self):
if not self.pipeline:
return
self.pipeline.set_state(Gst.State.PLAYING)
bus = self.pipeline.get_bus()
bus.add_signal_watch()
bus.connect("message", self.on_message)
self.loop_thread.start()
print("Streaming to RTMP server...")

def stop_pipeline(self):
if self.pipeline:
self.pipeline.set_state(Gst.State.NULL)
self.loop.quit()
self.loop_thread.join()
print("Pipeline stopped.")

Step 5: Running Diagnostics and Monitoring

You can monitor and test the adaptive streaming setup by running diagnostics:

def run_diagnostics(self):
self.start_pipeline()
time_for_bitrate_adjustment = self.time_period
time_step = time_for_bitrate_adjustment / 100
for percent in range(1, 101):
time.sleep(time_step)
if self.update_callback:
self.update_callback(percent, self.found_bitrate)
self.stop_pipeline()

Callback Function for Monitoring

You can define a callback function to get periodic updates:

def test_callback(percent, bitrate):
if percent is not None:
print(f"Percent: {percent}%")
if bitrate is not None:
print(f"Bitrate: {bitrate} kbps")

Starting the Diagnostics

Run the diagnostics by calling:

if __name__ == '__main__':
rtmp_url = "rtmp://yourserver/live/stream"
start_diagnostics_from_thread(rtmp_url, 120, test_callback).join()

This script will simulate streaming to the RTMP server and adjust the bitrate dynamically based on the queue levels.

Conclusion

This guide has provided a step-by-step approach to implementing adaptive streaming using GStreamer. By monitoring buffer levels and dynamically adjusting the bitrate, you can create a streaming solution that adapts to network conditions in real-time, offering a better and more stable experience for viewers.

This implementation can be extended further by incorporating more sophisticated algorithms for bitrate adjustment or adding additional features like dynamic framerate and resolution changes based on system and network performance.