From 5f4709e498ab77fe0508262e791af529c32062f1 Mon Sep 17 00:00:00 2001 From: Rafael Caricio Date: Sun, 10 Oct 2021 15:07:00 +0200 Subject: [PATCH] Basic example --- .gitignore | 1 + Makefile | 5 ++ README.md | 49 +++++++++++++++ hlssink3_server.py | 153 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 5 files changed, 210 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 hlssink3_server.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index b6e4761..e2fb4d7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ build/ develop-eggs/ dist/ downloads/ +public/ eggs/ .eggs/ lib/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7bcf054 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +.PHONY: clean + +clean: + rm -f public/*.m3u8 + rm -f public/*.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ea1b2f --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# GStreamer HTTP Live Streaming Examples + +This repository contains some examples of usage of the GStreamer HLS plugin `hlssink3`. + +## Available Examples + +### Serving a video file to live HLS + +The example `hlssink3_server.py` reads from a local file and generates a HLS manifest and segment files. The files are +updated in realtime for live streaming. + +## Installation + +Needs Python 3.9 (as of today 2021-10-10). + +Make sure GStreamer is installed and available: +``` +$ pkg-config --print-errors --exists gstreamer-1.0 +``` + +If GStreamer dependency is not installed, please try the those +[installation steps](https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/-/blob/068b078edfa4f2f10e1824b41548c965b710626d/gstreamer/README.md#installation). + +#### MacOS Specific +Make sure `pkg-config` version being used can find all libs installed using `brew`: +``` +$ export PKG_CONFIG_PATH="/usr/local/lib/pkgconfig" +``` + +#### Building Python Bindings +In order to be able to use the GStreamer bindings in a virtualenv. We need to build from source. +Checkout the code from https://gitlab.freedesktop.org/gstreamer/gst-python/-/tree/master +``` +$ meson builddir +$ cd builddir +$ ninja -v +$ ninja install -v +``` + +The `ninja install` will attempt to install, but we need to manually copy the overrides, if we want to use under a virtualenv. +``` +$ cp gi/overrides/_gi_gst.cpython-39-darwin.so ~/.pyenv/versions/gstreamer/lib/python3.9/site-packages/gi/overrides +$ cp ~/development/opensource/gst-python/gi/overrides/GstPbutils.py ~/.pyenv/versions/gstreamer/lib/python3.9/site-packages/gi/overrides +$ cp ~/development/opensource/gst-python/gi/overrides/Gst.py ~/.pyenv/versions/gstreamer/lib/python3.9/site-packages/gi/overrides +``` + +### Additional Info + +Example of sometimes pad: https://github.com/gkralik/python-gst-tutorial/blob/master/basic-tutorial-3.py diff --git a/hlssink3_server.py b/hlssink3_server.py new file mode 100644 index 0000000..4bc6d4d --- /dev/null +++ b/hlssink3_server.py @@ -0,0 +1,153 @@ +import sys +import traceback +import itertools +import logging +from typing import Optional + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst + +logging.basicConfig(level=logging.DEBUG) +log = logging.getLogger('hls_server') + + +def bus_call(_bus, message, loop): + t = message.type + if t == Gst.MessageType.EOS: + print("End-of-stream") + loop.quit() + elif t == Gst.MessageType.ERROR: + err, debug = message.parse_error() + fail(f"Bus error: {err}:{debug}") + loop.quit() + return True + + +def gst_element(element_name: str, alias: Optional[str] = None) -> Gst.Element: + element = Gst.ElementFactory.make(element_name, alias) + if element is None: + fail(f"Could not find element {element_name}") + return element + + +class FileHlsOrigin: + def __init__(self, filename: str) -> None: + if Gst.uri_is_valid(filename): + self.uri = filename + else: + self.uri = Gst.filename_to_uri(filename) + + self.pipeline = Gst.Pipeline.new("FileHlsOrigin") + + self.origin = gst_element("uridecodebin", "origin") + self.origin.set_property('uri', self.uri) + self.origin.connect("pad-added", self.on_origin_pad_added) + + self.videoconvert = gst_element("videoconvert") + self.audioconvert = gst_element("audioconvert") + audio_encoder = gst_element("avenc_aac") + + video_queue = gst_element("queue") + # videoscale = gst_element("videoscale") + video_encoder = gst_element("vtenc_h264_hw") + h264parse = gst_element("h264parse") + + audio_queue = gst_element("queue") + + hlssink3 = gst_element("hlssink3", "hls") + hlssink3.set_property("playlist-location", "master.m3u8") + hlssink3.set_property("target-duration", 6) + hlssink3.set_property("playlist-length", 5) + hlssink3.set_property("max-files", 5) + + self.pipeline.add(self.origin) + self.pipeline.add(self.videoconvert) + self.pipeline.add(self.audioconvert) + self.pipeline.add(audio_encoder) + self.pipeline.add(video_queue) + # self.pipeline.add(videoscale) + self.pipeline.add(video_encoder) + self.pipeline.add(h264parse) + self.pipeline.add(audio_queue) + self.pipeline.add(hlssink3) + + self.pipeline.link(self.origin) + Gst.Element.link_many(self.videoconvert, video_queue, video_encoder, h264parse, hlssink3) + Gst.Element.link_many(self.audioconvert, audio_queue, audio_encoder) + + hls_sink_pad_templ = hlssink3.get_pad_template('audio') + hls_sink = hlssink3.request_pad(hls_sink_pad_templ) + + audio_encoder_src = audio_encoder.get_static_pad('src') + + audio_encoder_src.link(hls_sink) + + def on_origin_pad_added(self, _src, new_pad): + new_pad_caps = new_pad.get_current_caps() + new_pad_struct = new_pad_caps.get_structure(0) + new_pad_type = new_pad_struct.get_name() + + if new_pad_type.startswith("audio/"): + log.info(f"Audio pad added to origin element: {new_pad_type}") + audioconvert_sink = self.audioconvert.get_static_pad('sink') + if audioconvert_sink.is_linked(): + log.warning("Already linked audio contents source. Ignoring..") + new_pad.link(audioconvert_sink) + + elif new_pad_type.startswith('video/'): + log.info(f"Video pad added to origin element: {new_pad_type}") + videoconvert_sink = self.videoconvert.get_static_pad('sink') + if videoconvert_sink.is_linked(): + log.warning("Already linked video contents source. Ignoring..") + return + new_pad.link(videoconvert_sink) + + else: + log.error(f"New unexpected pad added to origin element with type: {new_pad_type}") + + +def main(): + Gst.init(None) + + if len(sys.argv) <= 1: + fail("Needs at least one argument") + + origin = FileHlsOrigin(sys.argv[-1]) + + loop = GObject.MainLoop() + bus = origin.pipeline.get_bus() + bus.add_signal_watch() + bus.connect("message", bus_call, loop) + + # start play back and listed to events + origin.pipeline.set_state(Gst.State.PLAYING) + try: + print("Running pipeline..") + loop.run() + finally: + # cleanup + origin.pipeline.set_state(Gst.State.NULL) + print("All good! ;)") + + +def fail(message: str) -> None: + traceback.print_stack() + print(f"\nFailure: {message}") + sys.exit(1) + + +def pairwise(iterable): + a, b = itertools.tee(iterable) + next(b, None) + return zip(a, b) + + +def link_many(*args): + for pair in pairwise(args): + if not pair[0].link(pair[1]): + fail(f'Failed to link {pair[0]} and {pair[1]}') + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a17efa6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pycairo==1.20.1 +PyGObject==3.42.0