Basic example
This commit is contained in:
parent
84ef90ad66
commit
5f4709e498
5 changed files with 210 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,6 +12,7 @@ build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
|
public/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
lib/
|
||||||
|
|
5
Makefile
Normal file
5
Makefile
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f public/*.m3u8
|
||||||
|
rm -f public/*.ts
|
49
README.md
Normal file
49
README.md
Normal file
|
@ -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
|
153
hlssink3_server.py
Normal file
153
hlssink3_server.py
Normal file
|
@ -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()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pycairo==1.20.1
|
||||||
|
PyGObject==3.42.0
|
Loading…
Reference in a new issue