From e655f904eae9862083765d2ca65ec55c039e7606 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 21 May 2024 22:04:15 +0530 Subject: [PATCH 01/29] =?UTF-8?q?=F0=9F=90=9B=20StreamGear:=20Refactor=20s?= =?UTF-8?q?tream=20copy=20handling=20(Fixes=20#396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’¬ When the output codec is set to "copy" (stream copy mode), certain video processing parameters like "-vf" (video filters) and "-aspect" (aspect ratio) are not supported and can lead to errors and invalid output files. ♻️ This commit refactors the internal `PreProcess` method in StreamGear API to handle the stream copy mode correctly: - πŸ₯… Moved the existing code for setting "-vf" and "-aspect" inside conditional block that checks if the output stream codec is not "copy". - πŸ”Š Added an else block to log warnings and discard "-vf" and "-aspect" in stream copy mode. --- vidgear/gears/streamgear.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 65e623d97..00e5ff051 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -425,11 +425,21 @@ def __PreProcess(self, channels=0, rgb=False): default_codec = "libx264rgb" if rgb else "libx264" output_parameters["-vcodec"] = self.__params.pop("-vcodec", default_codec) # enable optimizations and enforce compatibility - output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") - aspect_ratio = Fraction( - self.__inputwidth / self.__inputheight - ).limit_denominator(10) - output_parameters["-aspect"] = ":".join(str(aspect_ratio).split("/")) + if output_parameters["-vcodec"] != "copy": + # NOTE: these parameters only supported when stream copy not defined + output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") + aspect_ratio = Fraction( + self.__inputwidth / self.__inputheight + ).limit_denominator(10) + output_parameters["-aspect"] = ":".join(str(aspect_ratio).split("/")) + else: + # log warnings for these parameters + self.__params.pop("-vf", False) and logger.warning( + "Filtering and stream copy cannot be used together. Discarding `-vf` parameter!" + ) + self.__params.pop("-aspect", False) and logger.warning( + "Overriding aspect ratio with stream copy may produce invalid files. Discarding `-aspect` parameter!" + ) # w.r.t selected codec if output_parameters["-vcodec"] in [ "libx264", From 33abf4ac38ca9c39fda90bf786509b1ef3191356 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 24 May 2024 10:18:37 +0530 Subject: [PATCH 02/29] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Update=20StreamGea?= =?UTF-8?q?r=20usage=20examples=20for=20device=20audio=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ§‘β€πŸ’» Update the StreamGear usage examples for streaming live audio from an external device - Fixed typo in source, code comments and filename in usage example code. - Updated usage example for device video source. - ✏️ Fixed minor typo in `js_hook.py.` StreamGear: - πŸ’‘ Fix minor typos and formatting issues in code comments for better clarity Helper: - 🩹Update `extract_time` helper function regex to handle milliseconds. --- docs/gears/streamgear/rtfm/usage.md | 16 ++++++++-------- docs/overrides/hooks/js_hook.py | 2 +- vidgear/gears/helper.py | 7 +++---- vidgear/gears/streamgear.py | 10 +++++----- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index be8a38e23..856457571 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -1006,7 +1006,7 @@ The complete example is as follows: !!! failure "If audio still doesn't work then reach us out on [Gitter ➢](https://gitter.im/vidgear/community) Community channel" -!!! danger "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! danger "Make sure this `-audio` audio-source it compatible with provided Device video-source, otherwise you could encounter multiple errors or no output at all." !!! warning "You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in Real-time Frames mode, otherwise audio delay will occur in output streams." @@ -1021,8 +1021,8 @@ The complete example is as follows: from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source="foo1.mp4").start() + # open any valid DEVICE video stream + stream = CamGear(source=0).start() # add various streams, along with custom audio stream_params = { @@ -1039,7 +1039,7 @@ The complete example is as follows: "dshow", "-i", "audio=Microphone (USB2.0 Camera)", - ], # assign appropriate input audio-source device and demuxer + ], # assign appropriate input audio-source device(compatible with video source) and its demuxer } # describe a suitable manifest-file location/name and assign params @@ -1086,8 +1086,8 @@ The complete example is as follows: from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source="foo1.mp4").start() + # open any valid DEVICE video stream + stream = CamGear(source=0).start() # add various streams, along with custom audio stream_params = { @@ -1104,11 +1104,11 @@ The complete example is as follows: "dshow", "-i", "audio=Microphone (USB2.0 Camera)", - ], # assign appropriate input audio-source device and demuxer + ], # assign appropriate input audio-source device(compatible with video source) and its demuxer } # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="dash_out.m3u8", format="hls", **stream_params) + streamer = StreamGear(output="hls_out.m3u8", format="hls", **stream_params) # loop over while True: diff --git a/docs/overrides/hooks/js_hook.py b/docs/overrides/hooks/js_hook.py index 27b72a194..8f0175307 100644 --- a/docs/overrides/hooks/js_hook.py +++ b/docs/overrides/hooks/js_hook.py @@ -22,7 +22,7 @@ from mkdocs.structure.pages import Page js_scripts = """ -, + diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index e900d016c..d0dff4305 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -669,12 +669,11 @@ def extract_time(value): return 0 else: stripped_data = value.strip() - t_duration = re.findall( - r"(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)", stripped_data - ) + t_duration = re.findall(r"\d{2}:\d{2}:\d{2}(?:\.\d{2})?", stripped_data) return ( sum( - int(x) * 60**i for i, x in enumerate(reversed(t_duration[0].split(":"))) + float(x) * 60**i + for i, x in enumerate(reversed(t_duration[0].split(":"))) ) if t_duration else 0 diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 00e5ff051..0c665a69b 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -657,7 +657,7 @@ def __evaluate_streams(self, streams, output_params, bpp): Internal function that Extracts, Evaluates & Validates user-defined streams Parameters: - streams (dict): Indivisual streams formatted as list of dict. + streams (dict): Individual streams formatted as list of dict. output_params (dict): Output FFmpeg parameters """ # temporary streams count variable @@ -693,7 +693,7 @@ def __evaluate_streams(self, streams, output_params, bpp): "{}:a".format(1 if "-core_audio" in output_params else 0), ] - # extract resolution & indivisual dimension of stream + # extract resolution & individual dimension of stream resolution = stream.pop("-resolution", "") dimensions = ( resolution.lower().split("x") @@ -868,12 +868,12 @@ def __generate_dash_stream(self, input_params, output_params): output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) # default behaviour output_params["-seg_duration"] = self.__params.pop("-seg_duration", 20) - # Disable (0) the use of a SegmentTimline inside a SegmentTemplate. + # Disable (0) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 0 else: # default behaviour output_params["-seg_duration"] = self.__params.pop("-seg_duration", 5) - # Enable (1) the use of a SegmentTimline inside a SegmentTemplate. + # Enable (1) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 1 # Finally, some hardcoded DASH parameters (Refer FFmpeg docs for more info.) @@ -944,7 +944,7 @@ def __Build_n_Execute(self, input_params, output_params): ffmpeg_cmd = None hide_banner = ( [] if self.__logging else ["-hide_banner"] - ) # ensuring less cluterring if specified + ) # ensuring less cluttering if specified # format commands if self.__video_source: ffmpeg_cmd = ( From 27c0ff294ebac9198675bc26fced2d277bcd7cec Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 24 May 2024 11:21:37 +0530 Subject: [PATCH 03/29] =?UTF-8?q?=F0=9F=92=A5=20StreamGear:=20Deprecate=20?= =?UTF-8?q?`terminate()`=20method=20and=20introduce=20`close()`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new `close()` method instead, which provides a more descriptive name like in WriteGear API for terminating StreamGear processes safely. - πŸ“Œ Pinned `typing_extensions` dependency to `>=4.7.1` for using the `@deprecated` decorator. See issue https://github.com/tiangolo/fastapi/discussions/9808 - πŸ—‘οΈ Deprecate the `terminate()` method in StreamGear and added backward compatibility. - ⚑️ Introduce a new `close()` method to safely terminate StreamGear processes - πŸ“ Minor formatting and docstring updates --- setup.py | 2 ++ vidgear/gears/streamgear.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index d1c35c53f..b368e05f4 100644 --- a/setup.py +++ b/setup.py @@ -100,6 +100,8 @@ def latest_version(package_name): "requests", "colorlog", "tqdm", + # typing_extensions for `deprecated` decorator + "typing_extensions>=4.7.1", ] + (["opencv-python"] if test_opencv() else []), long_description=long_description, diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 0c665a69b..c39338177 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -30,6 +30,7 @@ from tqdm import tqdm from fractions import Fraction from collections import OrderedDict +from typing_extensions import deprecated # import helper packages from .helper import ( @@ -1048,23 +1049,38 @@ def __exit__(self, exc_type, exc_val, exc_tb): """ Handles exit with the `with` statement. See [PEP343 -- The 'with' statement'](https://peps.python.org/pep-0343/). """ - self.terminate() + self.close() + @deprecated( + "The `terminate()` method will be removed in the next release. Kindly use `close()` method instead." + ) def terminate(self): """ - Safely terminates StreamGear. + !!! warning "[DEPRECATION NOTICE]: This method will be removed in the next release. Kindly use `close()` method instead." + + This function simply provides backward compatibility with the old `terminate()` function. + It simply calls the new `close()` method to terminate various StreamGear process. + """ + + self.close() + + def close(self): + """ + Safely terminates various StreamGear process. """ + # log termination + if self.__logging: + logger.debug("Terminating StreamGear Processes.") # return if no process was initiated at first place if self.__process is None or not (self.__process.poll() is None): return # close `stdin` output - if self.__process.stdin: - self.__process.stdin.close() - # force terminate if external audio source - if isinstance(self.__audio, list): - self.__process.terminate() - # wait if still process is still processing some information + self.__process.stdin and self.__process.stdin.close() + # close `stdout` output + self.__process.stdout and self.__process.stdout.close() + # wait if process is still processing self.__process.wait() + # discard process self.__process = None # log it logger.critical( From ec6ae3d847cfc3396954108468aa5a46d374ffce Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 24 May 2024 11:29:27 +0530 Subject: [PATCH 04/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Setup:=20Update?= =?UTF-8?q?=20`setup.py`=20to=20use=20the=20latest=20versions=20of=20pyzmq?= =?UTF-8?q?=20(Fixes=20#399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ’¬ pyzmq version `24.0.1` has a bug with Cython, and it breaks the installation process. See issue [cython/cython#5238](https://github.com/cython/cython/issues/5238). --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b368e05f4..e8d1c48d7 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,7 @@ def latest_version(package_name): # API specific deps "core": [ "yt_dlp{}".format(latest_version("yt_dlp")), - "pyzmq==24.0.1", + "pyzmq{}".format(latest_version("pyzmq")), "Pillow", "simplejpeg{}".format(latest_version("simplejpeg")), "mss{}".format(latest_version("mss")), @@ -126,7 +126,7 @@ def latest_version(package_name): # API specific + Asyncio deps "asyncio": [ "yt_dlp{}".format(latest_version("yt_dlp")), - "pyzmq==24.0.1", + "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format(latest_version("simplejpeg")), "mss{}".format(latest_version("mss")), "Pillow", From 9e537d19d768fd8b63c28887abdda41ccc30c58f Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 29 May 2024 23:50:40 +0530 Subject: [PATCH 05/29] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Update=20StreamGea?= =?UTF-8?q?r=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’¬ The updates aim to provide a better understanding of the StreamGear API's functionality, modes of operation, and usage scenarios. The documentation now includes more detailed explanations, practical examples, and best practices for working with StreamGear in various situations. - πŸ“ Improve the overview section's description and wording. - πŸ§‘β€πŸ’» Update usage examples for both Single-Source Mode and Real-time Frames Mode. - ♻️ Refactored sections for Live Streaming and RGB Mode usage. - ♿️ Clarify warnings, alerts, and important information. - 🎨 Fix markdown formatting and code highlighting issues. - πŸ—‘οΈ Addressed deprecation of the `terminate()` method in favor of new `close()` method. - 🚸 Enhance overall clarity and readability of the documentation. WriteGear: - ⚑️ Simplified the logic for formatting output parameters. CI: - πŸ‘· Updated Streamgear tests to use new `close()` method instead of deprecated `terminate()`. --- README.md | 14 +- docs/gears/streamgear/introduction.md | 26 +- docs/gears/streamgear/rtfm/overview.md | 15 +- docs/gears/streamgear/rtfm/usage.md | 311 +++++++++--------- docs/gears/streamgear/ssm/overview.md | 4 +- docs/gears/streamgear/ssm/usage.md | 179 +++++----- vidgear/gears/writegear.py | 4 +- vidgear/tests/streamer_tests/test_IO_rtf.py | 10 +- vidgear/tests/streamer_tests/test_IO_ss.py | 12 +- vidgear/tests/streamer_tests/test_init.py | 6 +- .../streamer_tests/test_streamgear_modes.py | 16 +- vidgear/tests/test_helper.py | 2 +- 12 files changed, 288 insertions(+), 311 deletions(-) diff --git a/README.md b/README.md index ac9429546..a0aa327d2 100644 --- a/README.md +++ b/README.md @@ -427,21 +427,21 @@ In addition to this, WriteGear also provides flexible access to [**OpenCV's Vide NetGear API

-> _StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH and Apple HLS) in just few lines of python code._ +> _StreamGear streamlines and simplifies the transcoding workflow to generate Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats like MPEG-DASH and Apple HLS with just a few lines of Python code, allowing developers to focus on their application logic rather than dealing with the complexities of transcoding and chunking media files._ -StreamGear provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**][ffmpeg] multimedia framework for generating chunked-encoded media segments of the content. +StreamGear API provides a standalone, highly extensible, and flexible wrapper around the [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunk-encoded media segments from your multimedia content effortlessly. -SteamGear is an out-of-the-box solution for transcoding source videos/audio files & real-time video frames and breaking them into a sequence of multiple smaller chunks/segments of suitable lengths. These segments make it possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. +With StreamGear, you can transcode source video/audio files and real-time video frames into a sequence of multiple smaller chunks/segments of suitable lengths. These segments facilitate streaming at different quality levels _(bitrates or spatial resolutions)_ and allow for seamless switching between quality levels during playback based on available bandwidth. You can serve these segments on a web server, making them easily accessible via standard **HTTP GET** requests. -SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. But, Multiple DRM support is yet to be implemented. +SteamGear currently supports both [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Master Playlist _(such as M3U8 in-case of Apple HLS)_ besides segments that describe these segment information _(timing, URL, media characteristics like video resolution and bit rates)_ and is provided to the client before the streaming session. +Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrates_. They are provided to the client before the streaming session begins. **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** -- **Single-Source Mode:** In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. **_Learn more about this mode [here ➢][ss-mode-doc]_** +- **Single-Source Mode :cd: :** In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. **_Learn more about this mode [here ➢][ss-mode-doc]_** -- **Real-time Frames Mode:** In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. **_Learn more about this mode [here ➢][rtf-mode-doc]_** +- **Real-time Frames Mode :film_frames: :** In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. **_Learn more about this mode [here ➢][rtf-mode-doc]_** ### StreamGear API Guide: diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 8a12c3b12..e93fc8b01 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -30,15 +30,15 @@ limitations under the License. ## Overview -> StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH and Apple HLS)_ in just few lines of python code. +> StreamGear streamlines and simplifies the transcoding workflow to generate _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats like MPEG-DASH and Apple HLS_ with just a few lines of Python code, allowing developers to focus on their application logic rather than dealing with the complexities of transcoding and chunking media files. -StreamGear provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunked-encoded media segments of the content. +StreamGear API provides a standalone, highly extensible, and flexible wrapper around the [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunk-encoded media segments from your multimedia content effortlessly. -SteamGear is an out-of-the-box solution for transcoding source videos/audio files & real-time video frames and breaking them into a sequence of multiple smaller chunks/segments of suitable lengths. These segments make it possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. +With StreamGear, you can transcode source video/audio files and real-time video frames into a sequence of multiple smaller chunks/segments of suitable lengths. These segments facilitate streaming at different quality levels _(bitrates or spatial resolutions)_ and allow for seamless switching between quality levels during playback based on available bandwidth. You can serve these segments on a web server, making them easily accessible via standard **HTTP GET** requests. -SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. +SteamGear currently supports both [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Master Playlist _(such as M3U8 in-case of Apple HLS)_ besides segments that describe these segment information _(timing, URL, media characteristics like video resolution and adaptive bit rates)_ and is provided to the client before the streaming session. +Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrates_. They are provided to the client before the streaming session begins. !!! alert "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead." @@ -59,9 +59,9 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast !!! tip "Useful Links" - - Checkout [this detailed blogpost](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works. - - Checkout [this detailed blogpost](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) on how HLS works. - - Checkout [this detailed blogpost](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) for HLS vs. MPEG-DASH comparison. + - Checkout [this detailed blogpost ➢](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works. + - Checkout [this detailed blogpost ➢](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) on how HLS works. + - Checkout [this detailed blogpost ➢](https://imagekit.io/blog/hls-vs-dash/) for HLS vs. MPEG-DASH comparison.   @@ -71,14 +71,12 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast StreamGear primarily operates in following independent modes for transcoding: -??? warning "Real-time Frames Mode is NOT Live-Streaming." +???+ alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout [this usage example ➢](../rtfm/usage/#bare-minimum-usage-with-live-streaming) for more information. - Rather, you can enable live-streaming in Real-time Frames Mode by using the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter in StreamGear API. Checkout [this usage example](../rtfm/usage/#bare-minimum-usage-with-live-streaming) for more information. +- [**Single-Source Mode :material-file-video-outline:**](../ssm/overview) : In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. - -- [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. - -- [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. +- [**Real-time Frames Mode :material-camera-burst:**](../rtfm/overview) : In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams.   diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index 13e78154b..f213821cb 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -18,7 +18,7 @@ limitations under the License. =============================================== --> -# StreamGear API: Real-time Frames Mode +# StreamGear API: Real-time Frames Mode :material-camera-burst:
@@ -44,18 +44,15 @@ For this mode, StreamGear API provides exclusive [`stream()`](../../../../bonus/ Apple HLS support was added in `v0.2.2`. -!!! alert "Real-time Frames Mode is NOT Live-Streaming." +!!! alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout [this usage example ➢](../usage/#bare-minimum-usage-with-live-streaming) for more information. - Rather, you can easily enable live-streaming in Real-time Frames Mode by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. Checkout its [usage example here](../usage/#bare-minimum-usage-with-live-streaming). +!!! danger "Please Remember :material-police-badge-outline:" -!!! danger + * Using [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function instead of [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) in Real-time Frames Mode will immediately result in **`RuntimeError`**! - * Using [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function instead of [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) in Real-time Frames Mode will instantly result in **`RuntimeError`**! - - * **NEVER** assign anything to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, otherwise [Single-Source Mode](../#a-single-source-mode) may get activated, and as a result, using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function will throw **`RuntimeError`**! - - * You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in this mode, otherwise audio delay will occur in output streams. + * **NEVER** assign anything to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, otherwise [Single-Source Mode](../#a-single-source-mode) get activated, and as a result, using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function will throw **`RuntimeError`**! * Input framerate defaults to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined. diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 856457571..196e6427a 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -18,22 +18,23 @@ limitations under the License. =============================================== --> -# StreamGear API Usage Examples: Real-time Frames Mode +# StreamGear API Usage Examples: Real-time Frames Mode :material-camera-burst: -!!! alert "Real-time Frames Mode is NOT Live-Streaming." +!!! alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" - Rather you can easily enable live-streaming in Real-time Frames Mode by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. Checkout following [usage example](#bare-minimum-usage-with-live-streaming). + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout following [usage example ➢](#bare-minimum-usage-with-live-streaming) for more information. -!!! warning "Important Information" +!!! warning "Important Information :fontawesome-solid-person-military-pointing:" - * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➢](../../ffmpeg_install/) for its installation. + - [x] StreamGear API **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➢](../../ffmpeg_install/) for its installation. API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + - [x] In this mode, ==API by default generates a primary stream _(at the index `0`)_ of same resolution as the input frames and at default framerate[^1].== + - [x] In this mode, API **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. + - [x] Always use `close()` function at the very end of the main code. - * StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - - * By default, ==StreamGear generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== - - * Always use `terminate()` function at the very end of the main code. +??? danger "[DEPRECATION NOTICE]: The `terminate()` method in StreamGear is now deprecated." + + The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➢](../../../help/streamgear_ex/)" @@ -47,6 +48,7 @@ Following is the bare-minimum code you need to get started with StreamGear API i !!! note "We are using [CamGear](../../../camgear/overview/) in this Bare-Minimum example, but any [VideoCapture Gear](../../../#a-videocapture-gears) will work in the similar manner." +!!! danger "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." === "DASH" @@ -94,7 +96,7 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -143,41 +145,36 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` !!! success "After running this bare-minimum example, StreamGear will produce a Manifest file _(`dash.mpd`)_ with streamable chunks that contains information about a Primary Stream of same resolution and framerate[^1] as input _(without any audio)_." -   -## Bare-Minimum Usage with Live-Streaming - -You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, where chunks will contain information for few new frames only and forgets all previous ones), using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: +## Bare-Minimum Usage with controlled Input-framerate -!!! note "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." +> In Real-time Frames Mode, StreamGear API provides the exclusive [`-input_framerate`](../../params/#a-exclusive-parameters) attribute for the `stream_params` dictionary parameter, which allows you to set the assumed constant framerate for incoming frames. -=== "DASH" +In this example, we will retrieve the framerate from a webcam video stream and set it as the value for the `-input_framerate` attribute in StreamGear. - !!! tip "Chunk size in DASH" - Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks in DASH stream. Less these value, less will be latency. +!!! danger "Remember, the input framerate defaults to 25.0 fps if the `-input_framerate` attribute value is not defined in Real-time Frames mode." - !!! alert "After every few chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in manifest will contain NO information of any older ones, and therefore resultant DASH stream will play only the most recent frames." +=== "DASH" - ```python linenums="1" hl_lines="11" + ```python linenums="1" hl_lines="10" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(from web-camera attached at index `0`) + # Open live video stream on webcam at first index(i.e. 0) device stream = CamGear(source=0).start() - # enable livestreaming and retrieve framerate from CamGear Stream and - # pass it as `-input_framerate` parameter for controlled framerate - stream_params = {"-input_framerate": stream.framerate, "-livestream": True} + # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value + stream_params = {"-input_framerate":stream.framerate} - # describe a suitable manifest-file location/name + # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) # loop over @@ -210,31 +207,24 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - !!! tip "Chunk size in HLS" - - Use `-hls_init_time` & `-hls_time` FFmpeg parameters for controlling number of frames to be kept in Chunks in HLS stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in playlist will contain NO information of any older ones, and therefore resultant HLS stream will play only the most recent frames." - - ```python linenums="1" hl_lines="11" + ```python linenums="1" hl_lines="10" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(from web-camera attached at index `0`) + # Open live video stream on webcam at first index(i.e. 0) device stream = CamGear(source=0).start() - # enable livestreaming and retrieve framerate from CamGear Stream and - # pass it as `-input_framerate` parameter for controlled framerate - stream_params = {"-input_framerate": stream.framerate, "-livestream": True} + # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value + stream_params = {"-input_framerate":stream.framerate} - # describe a suitable manifest-file location/name + # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) # loop over @@ -267,31 +257,40 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` -   -## Bare-Minimum Usage with RGB Mode +## Bare-Minimum Usage with Live-Streaming -In Real-time Frames Mode, StreamGear API provide [`rgb_mode`](../../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) boolean parameter with its `stream()` function, which if enabled _(i.e. `rgb_mode=True`)_, specifies that incoming frames are of RGB format _(instead of default BGR format)_, thereby also known as ==RGB Mode==. +You can easily activate **Low-latency Live-Streaming :material-video-wireless-outline:** in Real-time Frames Mode, where chunks will contain information for new frames only and forget previous ones, using the exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. +The complete example is as follows: -The complete usage example is as follows: +!!! danger "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." === "DASH" - ```python linenums="1" hl_lines="28" + !!! tip "Controlling chunk size in DASH" + To control the number of frames kept in Chunks for the DASH stream _(controlling latency)_, you can use the `-window_size` and `-extra_window_size` FFmpeg parameters. Lower values for these parameters will result in lower latency. + + !!! alert "After every few chunks _(equal to the sum of `-window_size` and `-extra_window_size` values)_, all chunks will be overwritten while Live-Streaming. This means that newer chunks in the manifest will contain NO information from older chunks, and the resulting DASH stream will only play the most recent frames, reducing latency." + + ```python linenums="1" hl_lines="11" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source='foo1.mp4').start() + # open any valid video stream(from web-camera attached at index `0`) + stream = CamGear(source=0).start() + + # enable livestreaming and retrieve framerate from CamGear Stream and + # pass it as `-input_framerate` parameter for controlled framerate + stream_params = {"-input_framerate": stream.framerate, "-livestream": True} # describe a suitable manifest-file location/name - streamer = StreamGear(output="dash_out.mpd") + streamer = StreamGear(output="dash_out.mpd", **stream_params) # loop over while True: @@ -303,13 +302,10 @@ The complete usage example is as follows: if frame is None: break - - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] - + # {do something with the frame here} # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode + streamer.stream(frame) # Show output window cv2.imshow("Output Frame", frame) @@ -326,22 +322,31 @@ The complete usage example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="28" + !!! tip "Controlling chunk size in HLS" + To control the number of frames kept in Chunks for the HLS stream _(controlling latency)_, you can use the `-hls_init_time` & `-hls_time` FFmpeg parameters. Lower values for these parameters will result in lower latency. + + !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten while Live-Streaming. This means that newer chunks in the master playlist will contain NO information from older chunks, and the resulting HLS stream will only play the most recent frames, reducing latency." + + ```python linenums="1" hl_lines="11" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source='foo1.mp4').start() + # open any valid video stream(from web-camera attached at index `0`) + stream = CamGear(source=0).start() + + # enable livestreaming and retrieve framerate from CamGear Stream and + # pass it as `-input_framerate` parameter for controlled framerate + stream_params = {"-input_framerate": stream.framerate, "-livestream": True} # describe a suitable manifest-file location/name - streamer = StreamGear(output="hls_out.m3u8", format = "hls") + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) # loop over while True: @@ -353,13 +358,10 @@ The complete usage example is as follows: if frame is None: break - - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] - + # {do something with the frame here} # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode + streamer.stream(frame) # Show output window cv2.imshow("Output Frame", frame) @@ -376,37 +378,31 @@ The complete usage example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   -## Bare-Minimum Usage with controlled Input-framerate - -In Real-time Frames Mode, StreamGear API provides exclusive [`-input_framerate`](../../params/#a-exclusive-parameters) attribute for its `stream_params` dictionary parameter, that allow us to set the assumed constant framerate for incoming frames. - -In this example, we will retrieve framerate from webcam video-stream, and set it as value for `-input_framerate` attribute in StreamGear: +## Bare-Minimum Usage with RGB Mode -!!! danger "Remember, Input framerate default to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined in Real-time Frames mode." +In Real-time Frames Mode, StreamGear API provide [`rgb_mode`](../../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) boolean parameter with its `stream()` function, which if enabled _(i.e. `rgb_mode=True`)_, specifies that incoming frames are of RGB format _(instead of default BGR format)_, thereby also known as ==RGB Mode==. +The complete usage example is as follows: === "DASH" - ```python linenums="1" hl_lines="10" + ```python linenums="1" hl_lines="28" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # Open live video stream on webcam at first index(i.e. 0) device - stream = CamGear(source=0).start() - - # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value - stream_params = {"-input_framerate":stream.framerate} + # open any valid video stream(for e.g `foo1.mp4` file) + stream = CamGear(source='foo1.mp4').start() - # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="dash_out.mpd", **stream_params) + # describe a suitable manifest-file location/name + streamer = StreamGear(output="dash_out.mpd") # loop over while True: @@ -419,11 +415,12 @@ In this example, we will retrieve framerate from webcam video-stream, and set it break - # {do something with the frame here} + # {simulating RGB frame for this example} + frame_rgb = frame[:,:,::-1] # send frame to streamer - streamer.stream(frame) + streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode # Show output window cv2.imshow("Output Frame", frame) @@ -440,25 +437,22 @@ In this example, we will retrieve framerate from webcam video-stream, and set it stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="10" + ```python linenums="1" hl_lines="28" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # Open live video stream on webcam at first index(i.e. 0) device - stream = CamGear(source=0).start() - - # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value - stream_params = {"-input_framerate":stream.framerate} + # open any valid video stream(for e.g `foo1.mp4` file) + stream = CamGear(source='foo1.mp4').start() - # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + # describe a suitable manifest-file location/name + streamer = StreamGear(output="hls_out.m3u8", format = "hls") # loop over while True: @@ -471,11 +465,12 @@ In this example, we will retrieve framerate from webcam video-stream, and set it break - # {do something with the frame here} + # {simulating RGB frame for this example} + frame_rgb = frame[:,:,::-1] # send frame to streamer - streamer.stream(frame) + streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode # Show output window cv2.imshow("Output Frame", frame) @@ -492,18 +487,19 @@ In this example, we will retrieve framerate from webcam video-stream, and set it stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` +   ## Bare-Minimum Usage with OpenCV -You can easily use StreamGear API directly with any other Video Processing library(_For e.g. [OpenCV](https://github.com/opencv/opencv) itself_) in Real-time Frames Mode. +> You can easily use the StreamGear API directly with any other Video Processing library _(for e.g. [OpenCV](https://github.com/opencv/opencv))_ in Real-time Frames Mode. -The complete usage example is as follows: +The following is a complete StreamGear API usage example with OpenCV: -!!! tip "This just a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature/example will work in the similar manner." +!!! note "This is a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature or example will work in a similar manner." === "DASH" @@ -532,7 +528,6 @@ The complete usage example is as follows: # lets convert frame to gray for this example gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # send frame to streamer streamer.stream(gray) @@ -551,7 +546,7 @@ The complete usage example is as follows: stream.release() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -581,7 +576,6 @@ The complete usage example is as follows: # lets convert frame to gray for this example gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # send frame to streamer streamer.stream(gray) @@ -600,33 +594,35 @@ The complete usage example is as follows: stream.release() # safely close streamer - streamer.terminate() + streamer.close() ``` -   ## Usage with Additional Streams -Similar to Single-Source Mode, you can easily generate any number of additional Secondary Streams of variable bitrates or spatial resolutions, using exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to add each resolution and bitrate/framerate as list of dictionaries to this attribute, and rest is done automatically. +> Similar to Single-Source Mode, in addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrate or spatial resolution, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. + +To generate Secondary Streams, add each desired resolution and bitrate/framerate as a list of dictionaries to the `-streams` attribute. StreamGear will handle the rest automatically. The complete example is as follows: !!! info "A more detailed information on `-streams` attribute can be found [here ➢](../../params/#a-exclusive-parameters)" -The complete example is as follows: +!!! alert "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." -??? danger "Important `-streams` attribute Information" - * On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate[^1] as the input, at the index `0`. - * :warning: Make sure your System/Machine/Server/Network is able to handle these additional streams, discretion is advised! - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. +???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" -!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + * In addition to the user-defined Secondary Streams, StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution as the input frames and at default framerate[^1]. + * :warning: Ensure that your system, machine, server, or network can handle the additional resource requirements of the Secondary Streams. Exercise discretion when configuring multiple streams. + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. +!!! failure "Always use the `-streams` attribute to define additional streams safely. Duplicate or incorrect definitions can break the transcoding pipeline and corrupt the output chunks." === "DASH" - ```python linenums="1" hl_lines="11-15" + ```python linenums="1" hl_lines="12-14" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -638,8 +634,8 @@ The complete example is as follows: # define various streams stream_params = { "-streams": [ - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "1280x720", "-framerate": 30.0}, # Stream1: 1280x720 at 30fps framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps framerate {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate ], } @@ -657,10 +653,8 @@ The complete example is as follows: if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -679,12 +673,12 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="11-15" + ```python linenums="1" hl_lines="12-14" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -696,8 +690,8 @@ The complete example is as follows: # define various streams stream_params = { "-streams": [ - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "1280x720", "-framerate": 30.0}, # Stream1: 1280x720 at 30fps framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps framerate {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate ], } @@ -715,10 +709,8 @@ The complete example is as follows: if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -737,23 +729,22 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with File Audio-Input -In Real-time Frames Mode, if you want to add audio to your streams, you've to use exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to input the path of your audio file to this attribute as `string` value, and the API will automatically validate as well as maps it to all generated streams. +> In Real-time Frames Mode, if you want to add audio to your streams, you need to use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -The complete example is as follows: +To add a audio source, provide the path to your audio file as a string to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: -!!! failure "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the input video source. Incompatibility can cause multiple errors or result in no output at all." !!! warning "You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in Real-time Frames mode, otherwise audio delay will occur in output streams." -!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➢](../../params/#a-exclusive-parameters)" - +!!! tip "You can also assign a valid audio URL as input instead of a file path. More details can be found [here ➢](../../params/#a-exclusive-parameters)" === "DASH" @@ -774,7 +765,7 @@ The complete example is as follows: {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac" # assign external audio-source } # describe a suitable manifest-file location/name and assign params @@ -812,7 +803,7 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -834,7 +825,7 @@ The complete example is as follows: {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac" # assign external audio-source } # describe a suitable manifest-file location/name and assign params @@ -872,25 +863,26 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with Device Audio-Input -In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter for streaming live audio from external device. You just need to format your audio device name followed by suitable demuxer as `list` and assign to this attribute, and the API will automatically validate as well as map it to all generated streams. +> In Real-time Frames Mode, you can also use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter for streaming live audio from an external device. -The complete example is as follows: +To stream live audio, format your audio device name followed by a suitable demuxer as a list, and assign it to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: +!!! alert "Example Assumptions :octicons-checklist-24:" -!!! alert "Example Assumptions" + - [x] You're running a Windows machine with all necessary audio drivers and software installed. + - [x] There's an audio device named "Microphone (USB2.0 Camera)" connected to your Windows machine. Check instructions below to use device sources with the `-audio` attribute on different OS platforms. - * You're running are Windows machine with all neccessary audio drivers and software installed. - * There's a audio device with named `"Microphone (USB2.0 Camera)"` connected to your windows machine. +??? info "Using devices sources with `-audio` attribute on different OS platforms" -??? tip "Using devices with `-audio` attribute on different OS platforms" + To use device sources with the `-audio` attribute on different OS platforms, follow these instructions: === ":fontawesome-brands-windows: Windows" @@ -1005,17 +997,15 @@ The complete example is as follows: !!! failure "If audio still doesn't work then reach us out on [Gitter ➢](https://gitter.im/vidgear/community) Community channel" +!!! tip "It is advised to use this example with live-streaming enabled(`True`) by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." -!!! danger "Make sure this `-audio` audio-source it compatible with provided Device video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the video source device. Incompatibility can cause multiple errors or result in no output at all." !!! warning "You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in Real-time Frames mode, otherwise audio delay will occur in output streams." -!!! note "It is advised to use this example with live-streaming enabled(True) by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." - - === "DASH" - ```python linenums="1" hl_lines="18-24" + ```python linenums="1" hl_lines="18-25" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -1028,12 +1018,13 @@ The complete example is as follows: stream_params = { "-streams": [ { - "-resolution": "1280x720", + "-resolution": "640x360", "-video_bitrate": "4000k", - }, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps + }, # Stream1: 640x360 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 30.0}, # Stream2: 320x240 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-livestream": True, "-audio": [ "-f", "dshow", @@ -1075,12 +1066,12 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="18-24" + ```python linenums="1" hl_lines="18-25" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -1093,12 +1084,13 @@ The complete example is as follows: stream_params = { "-streams": [ { - "-resolution": "1280x720", + "-resolution": "640x360", "-video_bitrate": "4000k", - }, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps + }, # Stream1: 640x360 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 30.0}, # Stream2: 320x240 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-livestream": True, "-audio": [ "-f", "dshow", @@ -1140,21 +1132,20 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with Hardware Video-Encoder +> In Real-time Frames Mode, you can easily change the video encoder according to your requirements by passing the `-vcodec` FFmpeg parameter as an attribute in the `stream_params` dictionary parameter. Additionally, you can specify additional properties, features, and optimizations for your system's GPU. -In Real-time Frames Mode, you can also easily change encoder as per your requirement just by passing `-vcodec` FFmpeg parameter as an attribute in `stream_params` dictionary parameter. In addition to this, you can also specify the additional properties/features/optimizations for your system's GPU similarly. +In this example, we will be using `h264_vaapi` as our Hardware Encoder and specifying the device hardware's location and compatible video filters by formatting them as attributes in the `stream_params` dictionary parameter. -In this example, we will be using `h264_vaapi` as our hardware encoder and also optionally be specifying our device hardware's location (i.e. `'-vaapi_device':'/dev/dri/renderD128'`) and other features such as `'-vf':'format=nv12,hwupload'` like properties by formatting them as `option` dictionary parameter's attributes, as follows: +!!! warning "This example is just conveying the idea of how to use FFmpeg's hardware encoders with the StreamGear API in Real-time Frames Mode, which MAY OR MAY NOT suit your system. Please use suitable parameters based on your supported system and FFmpeg configurations only." -!!! warning "Check VAAPI support" - - **This example is just conveying the idea on how to use FFmpeg's hardware encoders with WriteGear API in Compression mode, which MAY/MAY-NOT suit your system. Kindly use suitable parameters based your supported system and FFmpeg configurations only.** +??? danger "Check VAAPI support" To use `h264_vaapi` encoder, remember to check if its available and your FFmpeg compiled with VAAPI support. You can easily do this by executing following one-liner command in your terminal, and observing if output contains something similar as follows: @@ -1189,7 +1180,7 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also ], "-vcodec": "h264_vaapi", # define custom Video encoder "-vaapi_device": "/dev/dri/renderD128", # define device location - "-vf": "format=nv12,hwupload", # define video pixformat + "-vf": "format=nv12,hwupload", # define video filters } # describe a suitable manifest-file location/name and assign params @@ -1227,7 +1218,7 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -1288,10 +1279,10 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   [^1]: - :bulb: In Real-time Frames Mode, the Primary Stream's framerate defaults to [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value, if defined, else it will be 25fps. \ No newline at end of file + :bulb: In Real-time Frames Mode, the Primary Stream's framerate defaults to the value of the [`-input_framerate`](../../params/#a-exclusive-parameters) attribute, if defined. Otherwise, it will be set to 25 fps. \ No newline at end of file diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 83e899b12..bbcdc32ce 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -18,7 +18,7 @@ limitations under the License. =============================================== --> -# StreamGear API: Single-Source Mode +# StreamGear API: Single-Source Mode :material-file-video-outline:
Single-Source Mode Flow Diagram @@ -45,7 +45,7 @@ This mode can be easily activated by assigning suitable video path as input to [ Apple HLS support was added in `v0.2.2`. -!!! warning +!!! danger "Please Remember :material-police-badge-outline:" * Using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function instead of [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) in Single-Source Mode will instantly result in **`RuntimeError`**! * Any invalid value to the [`-video_source`](../../params/#a-exclusive-parameters) attribute will result in **`AssertionError`**! diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 6a83d82ff..19e5096ba 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -18,18 +18,17 @@ limitations under the License. =============================================== --> -# StreamGear API Usage Examples: Single-Source Mode +# StreamGear API Usage Examples: Single-Source Mode :material-file-video-outline: -!!! warning "Important Information" +!!! warning "Important Information :fontawesome-solid-person-military-pointing:" - * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➢](../../ffmpeg_install/) for its installation. - - * StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - - * By default, ==StreamGear generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== - - * Always use `terminate()` function at the very end of the main code. + - [x] StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➢](../../ffmpeg_install/) for its installation. API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + - [x] In this mode, ==API auto generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== + - [x] In this mode, if input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams. + - [x] Always use `close()` function at the very end of the main code. +??? danger "[DEPRECATION NOTICE]: The `terminate()` method in StreamGear is now deprecated." + The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➢](../../../help/streamgear_ex/)" @@ -40,7 +39,7 @@ limitations under the License. Following is the bare-minimum code you need to get started with StreamGear API in Single-Source Mode: -!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." +!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams." === "DASH" @@ -52,12 +51,14 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream_params = {"-video_source": "foo.mp4"} # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` + !!! success "After running this bare-minimum example, StreamGear will produce a Manifest file (`dash_out.mpd`) with streamable chunks, containing information about a Primary Stream with the same resolution and framerate as the input." + === "HLS" ```python linenums="1" @@ -68,14 +69,14 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream_params = {"-video_source": "foo.mp4"} # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` + !!! success "After running this bare-minimum example, StreamGear will produce a Master Playlist file (`hls_out.mpd`) with streamable chunks, containing information about a Primary Stream with the same resolution and framerate as the input." -!!! success "After running this bare-minimum example, StreamGear will produce a Manifest file _(`dash.mpd`)_ with streamable chunks that contains information about a Primary Stream of same resolution and framerate as the input."   @@ -100,10 +101,10 @@ You can easily activate ==Low-latency Livestreaming in Single-Source Mode== - ch stream_params = {"-video_source": 0, "-livestream": True} # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" @@ -122,34 +123,34 @@ You can easily activate ==Low-latency Livestreaming in Single-Source Mode== - ch stream_params = {"-video_source": 0, "-livestream": True} # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ```   ## Usage with Additional Streams -In addition to Primary Stream, you can easily generate any number of additional Secondary Streams of variable bitrates or spatial resolutions, using exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to add each resolution and bitrate/framerate as list of dictionaries to this attribute, and rest is done automatically. +> In addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrates or spatial resolutions, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -!!! info "A more detailed information on `-streams` attribute can be found [here ➢](../../params/#a-exclusive-parameters)" +To generate Secondary Streams, add each desired resolution and bitrate/framerate as a list of dictionaries to the `-streams` attribute. StreamGear will handle the rest automatically. The complete example is as follows: -The complete example is as follows: +!!! info "A more detailed information on `-streams` attribute can be found [here ➢](../../params/#a-exclusive-parameters)" -!!! note "If input video-source contains any audio stream/channel, then it automatically gets assigned to all generated streams without any extra efforts." +!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." -??? danger "Important `-streams` attribute Information" - - * On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate as the input, at the index `0`. - * :warning: Make sure your System/Machine/Server/Network is able to handle these additional streams, discretion is advised! - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. +???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" -!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + * In addition to the user-defined Secondary Streams, StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution and framerate as the input video-source _(i.e. `-video_source`)_. + * :warning: Ensure that your system, machine, server, or network can handle the additional resource requirements of the Secondary Streams. Exercise discretion when configuring multiple streams. + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. +!!! failure "Always use the `-streams` attribute to define additional streams safely. Duplicate or incorrect definitions can break the transcoding pipeline and corrupt the output chunks." === "DASH" @@ -169,10 +170,10 @@ The complete example is as follows: } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" @@ -193,28 +194,28 @@ The complete example is as follows: } # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ```   -## Usage with Custom Audio +## Usage with Custom Audio-Input -By default, if input video-source _(i.e. `-video_source`)_ contains any audio, then it gets automatically mapped to all generated streams. But, if you want to add any custom audio, you can easily do it by using exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to input the path of your audio file to this attribute as `string`, and the API will automatically validate as well as map it to all generated streams. +> In single source mode, by default, if the input video source (i.e., `-video_source`) contains audio, it gets automatically mapped to all generated streams. However, if you want to add a custom audio source, you can use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -The complete example is as follows: +To add a custom audio source, provide the path to your audio file as a string to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: -!!! failure "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the input video source (`-video_source`). Incompatibility can cause multiple errors or result in no output at all." -!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➢](../../params/#a-exclusive-parameters)" +!!! tip "You can also assign a valid audio URL as input instead of a file path. More details can be found [here ➢](../../params/#a-exclusive-parameters)" === "DASH" - ```python linenums="1" hl_lines="12" + ```python linenums="1" hl_lines="11-12" # import required libraries from vidgear.gears import StreamGear @@ -222,23 +223,23 @@ The complete example is as follows: stream_params = { "-video_source": "foo.mp4", "-streams": [ - {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac", # define custom audio-source + "-acodec": "copy", # define copy audio encoder } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="12" + ```python linenums="1" hl_lines="11-12" # import required libraries from vidgear.gears import StreamGear @@ -246,18 +247,18 @@ The complete example is as follows: stream_params = { "-video_source": "foo.mp4", "-streams": [ - {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac", # define custom audio-source + "-acodec": "copy", # define copy audio encoder } # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` @@ -266,75 +267,67 @@ The complete example is as follows: ## Usage with Variable FFmpeg Parameters -For seamlessly generating these streaming assets, StreamGear provides a highly extensible and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) and access to almost all of its parameter. Thereby, you can access almost any parameter available with FFmpeg itself as dictionary attributes in [`stream_params` dictionary parameter](../../params/#stream_params), and use it to manipulate transcoding as you like. +> For fine-grained control over the transcoding process, StreamGear provides a highly extensible and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) library and access to almost all of its configurational parameter. -For this example, let us use our own [H.265/HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) video and [AAC](https://trac.ffmpeg.org/wiki/Encode/AAC) audio encoder, and set custom audio bitrate, and various other optimizations: +In this example, we'll use the [H.265/HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) video encoder and [AAC](https://trac.ffmpeg.org/wiki/Encode/AAC) audio encoder, apply various optimal FFmpeg configurational parameters. +!!! warning "This example assumes that the given input video source (`-video_source`) contains at least one audio stream." -!!! tip "This example is just conveying the idea on how to use FFmpeg's encoders/parameters with StreamGear API. You can use any FFmpeg parameter in the similar manner." +!!! info "This example is just conveying the idea on how to use FFmpeg's internal encoders/parameters with StreamGear API. You can use any FFmpeg parameter in the similar manner." -!!! danger "Kindly read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully, before passing any FFmpeg values to `stream_params` parameter. Wrong values may result in undesired errors or no output at all." - -!!! failure "Always use `-streams` attribute to define additional streams safely, any duplicate or incorrect stream definition can break things!" +!!! danger "Refer to the FFmpeg Documentation (https://ffmpeg.org/documentation.html) before passing FFmpeg values to `stream_params`. Incorrect values may result in errors or no output." === "DASH" - ```python linenums="1" hl_lines="6-10 15-17" + ```python linenums="1" hl_lines="6-9 14" # import required libraries from vidgear.gears import StreamGear # activate Single-Source Mode and various other parameters stream_params = { "-video_source": "foo.mp4", # define Video-Source - "-vcodec": "libx265", # assigns H.265/HEVC video encoder + "-vcodec": "libx265", # specify H.265/HEVC video encoder "-x265-params": "lossless=1", # enables Lossless encoding - "-crf": 25, # Constant Rate Factor: 25 - "-bpp": "0.15", # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes + "-bpp": 0.15, # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps + {"-resolution": "640x360", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac", # define input audio-source: "/home/foo/foo1.aac", - "-acodec": "libfdk_aac", # assign lossless AAC audio encoder - "-vbr": 4, # Variable Bit Rate: `4` + "-acodec": "aac", # specify AAC audio encoder } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="6-10 15-17" + ```python linenums="1" hl_lines="6-9 14" # import required libraries from vidgear.gears import StreamGear - # activate Single-Source Mode and various other parameters stream_params = { "-video_source": "foo.mp4", # define Video-Source - "-vcodec": "libx265", # assigns H.265/HEVC video encoder + "-vcodec": "libx265", # specify H.265/HEVC video encoder "-x265-params": "lossless=1", # enables Lossless encoding - "-crf": 25, # Constant Rate Factor: 25 - "-bpp": "0.15", # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes + "-bpp": 0.15, # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps + {"-resolution": "640x360", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac", # define input audio-source: "/home/foo/foo1.aac", - "-acodec": "libfdk_aac", # assign lossless AAC audio encoder - "-vbr": 4, # Variable Bit Rate: `4` + "-acodec": "aac", # specify AAC audio encoder } # describe a suitable master playlist file location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", logging=True, **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ```   diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 895360ca2..e729d963a 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -165,9 +165,7 @@ def __init__( # cleans and reformat output parameters self.__output_parameters = { - str(k).strip(): ( - str(v).strip() if not isinstance(v, (list, tuple, int, float)) else v - ) + str(k).strip(): (v.strip() if not isinstance(v, str) else v) for k, v in output_params.items() } # log it if specified diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index 187af1caf..fe3bd5bd7 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -39,13 +39,13 @@ def test_failedchannels(size): streamer = StreamGear("output.mpd", logging=True) streamer.stream(input_data_ch1) streamer.stream(input_data_ch3) - streamer.terminate() + streamer.close() else: random_data = np.random.random(size=size) * 255 input_data = random_data.astype(np.uint8) streamer = StreamGear("output.mpd", logging=True) streamer.stream(input_data) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=ValueError) @@ -66,7 +66,7 @@ def test_fail_framedimension(): streamer.stream(None) streamer.stream(input_data1) streamer.stream(input_data2) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=RuntimeError) @@ -77,7 +77,7 @@ def test_method_call_rtf(): stream_params = {"-video_source": 1234} # for CI testing only streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=ValueError) @@ -100,4 +100,4 @@ def test_invalid_params_rtf(format): ) streamer.stream(input_data) streamer.stream(input_data) - streamer.terminate() + streamer.close() diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index 217ad6fe3..0e0226b8c 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -47,7 +47,7 @@ def test_failedextension(output): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output=output, logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() def test_failedextensionsource(): @@ -59,7 +59,7 @@ def test_failedextensionsource(): stream_params = {"-video_source": "garbage.garbage"} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() @pytest.mark.parametrize( @@ -85,7 +85,7 @@ def test_paths_ss(path, format): pytest.fail(str(e)) finally: if not streamer is None: - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=RuntimeError) @@ -96,7 +96,7 @@ def test_method_call_ss(): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.stream("garbage.garbage") - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=(AttributeError, RuntimeError)) @@ -107,7 +107,7 @@ def test_method_call_ss(): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.stream("garbage.garbage") - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=subprocess.CalledProcessError) @@ -124,4 +124,4 @@ def test_invalid_params_ss(format): **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() diff --git a/vidgear/tests/streamer_tests/test_init.py b/vidgear/tests/streamer_tests/test_init.py index 06cec587a..4eeb1c030 100644 --- a/vidgear/tests/streamer_tests/test_init.py +++ b/vidgear/tests/streamer_tests/test_init.py @@ -63,7 +63,7 @@ def test_custom_ffmpeg(c_ffmpeg): Testing custom FFmpeg for StreamGear """ streamer = StreamGear(output="output.mpd", custom_ffmpeg=c_ffmpeg, logging=True) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=(AssertionError, ValueError)) @@ -73,7 +73,7 @@ def test_formats(format): Testing different formats for StreamGear """ streamer = StreamGear(output="output.mpd", format=format, logging=True) - streamer.terminate() + streamer.close() @pytest.mark.parametrize( @@ -96,7 +96,7 @@ def test_outputs(output): logging=True, **stream_params ) - streamer.terminate() + streamer.close() except Exception as e: if output is None or output.endswith("m3u8"): pytest.xfail(str(e)) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index 2aaa3c079..a8c34d983 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -235,7 +235,7 @@ def test_ss_stream(format): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: @@ -263,7 +263,7 @@ def test_ss_livestream(format): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() except Exception as e: pytest.fail(str(e)) @@ -308,7 +308,7 @@ def test_rtf_stream(conversion, format): else: streamer.stream(frame) stream.stop() - streamer.terminate() + streamer.close() asset_file = [ os.path.join(assets_file_path, f) for f in os.listdir(assets_file_path) @@ -346,7 +346,7 @@ def test_rtf_livestream(format): break streamer.stream(frame) stream.stop() - streamer.terminate() + streamer.close() except Exception as e: if not isinstance(e, queue.Empty): pytest.fail(str(e)) @@ -386,7 +386,7 @@ def test_input_framerate_rtf(format): break streamer.stream(frame) stream.release() - streamer.terminate() + streamer.close() if format == "dash": meta_data = extract_meta_mpd(assets_file_path) assert meta_data and len(meta_data) > 0, "Test Failed!" @@ -480,7 +480,7 @@ def test_params(stream_params, format): break streamer.stream(frame) stream.release() - streamer.terminate() + streamer.close() if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: @@ -564,7 +564,7 @@ def test_audio(stream_params, format): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: @@ -712,7 +712,7 @@ def test_multistreams(format, stream_params): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": metadata = extract_meta_mpd(assets_file_path) meta_videos = [x for x in metadata if x["mime_type"].startswith("video")] diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 73882b9e2..19f63a9be 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -537,7 +537,7 @@ def test_delete_ext_safe(ext, result): } streamer = StreamGear(output=mpd_file_path, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() assert check_valid_mpd(mpd_file_path) delete_ext_safe(path, ext, logging=True) assert not os.listdir(path), "`delete_ext_safe` Test failed!" From 42343cd392e73654e295d27908de5c2af38ee174 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 00:01:57 +0530 Subject: [PATCH 06/29] =?UTF-8?q?=E2=9C=A8=20Helper:=20Added=20custom=20`d?= =?UTF-8?q?eprecated`=20decorator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚑️ Implemented a custom `deprecated` decorator function - This decorator can be used to mark functions or parameters as deprecated - It displays a warning message when a deprecated function or parameter is used StreamGear: - ⚑️ Updated the `stream` method to use the new custom `deprecated` decorator - πŸ—‘οΈ Marked the `rgb_mode` parameter as deprecated with a relevant warning message - This parameter will be removed in a future version, and only BGR format frames will be supported - 🚩 Added a new `-enable_force_termination` attribute, similar to WriteGear API. - When set to True, this parameter will force the termination of the FFmpeg process - This option can be useful in cases where the FFmpeg process needs to be terminated immediately - πŸ”Š Enhanced logging messages for better clarity and readability - πŸ§‘β€πŸ’» Improved parameter validation and added more descriptive warning/error messages - 🎨 Refactored some conditions and error handling for better code maintainability - πŸ’‘ Updated docstrings and comments to better reflect the current functionality - πŸ“ Improved code documentation for better understanding and easier maintenance - 🩹 Fixed `libx264rgb` encoder not compatible with `-profile:v` FFmpeg parameter. Setup - βͺ️ Removed the `typing_extensions` package as core dependency - This package was previously required for the `deprecated` decorator. - With the introduction of the custom `deprecated` decorator in helper.py, this dependency is no longer needed. --- setup.py | 2 - vidgear/gears/helper.py | 38 +++ vidgear/gears/streamgear.py | 523 ++++++++++++++++++++++-------------- 3 files changed, 357 insertions(+), 206 deletions(-) diff --git a/setup.py b/setup.py index e8d1c48d7..0c0e89feb 100644 --- a/setup.py +++ b/setup.py @@ -100,8 +100,6 @@ def latest_version(package_name): "requests", "colorlog", "tqdm", - # typing_extensions for `deprecated` decorator - "typing_extensions>=4.7.1", ] + (["opencv-python"] if test_opencv() else []), long_description=long_description, diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index d0dff4305..47069021d 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -35,6 +35,8 @@ import logging as log import platform import socket +import warnings +from functools import wraps from tqdm import tqdm from contextlib import closing from pathlib import Path @@ -154,6 +156,42 @@ def get_module_version(module=None): return str(version) +def deprecated(parameter=None, message=None, stacklevel=2): + """ + ### deprecated + + Decorator to mark a parameter or function as deprecated. + + Parameters: + parameter(str): Name of parameter to be deprecated. + message(str): Custom message to display in warning message. + stacklevel(int): Stack frames level. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if parameter and parameter in kwargs: + warnings.warn( + message + or f"Parameter '{parameter}' is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=stacklevel, + ) + else: + warnings.warn( + message + or f"Function '{func.__name__}' is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=stacklevel, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + def import_dependency_safe( name, error="raise", diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index c39338177..4e58e1441 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -22,19 +22,16 @@ import os import time import math -import platform -import pathlib import difflib import logging as log import subprocess as sp from tqdm import tqdm from fractions import Fraction from collections import OrderedDict -from typing_extensions import deprecated # import helper packages from .helper import ( - capPropId, + deprecated, dict2Args, delete_ext_safe, extract_time, @@ -61,11 +58,11 @@ class StreamGear: StreamGear provides a standalone, highly extensible, and flexible wrapper around FFmpeg multimedia framework for generating chunked-encoded media segments of the content. SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of suitable length. These segments make it - possible to stream videos at different quality levels (different bitrates or spatial resolutions) and can be switched in the middle of a video from one quality level to another – if bandwidth - permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. + possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth + permits - on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. - SteamGear also creates a Manifest/Playlist file (such as MPD in-case of DASH and M3U8 in-case of HLS) besides segments that describe these segment information (timing, URL, media characteristics like video resolution and bit rates) - and is provided to the client before the streaming session. + SteamGear also creates a Manifest/Playlist file (such as MPD in-case of DASH and M3U8 in-case of HLS) besides segments that describe these segment information + (timing, URL, media characteristics like video resolution and bit rates) and is provided to the client before the streaming session. SteamGear currently supports MPEG-DASH (Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1) and Apple HLS (HTTP live streaming). """ @@ -77,13 +74,12 @@ def __init__( This constructor method initializes the object state and attributes of the StreamGear class. Parameters: - output (str): sets the valid filename/path for storing the StreamGear assets. + output (str): sets the valid filename/path for generating the StreamGear assets. format (str): select the adaptive HTTP streaming format(DASH and HLS). custom_ffmpeg (str): assigns the location of custom path/directory for custom FFmpeg executables. logging (bool): enables/disables logging. stream_params (dict): provides the flexibility to control supported internal parameters and FFmpeg properties. """ - # enable logging if specified self.__logging = logging if isinstance(logging, bool) else False @@ -110,9 +106,7 @@ def __init__( # cleans and reformat user-defined parameters self.__params = { - str(k).strip(): ( - str(v).strip() if not isinstance(v, (dict, list, int, float)) else v - ) + str(k).strip(): (v.strip() if isinstance(v, str) else v) for k, v in stream_params.items() } @@ -142,24 +136,25 @@ def __init__( ) # handle Audio-Input - audio = self.__params.pop("-audio", "") + audio = self.__params.pop("-audio", False) if audio and isinstance(audio, str): if os.path.isfile(audio): self.__audio = os.path.abspath(audio) elif is_valid_url(self.__ffmpeg, url=audio, logging=self.__logging): self.__audio = audio else: - self.__audio = "" + self.__audio = False elif audio and isinstance(audio, list): self.__audio = audio else: - self.__audio = "" - - if self.__audio and self.__logging: - logger.debug("External audio source detected!") + self.__audio = False + # log external audio source + self.__audio and self.__logging and logger.debug( + "External audio source `{}` detected.".format(self.__audio) + ) # handle Video-Source input - source = self.__params.pop("-video_source", "") + source = self.__params.pop("-video_source", False) # Check if input is valid string if source and isinstance(source, str) and len(source) > 1: # Differentiate input @@ -169,7 +164,8 @@ def __init__( self.__video_source = source else: # discard the value otherwise - self.__video_source = "" + self.__video_source = False + # Validate input if self.__video_source: validation_results = validate_video( @@ -191,10 +187,17 @@ def __init__( ) ) else: - logger.warning("No valid video_source provided.") + # log warning + logger.warning("Discarded invalid `-video_source` value provided.") else: + if source: + # log warning if source provided + logger.warning("Invalid `-video_source` value provided.") + else: + # log normally + logger.info("No `-video_source` value provided.") # discard the value otherwise - self.__video_source = "" + self.__video_source = False # handle user-defined framerate self.__inputframerate = self.__params.pop("-input_framerate", 0.0) @@ -205,80 +208,103 @@ def __init__( # reset improper values self.__inputframerate = 0.0 - # handle old assests - self.__clear_assets = self.__params.pop("-clear_prev_assets", False) - if not isinstance(self.__clear_assets, bool): + # handle old assets + clear_assets = self.__params.pop("-clear_prev_assets", False) + if isinstance(clear_assets, bool): + self.__clear_assets = clear_assets + # log if clearing assets is enabled + clear_assets and logger.debug( + "Previous StreamGear API assets will be deleted in this run." + ) + else: # reset improper values self.__clear_assets = False # handle whether to livestream? - self.__livestreaming = self.__params.pop("-livestream", False) - if not isinstance(self.__livestreaming, bool): + livestreaming = self.__params.pop("-livestream", False) + if isinstance(livestreaming, bool): + self.__livestreaming = livestreaming + # log if live streaming is enabled + livestreaming and logger.info( + "Live-Streaming Mode is enabled for this run." + ) + else: # reset improper values self.__livestreaming = False - # handle Streaming formats - supported_formats = ["dash", "hls"] # will be extended in future - # Validate - if not (format is None) and format and isinstance(format, str): + # handle the special-case of forced-termination + enable_force_termination = self.__params.pop("-enable_force_termination", False) + # check if value is valid + if isinstance(enable_force_termination, bool): + self.__forced_termination = enable_force_termination + # log if forced termination is enabled + self.__forced_termination and logger.info( + "Forced termination is enabled for this run." + ) + else: + # handle improper values + self.__forced_termination = False + + # handle streaming format + supported_formats = ["dash", "hls"] # TODO will be extended in future + if format and isinstance(format, str): _format = format.strip().lower() if _format in supported_formats: self.__format = _format logger.info( - "StreamGear will generate files for {} HTTP streaming format.".format( + "StreamGear will generate asset files for {} streaming format.".format( self.__format.upper() ) ) elif difflib.get_close_matches(_format, supported_formats): raise ValueError( - "[StreamGear:ERROR] :: Incorrect format! Did you mean `{}`?".format( + "[StreamGear:ERROR] :: Incorrect `format` parameter value! Did you mean `{}`?".format( difflib.get_close_matches(_format, supported_formats)[0] ) ) else: raise ValueError( - "[StreamGear:ERROR] :: format value `{}` not valid/supported!".format( + "[StreamGear:ERROR] :: The `format` parameter value `{}` not valid/supported!".format( format ) ) else: raise ValueError( - "[StreamGear:ERROR] :: format value is Missing/Incorrect. Check vidgear docs!" + "[StreamGear:ERROR] :: The `format` parameter value is Missing or Invalid!" ) - # handles output name - if not output: - raise ValueError( - "[StreamGear:ERROR] :: Kindly provide a valid `output` value. Refer Docs for more information." - ) - else: + # handles output asset filenames + if output: # validate this class has the access rights to specified directory or not abs_path = os.path.abspath(output) - + # check if given output is a valid system path if check_WriteAccess( os.path.dirname(abs_path), is_windows=self.__os_windows, logging=self.__logging, ): - # check if given path is directory - valid_extension = "mpd" if self.__format == "dash" else "m3u8" # get all assets extensions + valid_extension = "mpd" if self.__format == "dash" else "m3u8" assets_exts = [ ("chunk-stream", ".m4s"), # filename prefix, extension ("chunk-stream", ".ts"), # filename prefix, extension ".{}".format(valid_extension), ] # add source file extension too - if self.__video_source: - assets_exts.append( - ( - "chunk-stream", - os.path.splitext(self.__video_source)[1], - ) # filename prefix, extension - ) + self.__video_source and assets_exts.append( + ( + "chunk-stream", + os.path.splitext(self.__video_source)[1], + ) # filename prefix, extension + ) + # handle output + # check if path is a directory if os.path.isdir(abs_path): - if self.__clear_assets: - delete_ext_safe(abs_path, assets_exts, logging=self.__logging) + # clear previous assets if specified + self.__clear_assets and delete_ext_safe( + abs_path, assets_exts, logging=self.__logging + ) + # auto-assign valid name and adds it to path abs_path = os.path.join( abs_path, "{}-{}.{}".format( @@ -286,8 +312,10 @@ def __init__( time.strftime("%Y%m%d-%H%M%S"), valid_extension, ), - ) # auto-assign valid name and adds it to path - elif self.__clear_assets and os.path.isfile(abs_path): + ) + # or check if path is a file + elif os.path.isfile(abs_path) and self.__clear_assets: + # clear previous assets if specified delete_ext_safe( os.path.dirname(abs_path), assets_exts, @@ -300,41 +328,45 @@ def __init__( output, self.__format.upper() ) self.__logging and logger.debug( - "Path:`{}` is sucessfully configured for streaming.".format( + "Output Path:`{}` is successfully configured for generating streaming assets.".format( abs_path ) ) - # assign it - self.__out_file = abs_path.replace( - "\\", "/" - ) # workaround for Windows platform only, others will not be affected - elif platform.system() == "Linux" and pathlib.Path(output).is_char_device(): - # check if linux video device path (such as `/dev/video0`) - self.__logging and logger.debug( - "Path:`{}` is a valid Linux Video Device path.".format(output) - ) - self.__out_file = output + # workaround patch for Windows only, + # others platforms will not be affected + self.__out_file = abs_path.replace("\\", "/") # check if given output is a valid URL elif is_valid_url(self.__ffmpeg, url=output, logging=self.__logging): self.__logging and logger.debug( - "URL:`{}` is valid and sucessfully configured for streaming.".format( + "URL:`{}` is valid and successfully configured for generating streaming assets.".format( output ) ) self.__out_file = output + # raise ValueError otherwise else: raise ValueError( - "[StreamGear:ERROR] :: Output value:`{}` is not valid/supported!".format( + "[StreamGear:ERROR] :: The output parameter value:`{}` is not valid/supported!".format( output ) ) + else: + # raise ValueError otherwise + raise ValueError( + "[StreamGear:ERROR] :: Kindly provide a valid `output` parameter value. Refer Docs for more information." + ) + # log Mode of operation - logger.info( + self.__video_source and logger.info( "StreamGear has been successfully configured for {} Mode.".format( "Single-Source" if self.__video_source else "Real-time Frames" ) ) + @deprecated( + parameter="rgb_mode", + message="The `rgb_mode` parameter is deprecated and will be removed in a future version. Only BGR format frames will be supported going forward.", + ) def stream(self, frame, rgb_mode=False): """ Pipelines `ndarray` frames to FFmpeg Pipeline for transcoding into multi-bitrate streamable assets. @@ -342,12 +374,11 @@ def stream(self, frame, rgb_mode=False): Parameters: frame (ndarray): a valid numpy frame rgb_mode (boolean): enable this flag to activate RGB mode _(i.e. specifies that incoming frames are of RGB format instead of default BGR)_. - """ # check if function is called in correct context if self.__video_source: raise RuntimeError( - "[StreamGear:ERROR] :: `stream()` function cannot be used when streaming from a `-video_source` input file. Kindly refer vidgear docs!" + "[StreamGear:ERROR] :: The `stream()` method cannot be used when streaming from a `-video_source` input file. Kindly refer vidgear docs!" ) # None-Type frames will be skipped if frame is None: @@ -400,7 +431,7 @@ def transcode_source(self): # check if function is called in correct context if not (self.__video_source): raise RuntimeError( - "[StreamGear:ERROR] :: `transcode_source()` function cannot be used without a valid `-video_source` input. Kindly refer vidgear docs!" + "[StreamGear:ERROR] :: The `transcode_source()` method cannot be used without a valid `-video_source` input. Kindly refer vidgear docs!" ) # assign height, width and framerate self.__inputheight = int(self.__aspect_source[1]) @@ -411,21 +442,22 @@ def transcode_source(self): def __PreProcess(self, channels=0, rgb=False): """ - Internal method that pre-processes default FFmpeg parameters before beginning pipelining. + Internal method that pre-processes default FFmpeg parameters before starting pipelining. Parameters: channels (int): Number of channels - rgb_mode (boolean): activates RGB mode _(if enabled)_. + rgb (boolean): activates RGB mode _(if enabled)_. """ # turn off initiate flag self.__initiate_stream = False - # initialize parameters + # initialize I/O parameters input_parameters = OrderedDict() output_parameters = OrderedDict() # pre-assign default codec parameters (if not assigned by user). default_codec = "libx264rgb" if rgb else "libx264" output_parameters["-vcodec"] = self.__params.pop("-vcodec", default_codec) - # enable optimizations and enforce compatibility + + # enforce compatibility if output_parameters["-vcodec"] != "copy": # NOTE: these parameters only supported when stream copy not defined output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") @@ -441,7 +473,9 @@ def __PreProcess(self, channels=0, rgb=False): self.__params.pop("-aspect", False) and logger.warning( "Overriding aspect ratio with stream copy may produce invalid files. Discarding `-aspect` parameter!" ) - # w.r.t selected codec + + # enable optimizations w.r.t selected codec + ### OPTIMIZATION-1 ### if output_parameters["-vcodec"] in [ "libx264", "libx264rgb", @@ -449,33 +483,36 @@ def __PreProcess(self, channels=0, rgb=False): "libvpx-vp9", ]: output_parameters["-crf"] = self.__params.pop("-crf", "20") - if output_parameters["-vcodec"] in ["libx264", "libx264rgb"]: + ### OPTIMIZATION-2 ### + if output_parameters["-vcodec"] == "libx264": if not (self.__video_source): output_parameters["-profile:v"] = self.__params.pop( "-profile:v", "high" ) + ### OPTIMIZATION-3 ### + if output_parameters["-vcodec"] in ["libx264", "libx264rgb"]: output_parameters["-tune"] = self.__params.pop("-tune", "zerolatency") output_parameters["-preset"] = self.__params.pop("-preset", "veryfast") + ### OPTIMIZATION-4 ### if output_parameters["-vcodec"] == "libx265": output_parameters["-x265-params"] = self.__params.pop( "-x265-params", "lossless=1" ) + # enable audio (if present) if self.__audio: # validate audio source bitrate = validate_audio(self.__ffmpeg, source=self.__audio) if bitrate: logger.info( - "Detected External Audio Source is valid, and will be used for streams." + "Detected External Audio Source is valid, and will be used for generating streams." ) - # assign audio source output_parameters[ "{}".format( "-core_asource" if isinstance(self.__audio, list) else "-i" ) ] = self.__audio - # assign audio codec output_parameters["-acodec"] = self.__params.pop( "-acodec", "aac" if isinstance(self.__audio, list) else "copy" @@ -488,30 +525,30 @@ def __PreProcess(self, channels=0, rgb=False): logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) ) + # validate input video's audio source if available elif self.__video_source: - # validate audio source bitrate = validate_audio(self.__ffmpeg, source=self.__video_source) if bitrate: - logger.info("Source Audio will be used for streams.") + logger.info("Input Video's audio source will be used for this run.") # assign audio codec output_parameters["-acodec"] = ( "aac" if self.__format == "hls" else "copy" ) output_parameters["a_bitrate"] = bitrate # temporary handler else: - logger.warning( - "No valid audio_source available. Disabling audio for streams!" + logger.info( + "No valid audio source available in the input video. Disabling audio while generating streams." ) else: - logger.warning( - "No valid audio_source provided. Disabling audio for streams!" + logger.info( + "No valid audio source provided. Disabling audio while generating streams." ) # enable audio optimizations based on audio codec if "-acodec" in output_parameters and output_parameters["-acodec"] == "aac": output_parameters["-movflags"] = "+faststart" # set input framerate - if self.__sourceframerate > 0 and not (self.__video_source): + if self.__sourceframerate > 0.0 and not (self.__video_source): # set input framerate self.__logging and logger.debug( "Setting Input framerate: {}".format(self.__sourceframerate) @@ -542,10 +579,10 @@ def __PreProcess(self, channels=0, rgb=False): # check if processing completed successfully assert not ( process_params is None - ), "[StreamGear:ERROR] :: {} stream cannot be initiated!".format( + ), "[StreamGear:ERROR] :: `{}` stream cannot be initiated properly!".format( self.__format.upper() ) - # Finally start FFmpef pipline and process everything + # Finally start FFmpeg pipeline and process everything self.__Build_n_Execute(process_params[0], process_params[1]) def __handle_streams(self, input_params, output_params): @@ -558,42 +595,45 @@ def __handle_streams(self, input_params, output_params): """ # handle bit-per-pixels bpp = self.__params.pop("-bpp", 0.1000) - if isinstance(bpp, (float, int)) and bpp > 0.0: - bpp = float(bpp) if (bpp > 0.001) else 0.1000 + if isinstance(bpp, float) and bpp >= 0.001: + bpp = float(bpp) else: - # reset to defaut if invalid + # reset to default if invalid bpp = 0.1000 # log it - self.__logging and logger.debug( + bpp and self.__logging and logger.debug( "Setting bit-per-pixels: {} for this stream.".format(bpp) ) # handle gop - gop = self.__params.pop("-gop", 0) - if isinstance(gop, (int, float)) and gop > 0: + gop = self.__params.pop("-gop", 2 * int(self.__sourceframerate)) + if isinstance(gop, (int, float)) and gop >= 0: gop = int(gop) else: # reset to some recommended value gop = 2 * int(self.__sourceframerate) # log it - self.__logging and logger.debug("Setting GOP: {} for this stream.".format(gop)) + gop and self.__logging and logger.debug( + "Setting GOP: {} for this stream.".format(gop) + ) - # define and map default stream - if self.__format != "hls": - output_params["-map"] = 0 - else: + # define default stream and its mapping + if self.__format == "hls": output_params["-corev0"] = ["-map", "0:v"] if "-acodec" in output_params: output_params["-corea0"] = [ "-map", "{}:a".format(1 if "-core_audio" in output_params else 0), ] - # assign resolution + else: + output_params["-map"] = 0 + + # assign default output resolution if "-s:v:0" in self.__params: # prevent duplicates del self.__params["-s:v:0"] output_params["-s:v:0"] = "{}x{}".format(self.__inputwidth, self.__inputheight) - # assign video-bitrate + # assign default output video-bitrate if "-b:v:0" in self.__params: # prevent duplicates del self.__params["-b:v:0"] @@ -608,12 +648,13 @@ def __handle_streams(self, input_params, output_params): ) + "k" ) - # assign audio-bitrate + + # assign default output audio-bitrate if "-b:a:0" in self.__params: # prevent duplicates del self.__params["-b:a:0"] - # extract audio-bitrate from temporary handler - a_bitrate = output_params.pop("a_bitrate", "") + # extract and assign audio-bitrate from temporary handler + a_bitrate = output_params.pop("a_bitrate", False) if "-acodec" in output_params and a_bitrate: output_params["-b:a:0"] = a_bitrate @@ -621,7 +662,7 @@ def __handle_streams(self, input_params, output_params): streams = self.__params.pop("-streams", {}) output_params = self.__evaluate_streams(streams, output_params, bpp) - # define additional stream optimization parameters + # define additional streams optimization parameters if output_params["-vcodec"] in ["libx264", "libx264rgb"]: if not "-bf" in self.__params: output_params["-bf"] = 1 @@ -629,17 +670,18 @@ def __handle_streams(self, input_params, output_params): output_params["-sc_threshold"] = 0 if not "-keyint_min" in self.__params: output_params["-keyint_min"] = gop - if output_params["-vcodec"] in ["libx264", "libx264rgb", "libvpx-vp9"]: - if not "-g" in self.__params: - output_params["-g"] = gop + if ( + output_params["-vcodec"] in ["libx264", "libx264rgb", "libvpx-vp9"] + and not "-g" in self.__params + ): + output_params["-g"] = gop if output_params["-vcodec"] == "libx265": output_params["-core_x265"] = [ "-x265-params", "keyint={}:min-keyint={}".format(gop, gop), ] - # process given dash/hls stream - processed_params = None + # process given dash/hls stream and return it if self.__format == "dash": processed_params = self.__generate_dash_stream( input_params=input_params, @@ -650,7 +692,6 @@ def __handle_streams(self, input_params, output_params): input_params=input_params, output_params=output_params, ) - return processed_params def __evaluate_streams(self, streams, output_params, bpp): @@ -666,12 +707,13 @@ def __evaluate_streams(self, streams, output_params, bpp): # check if streams are empty if not streams: - logger.warning("No `-streams` are provided!") + logger.info("No additional `-streams` are provided.") return output_params # check if streams are valid if isinstance(streams, list) and all(isinstance(x, dict) for x in streams): - stream_count = 1 # keep track of streams + # keep track of streams + stream_count = 1 # calculate source aspect-ratio source_aspect_ratio = self.__inputwidth / self.__inputheight # log the process @@ -679,20 +721,23 @@ def __evaluate_streams(self, streams, output_params, bpp): "Processing {} streams.".format(len(streams)) ) # iterate over given streams - for stream in streams: - stream_copy = stream.copy() # make copy - intermediate_dict = {} # handles intermediate stream data as dictionary - + for idx, stream in enumerate(streams): + # log stream processing + self.__logging and logger.debug("Processing #{} stream now".format(idx)) + # make copy + stream_copy = stream.copy() + # handle intermediate stream data as dictionary + intermediate_dict = {} # define and map stream to intermediate dict - if self.__format != "hls": - intermediate_dict["-core{}".format(stream_count)] = ["-map", "0"] - else: + if self.__format == "hls": intermediate_dict["-corev{}".format(stream_count)] = ["-map", "0:v"] if "-acodec" in output_params: intermediate_dict["-corea{}".format(stream_count)] = [ "-map", "{}:a".format(1 if "-core_audio" in output_params else 0), ] + else: + intermediate_dict["-core{}".format(stream_count)] = ["-map", "0"] # extract resolution & individual dimension of stream resolution = stream.pop("-resolution", "") @@ -713,7 +758,7 @@ def __evaluate_streams(self, streams, output_params, bpp): ) if int(dimensions[0]) != expected_width: logger.warning( - "Given stream resolution `{}` is not in accordance with the Source Aspect-Ratio. Stream Output may appear Distorted!".format( + "The provided stream resolution '{}' does not align with the source aspect ratio. Output stream may appear distorted!".format( resolution ) ) @@ -722,7 +767,7 @@ def __evaluate_streams(self, streams, output_params, bpp): else: # otherwise log error and skip stream logger.error( - "Missing `-resolution` value, Stream `{}` Skipped!".format( + "Missing `-resolution` value. Invalid stream `{}` Skipped!".format( stream_copy ) ) @@ -751,7 +796,7 @@ def __evaluate_streams(self, streams, output_params, bpp): else: # If everything fails, log and skip the stream! logger.error( - "Unable to determine Video-Bitrate for the stream `{}`, Skipped!".format( + "Unable to determine Video-Bitrate for the stream `{}`. Skipped!".format( stream_copy ) ) @@ -778,9 +823,16 @@ def __evaluate_streams(self, streams, output_params, bpp): stream_copy.clear() # increment to next stream stream_count += 1 + # log stream processing + self.__logging and logger.debug( + "Processed #{} stream successfully.".format(idx) + ) + # store stream count output_params["stream_count"] = stream_count + # log streams processing self.__logging and logger.debug("All streams processed successfully!") else: + # skip and log logger.warning("Invalid type `-streams` skipped!") return output_params @@ -794,8 +846,6 @@ def __generate_hls_stream(self, input_params, output_params): input_params (dict): Input FFmpeg parameters output_params (dict): Output FFmpeg parameters """ - # Check if live-streaming or not? - # validate `hls_segment_type` default_hls_segment_type = self.__params.pop("-hls_segment_type", "mpegts") if isinstance( @@ -803,38 +853,77 @@ def __generate_hls_stream(self, input_params, output_params): ) and default_hls_segment_type.strip() in ["fmp4", "mpegts"]: output_params["-hls_segment_type"] = default_hls_segment_type.strip() else: + # otherwise reset to default + logger.warning("Invalid `-hls_segment_type` value skipped!") output_params["-hls_segment_type"] = "mpegts" - # gather required parameters if self.__livestreaming: - # `hls_list_size` must be greater than 0 + # `hls_list_size` must be greater than or equal to 0 default_hls_list_size = self.__params.pop("-hls_list_size", 6) - if isinstance(default_hls_list_size, int) and default_hls_list_size > 0: + if isinstance(default_hls_list_size, int) and default_hls_list_size >= 0: output_params["-hls_list_size"] = default_hls_list_size else: - # otherwise reset to default + # otherwise reset to default + logger.warning("Invalid `-hls_list_size` value skipped!") output_params["-hls_list_size"] = 6 - # default behaviour - output_params["-hls_init_time"] = self.__params.pop("-hls_init_time", 4) - output_params["-hls_time"] = self.__params.pop("-hls_time", 6) - output_params["-hls_flags"] = self.__params.pop( + # `hls_init_time` must be greater than or equal to 0 + default_hls_init_time = self.__params.pop("-hls_init_time", 4) + if isinstance(default_hls_init_time, int) and default_hls_init_time >= 0: + output_params["-hls_init_time"] = default_hls_init_time + else: + # otherwise reset to default + logger.warning("Invalid `-hls_init_time` value skipped!") + output_params["-hls_init_time"] = 4 + # `hls_time` must be greater than or equal to 0 + default_hls_time = self.__params.pop("-hls_time", 4) + if isinstance(default_hls_time, int) and default_hls_time >= 0: + output_params["-hls_time"] = default_hls_time + else: + # otherwise reset to default + logger.warning("Invalid `-hls_time` value skipped!") + output_params["-hls_time"] = 6 + # `hls_flags` must be string + default_hls_flags = self.__params.pop( "-hls_flags", "delete_segments+discont_start+split_by_time" ) + if isinstance(default_hls_flags, str): + output_params["-hls_flags"] = default_hls_flags + else: + # otherwise reset to default + logger.warning("Invalid `-hls_flags` value skipped!") + output_params["-hls_flags"] = ( + "delete_segments+discont_start+split_by_time" + ) # clean everything at exit? - output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) + remove_at_exit = self.__params.pop("-remove_at_exit", 0) + if isinstance(remove_at_exit, int) and remove_at_exit in [ + 0, + 1, + ]: + output_params["-remove_at_exit"] = remove_at_exit + else: + # otherwise reset to default + logger.warning("Invalid `-remove_at_exit` value skipped!") + output_params["-remove_at_exit"] = 0 else: # enforce "contain all the segments" output_params["-hls_list_size"] = 0 output_params["-hls_playlist_type"] = "vod" # handle base URL for absolute paths - output_params["-hls_base_url"] = self.__params.pop("-hls_base_url", "") + hls_base_url = self.__params.pop("-hls_base_url", "") + if isinstance(hls_base_url, str): + output_params["-hls_base_url"] = hls_base_url + else: + # otherwise reset to default + logger.warning("Invalid `-hls_base_url` value skipped!") + output_params["-hls_base_url"] = "" - # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) + # Hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-allowed_extensions"] = "ALL" # Handling - # Here filenname will be based on `stream_count` dict parameter that - # would be used to check whether stream is multivariant(>1) or single(0-1) + # Here filename will be based on `stream_count` dict parameter that + # would be used to check whether stream is multi-variant(>1) or single(0-1) segment_template = ( "{}-stream%v-%03d.{}" if output_params["stream_count"] > 1 @@ -844,9 +933,11 @@ def __generate_hls_stream(self, input_params, output_params): os.path.join(os.path.dirname(self.__out_file), "chunk"), "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", ) + # Hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-hls_allow_cache"] = 0 # enable hls formatting output_params["-f"] = "hls" + # return HLS params return (input_params, output_params) def __generate_dash_stream(self, input_params, output_params): @@ -861,19 +952,52 @@ def __generate_dash_stream(self, input_params, output_params): # Check if live-streaming or not? if self.__livestreaming: - output_params["-window_size"] = self.__params.pop("-window_size", 5) - output_params["-extra_window_size"] = self.__params.pop( - "-extra_window_size", 5 - ) + # `extra_window_size` must be greater than or equal to 0 + window_size = self.__params.pop("-window_size", 5) + if isinstance(window_size, int) and window_size >= 0: + output_params["-window_size"] = window_size + else: + # otherwise reset to default + logger.warning("Invalid `-window_size` value skipped!") + output_params["-window_size"] = 5 + # `extra_window_size` must be greater than or equal to 0 + extra_window_size = self.__params.pop("-extra_window_size", 5) + if isinstance(extra_window_size, int) and extra_window_size >= 0: + output_params["-extra_window_size"] = window_size + else: + # otherwise reset to default + logger.warning("Invalid `-extra_window_size` value skipped!") + output_params["-extra_window_size"] = 5 # clean everything at exit? - output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) - # default behaviour - output_params["-seg_duration"] = self.__params.pop("-seg_duration", 20) + remove_at_exit = self.__params.pop("-remove_at_exit", 0) + if isinstance(remove_at_exit, int) and remove_at_exit in [ + 0, + 1, + ]: + output_params["-remove_at_exit"] = remove_at_exit + else: + # otherwise reset to default + logger.warning("Invalid `-remove_at_exit` value skipped!") + output_params["-remove_at_exit"] = 0 + # `seg_duration` must be greater than or equal to 0 + seg_duration = self.__params.pop("-seg_duration", 20) + if isinstance(seg_duration, int) and seg_duration >= 0: + output_params["-seg_duration"] = seg_duration + else: + # otherwise reset to default + logger.warning("Invalid `-seg_duration` value skipped!") + output_params["-seg_duration"] = 20 # Disable (0) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 0 else: - # default behaviour + # `seg_duration` must be greater than or equal to 0 output_params["-seg_duration"] = self.__params.pop("-seg_duration", 5) + if isinstance(seg_duration, int) and seg_duration >= 0: + output_params["-seg_duration"] = seg_duration + else: + # otherwise reset to default + logger.warning("Invalid `-seg_duration` value skipped!") + output_params["-seg_duration"] = 5 # Enable (1) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 1 @@ -884,6 +1008,7 @@ def __generate_dash_stream(self, input_params, output_params): ) # enable dash formatting output_params["-f"] = "dash" + # return DASH params return (input_params, output_params) def __Build_n_Execute(self, input_params, output_params): @@ -895,13 +1020,11 @@ def __Build_n_Execute(self, input_params, output_params): output_params (dict): Output FFmpeg parameters """ # handle audio source if present - if "-core_asource" in output_params: - output_params.move_to_end("-core_asource", last=False) - - # finally handle `-i` - if "-i" in output_params: - output_params.move_to_end("-i", last=False) - + "-core_asource" in output_params and output_params.move_to_end( + "-core_asource", last=False + ) + # handle `-i` parameter + "-i" in output_params and output_params.move_to_end("-i", last=False) # copy streams count stream_count = output_params.pop("stream_count", 1) @@ -930,22 +1053,20 @@ def __Build_n_Execute(self, input_params, output_params): ] # log it if enabled - if self.__logging: - logger.debug( - "User-Defined Output parameters: `{}`".format( - " ".join(output_commands) if output_commands else None - ) + self.__logging and logger.debug( + "User-Defined Output parameters: `{}`".format( + " ".join(output_commands) if output_commands else None ) - logger.debug( - "Additional parameters: `{}`".format( - " ".join(stream_commands) if stream_commands else None - ) + ) + self.__logging and logger.debug( + "Additional parameters: `{}`".format( + " ".join(stream_commands) if stream_commands else None ) + ) # build FFmpeg command from parameters ffmpeg_cmd = None - hide_banner = ( - [] if self.__logging else ["-hide_banner"] - ) # ensuring less cluttering if specified + # ensuring less cluttering if silent mode + hide_banner = [] if self.__logging else ["-hide_banner"] # format commands if self.__video_source: ffmpeg_cmd = ( @@ -986,7 +1107,10 @@ def __Build_n_Execute(self, input_params, output_params): return_code = 0 pbar = None sec_prev = 0 - if not self.__logging: + if self.__logging: + self.__process.communicate() + return_code = self.__process.returncode + else: # iterate until stdout runs out while True: # read and process data @@ -994,41 +1118,36 @@ def __Build_n_Execute(self, input_params, output_params): if data: data = data.decode("utf-8") # extract duration and time-left - if pbar is None: - if "Duration:" in data: - sec_duration = extract_time(data) - # initate progress bar - pbar = tqdm( - total=sec_duration, - desc="Processing Frames", - unit="frame", - ) - else: - if "time=" in data: - sec_current = extract_time(data) - # update progress bar - if sec_current: - pbar.update(sec_current - sec_prev) - sec_prev = sec_current + if pbar is None and "Duration:" in data: + # extract time in seconds + sec_duration = extract_time(data) + # initiate progress bar + pbar = tqdm( + total=sec_duration, + desc="Processing Frames", + unit="frame", + ) + elif "time=" in data: + # extract time in seconds + sec_current = extract_time(data) + # update progress bar + if sec_current: + pbar.update(sec_current - sec_prev) + sec_prev = sec_current else: # poll if no data if self.__process.poll() is not None: break return_code = self.__process.poll() - else: - self.__process.communicate() - return_code = self.__process.returncode # close progress bar - if pbar: - pbar.close() + not (pbar is None) and pbar.close() # handle return_code - if return_code: + if return_code != 0: # log and raise error if return_code is `1` logger.error( "StreamGear failed to initiate stream for this video source!" ) - error = sp.CalledProcessError(return_code, ffmpeg_cmd) - raise error + raise sp.CalledProcessError(return_code, ffmpeg_cmd) else: # log if successful logger.critical( @@ -1052,14 +1171,15 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() @deprecated( - "The `terminate()` method will be removed in the next release. Kindly use `close()` method instead." + message="The `terminate()` method will be removed in the next release. Kindly use `close()` method instead." ) def terminate(self): """ - !!! warning "[DEPRECATION NOTICE]: This method will be removed in the next release. Kindly use `close()` method instead." + !!! warning "[DEPRECATION NOTICE]: This method is now deprecated and will be removed in a future release." - This function simply provides backward compatibility with the old `terminate()` function. - It simply calls the new `close()` method to terminate various StreamGear process. + This function ensures backward compatibility for the `terminate()` method to maintain the API on existing systems. + It achieves this by calling the new `close()` method to terminate various + StreamGear processes. """ self.close() @@ -1078,13 +1198,8 @@ def close(self): self.__process.stdin and self.__process.stdin.close() # close `stdout` output self.__process.stdout and self.__process.stdout.close() + # forced termination if specified. + self.__forced_termination and self.__process.terminate() # wait if process is still processing - self.__process.wait() # discard process self.__process = None - # log it - logger.critical( - "Transcoding Ended. {} Streaming assets are successfully generated at specified path.".format( - self.__format.upper() - ) - ) From a8fc5b4e9c91c549bd5568e64dd42b3a2797cb43 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 00:19:45 +0530 Subject: [PATCH 07/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20StreamGear:=20Handle?= =?UTF-8?q?d=20process=20termination=20gracefully?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ Refactored the logic for terminating the FFmpeg process in the `close` method - Instead of directly terminating the process, the code now sends a CTRL_BREAK_EVENT signal - This allows for a more graceful termination and avoids potential issues with abrupt termination - If the forced termination option is enabled, the process is directly terminated - ✏️ Fixed a typo in an error message related to stream initiation - 🎨 Refactored and simplified some logging statements using Python's ternary operator 🍻 WriteGear: Mirrored logic for terminating the FFmpeg process in the `close` method --- vidgear/gears/streamgear.py | 15 ++++++++++----- vidgear/gears/writegear.py | 10 +++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 4e58e1441..28bf6aa7f 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -22,6 +22,7 @@ import os import time import math +import signal import difflib import logging as log import subprocess as sp @@ -582,7 +583,7 @@ def __PreProcess(self, channels=0, rgb=False): ), "[StreamGear:ERROR] :: `{}` stream cannot be initiated properly!".format( self.__format.upper() ) - # Finally start FFmpeg pipeline and process everything + # Finally start FFmpef pipline and process everything self.__Build_n_Execute(process_params[0], process_params[1]) def __handle_streams(self, input_params, output_params): @@ -723,7 +724,7 @@ def __evaluate_streams(self, streams, output_params, bpp): # iterate over given streams for idx, stream in enumerate(streams): # log stream processing - self.__logging and logger.debug("Processing #{} stream now".format(idx)) + self.__logging and logger.debug("Processing #{} stream ::".format(idx)) # make copy stream_copy = stream.copy() # handle intermediate stream data as dictionary @@ -1189,8 +1190,7 @@ def close(self): Safely terminates various StreamGear process. """ # log termination - if self.__logging: - logger.debug("Terminating StreamGear Processes.") + self.__logging and logger.debug("Terminating StreamGear Processes.") # return if no process was initiated at first place if self.__process is None or not (self.__process.poll() is None): return @@ -1199,7 +1199,12 @@ def close(self): # close `stdout` output self.__process.stdout and self.__process.stdout.close() # forced termination if specified. - self.__forced_termination and self.__process.terminate() + if self.__forced_termination: + self.__process.terminate() + else: + # send CTRL_BREAK_EVENT signal + self.__process.send_signal(signal.CTRL_BREAK_EVENT) # wait if process is still processing + self.__process.wait() # discard process self.__process = None diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index e729d963a..5bd217fd1 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -22,6 +22,7 @@ import os import cv2 import time +import signal import platform import pathlib import logging as log @@ -759,8 +760,7 @@ def close(self): Safely terminates various WriteGear process. """ # log termination - if self.__logging: - logger.debug("Terminating WriteGear Processes.") + self.__logging and logger.debug("Terminating WriteGear Processes.") # handle termination separately if self.__compression: # when Compression Mode is enabled @@ -773,7 +773,11 @@ def close(self): # close `stdout` output self.__process.stdout and self.__process.stdout.close() # forced termination if specified. - self.__forced_termination and self.__process.terminate() + if self.__forced_termination: + self.__process.terminate() + else: + # send CTRL_BREAK_EVENT signal + self.__process.send_signal(signal.CTRL_BREAK_EVENT) # wait if process is still processing self.__process.wait() else: From 19cf611537979dfa991a9e1ae27fa82ecbb85078 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 18:13:09 +0530 Subject: [PATCH 08/29] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20StreamGe?= =?UTF-8?q?ar=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ”₯ Removed the obsolete usage example for deprecation RGB mode with StreamGear. - πŸ—‘οΈ Added a deprecation warning admonition for the `rgb_mode` parameter in the `stream()` method. - πŸ’‘ Updated the docstring for the `stream()` method and `transcode_source()` method. - 🚩 Added documentation and usage of the new `-enable_force_termination` parameter. - πŸ“ Updated the documentation for the `-disable_force_termination` parameter in WriteGear API. - πŸ’¬ Added a new FAQ entry about deprecated `rgb_mode` parameter. - 🎨 Minor formatting and wording improvements in the documentation. --- docs/gears/streamgear/params.md | 16 ++- docs/gears/streamgear/rtfm/usage.md | 109 --------------------- docs/gears/writegear/compression/params.md | 4 +- docs/help/streamgear_faqs.md | 8 +- vidgear/gears/streamgear.py | 10 +- 5 files changed, 24 insertions(+), 123 deletions(-) diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 14575db35..fb81baecb 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -322,16 +322,24 @@ StreamGear API provides some exclusive internal parameters to easily generate St   +* **`-enable_force_termination`** _(bool)_: sets a special flag to enable the forced termination of FFmpeg process. Its usage is as follows: + + !!! warning "The `-enable_force_termination` flag can potentially cause unexpected behavior or prevent the program from producing the desired output in certain scenarios. It is recommended to use this flag with caution." + + ```python + stream_params = {"-enable_force_termination": True} # enables forced-termination behavior + ``` + +  + #### B. FFmpeg Parameters Almost all FFmpeg parameter can be passed as dictionary attributes in `stream_params`. For example, for using `libx264 encoder` to produce a lossless output video, we can pass required FFmpeg parameters as dictionary attributes, as follows: !!! tip "Kindly check [H.264 docs ➢](https://trac.ffmpeg.org/wiki/Encode/H.264) and other [FFmpeg Docs ➢](https://ffmpeg.org/documentation.html) for more information on these parameters" - !!! failure "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." - !!! note "In addition to these parameters, almost any FFmpeg parameter _(supported by installed FFmpeg)_ is also supported. But make sure to read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully first." ```python @@ -342,9 +350,9 @@ stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "ze ### Supported Encoders and Decoders -All the encoders and decoders that are compiled with FFmpeg in use, are supported by WriteGear API. You can easily check the compiled encoders by running following command in your terminal: +All the encoders and decoders that are compiled with FFmpeg in use, are supported by StreamGear API. You can easily check the compiled encoders by running following command in your terminal: -!!! info "Similarily, supported demuxers and filters depends upons compiled FFmpeg in use." +!!! info "Similarly, supported Demuxers and Filters depends upon compiled FFmpeg in use." ```sh # for checking encoder diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 196e6427a..e4262ad68 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -382,115 +382,6 @@ The complete example is as follows: ``` -  - -## Bare-Minimum Usage with RGB Mode - -In Real-time Frames Mode, StreamGear API provide [`rgb_mode`](../../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) boolean parameter with its `stream()` function, which if enabled _(i.e. `rgb_mode=True`)_, specifies that incoming frames are of RGB format _(instead of default BGR format)_, thereby also known as ==RGB Mode==. - -The complete usage example is as follows: - -=== "DASH" - - ```python linenums="1" hl_lines="28" - # import required libraries - from vidgear.gears import CamGear - from vidgear.gears import StreamGear - import cv2 - - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source='foo1.mp4').start() - - # describe a suitable manifest-file location/name - streamer = StreamGear(output="dash_out.mpd") - - # loop over - while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] - - - # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode - - # Show output window - cv2.imshow("Output Frame", frame) - - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - - # close output window - cv2.destroyAllWindows() - - # safely close video stream - stream.stop() - - # safely close streamer - streamer.close() - ``` - -=== "HLS" - - ```python linenums="1" hl_lines="28" - # import required libraries - from vidgear.gears import CamGear - from vidgear.gears import StreamGear - import cv2 - - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source='foo1.mp4').start() - - # describe a suitable manifest-file location/name - streamer = StreamGear(output="hls_out.m3u8", format = "hls") - - # loop over - while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] - - - # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode - - # Show output window - cv2.imshow("Output Frame", frame) - - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - - # close output window - cv2.destroyAllWindows() - - # safely close video stream - stream.stop() - - # safely close streamer - streamer.close() - ``` - -   ## Bare-Minimum Usage with OpenCV diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index 9afe0e092..b5724db2e 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -188,9 +188,9 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor output_params = {"-disable_ffmpeg_window": True} # disables FFmpeg creation window ``` - * **`-disable_force_termination`** _(bool)_: sets a special flag to manually disable the default forced-termination behaviour in WriteGear API when `-i` FFmpeg parameter is used _(For more details, see issue: #149)_. Its usage is as follows: + * **`-disable_force_termination`** _(bool)_: sets a special flag to manually disable the default forced termination of FFmpeg process in WriteGear API when `-i` FFmpeg parameter is used _(For more details, see issue: #149)_. Its usage is as follows: - !!! warning "`-disable_force_termination` flag is a absolute necessity when video duration is too short(<60sec), otherwise WriteGear will not produce any valid output." + !!! warning "The `-disable_force_termination` flag is a absolute necessity when video duration is too short(`< 60sec`), otherwise WriteGear may produce invalid or no output." ```python output_params = {"-disable_force_termination": True} # disable the default forced-termination behaviour diff --git a/docs/help/streamgear_faqs.md b/docs/help/streamgear_faqs.md index 073c59c9a..a91be9ddc 100644 --- a/docs/help/streamgear_faqs.md +++ b/docs/help/streamgear_faqs.md @@ -77,13 +77,11 @@ limitations under the License.   -## Is Real-time Frames Mode only used for Live-Streaming? +## How to use StreamGear API with RGB Frames? -**Answer:** Real-time Frame Modes and Live-Streaming are completely different terms and not directly related. +**Answer:** The `rgb_mode` parameter in [`stream()`](../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) method, which earlier used to support RGB frames in Real-time Frames Mode is now deprecated, and will be removed in a future version. Only BGR format frames will be supported going forward. Please update your code to handle BGR format frames. -- **Real-time Frame Mode** is one of [primary mode](../../gears/streamgear/introduction/#mode-of-operations) for directly transcoding real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) video-frames _(as opposed to a entire file)_ into a sequence of multiple smaller chunks/segments for streaming. - -- **Live-Streaming** is feature of StreamGear's primary modes that activates behaviour where chunks will contain information for few new frames only and forgets all previous ones for low latency streaming. It can be activated for any primary mode using exclusive [`-livestream`](../../gears/streamgear/params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. +  ## How to use Hardware/GPU encoder for StreamGear trancoding? diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 28bf6aa7f..d4b162597 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -59,7 +59,7 @@ class StreamGear: StreamGear provides a standalone, highly extensible, and flexible wrapper around FFmpeg multimedia framework for generating chunked-encoded media segments of the content. SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of suitable length. These segments make it - possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth + possible to stream videos at different quality levels _(different bitrate or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another - if bandwidth permits - on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. SteamGear also creates a Manifest/Playlist file (such as MPD in-case of DASH and M3U8 in-case of HLS) besides segments that describe these segment information @@ -370,7 +370,10 @@ def __init__( ) def stream(self, frame, rgb_mode=False): """ - Pipelines `ndarray` frames to FFmpeg Pipeline for transcoding into multi-bitrate streamable assets. + Pipes `ndarray` frames to FFmpeg Pipeline for transcoding them into chunked-encoded media segments of + streaming formats such as MPEG-DASH and HLS. + + !!! warning "[DEPRECATION NOTICE]: The `rgb_mode` parameter is deprecated and will be removed in a future version." Parameters: frame (ndarray): a valid numpy frame @@ -427,7 +430,8 @@ def stream(self, frame, rgb_mode=False): def transcode_source(self): """ - Transcodes entire Video Source _(with audio)_ into multi-bitrate streamable assets + Transcodes an entire video file _(with or without audio)_ into chunked-encoded media segments of + streaming formats such as MPEG-DASH and HLS. """ # check if function is called in correct context if not (self.__video_source): From 58a825655fc9376cbaca66c323fc7408d48aeef4 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 18:19:02 +0530 Subject: [PATCH 09/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20StreamGear:=20Rem?= =?UTF-8?q?ove=20non-essential=20aspect=20ratio=20parameter=20(Fixes=20#38?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ—οΈ Removed the `-aspect` parameter from the default FFmpeg pipeline - Previously, StreamGear would enforce a simplified aspect ratio using this parameter, which forces FFmpeg to use non-square pixels, leading to unwanted distortion on the output. - 🎨 Updated warning messages for better clarity. --- vidgear/gears/streamgear.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index d4b162597..ead6a0867 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -466,17 +466,14 @@ def __PreProcess(self, channels=0, rgb=False): if output_parameters["-vcodec"] != "copy": # NOTE: these parameters only supported when stream copy not defined output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") - aspect_ratio = Fraction( - self.__inputwidth / self.__inputheight - ).limit_denominator(10) - output_parameters["-aspect"] = ":".join(str(aspect_ratio).split("/")) + # Non-essential `-aspect` parameter is removed from the default pipeline. else: # log warnings for these parameters self.__params.pop("-vf", False) and logger.warning( - "Filtering and stream copy cannot be used together. Discarding `-vf` parameter!" + "Filtering and stream copy cannot be used together. Discarding specified `-vf` parameter!" ) self.__params.pop("-aspect", False) and logger.warning( - "Overriding aspect ratio with stream copy may produce invalid files. Discarding `-aspect` parameter!" + "Overriding aspect ratio with stream copy may produce invalid files. Discarding specified `-aspect` parameter!" ) # enable optimizations w.r.t selected codec From d6d78f1843e2df5464277ffc05fc1bd4f7d5edca Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 18:43:55 +0530 Subject: [PATCH 10/29] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Update=20documenta?= =?UTF-8?q?tion=20for=20forced=20termination?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ”Š Updated the StreamGear docs file to clarify the purpose and potential side effects of the `-enable_force_termination` parameter. StreamGear API: - πŸ§‘β€πŸ’» Modified the warning message to mention that forced termination can cause corrupted output in certain scenarios. - πŸ”Š Changed the log message in StreamGear to print a warning instead of an info message when forced termination is enabled. --- docs/gears/streamgear/params.md | 4 ++-- vidgear/gears/streamgear.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index fb81baecb..ddaebfcce 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -322,9 +322,9 @@ StreamGear API provides some exclusive internal parameters to easily generate St   -* **`-enable_force_termination`** _(bool)_: sets a special flag to enable the forced termination of FFmpeg process. Its usage is as follows: +* **`-enable_force_termination`** _(bool)_: sets a special flag to enable the forced termination of the FFmpeg process, required only if StreamGear is getting frozen when terminated. Its usage is as follows: - !!! warning "The `-enable_force_termination` flag can potentially cause unexpected behavior or prevent the program from producing the desired output in certain scenarios. It is recommended to use this flag with caution." + !!! warning "The `-enable_force_termination` flag can potentially cause unexpected behavior or corrupted output in certain scenarios. It is recommended to use this flag with caution." ```python stream_params = {"-enable_force_termination": True} # enables forced-termination behavior diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index ead6a0867..87026a63a 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -239,8 +239,8 @@ def __init__( if isinstance(enable_force_termination, bool): self.__forced_termination = enable_force_termination # log if forced termination is enabled - self.__forced_termination and logger.info( - "Forced termination is enabled for this run." + self.__forced_termination and logger.warning( + "Forced termination is enabled for this run. This may result in corrupted output in certain scenarios!" ) else: # handle improper values From 7cafc8b4b66a4c1298077d58e1dfa31017795184 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 30 May 2024 23:00:44 +0530 Subject: [PATCH 11/29] =?UTF-8?q?=F0=9F=92=A5=20StreamGear:=20Restricted?= =?UTF-8?q?=20`-livestream`=20parameter=20to=20Real-time=20Frames=20Mode?= =?UTF-8?q?=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’¬ Live streaming is intended for low-latency streaming of real-time frames, where chunks contain only the most recent frames. It doesn't make sense when streaming from a video file, as the entire file can be streamed normally without the need for live streaming. - πŸ—οΈ Disabled live streaming if `-video_source` is provided (Single-Source Mode) - πŸ”Š Log an error message when live streaming is attempted in Single-Source Mode, otherwise log normally in Real-time Frames Mode. Docs: - πŸ“ Refine description of `-streams` attribute of StreamGear API for better clarity in params. - Clarify primary stream generation and user-defined secondary streams. - Improve formatting and language for better readability. - ♻️ Replace usage of "tip" admonition with "example" for usage examples. - 🚸 Add warning for unsupported `-livestream` parameter in Single-Source Mode. - πŸ“ Updated respective notices for deprecate `terminate()` method and `rgb_mode` parameter. - πŸ”₯ Remove unsupported live-streaming usage examples in Single-Source Mode. --- docs/gears/streamgear/introduction.md | 2 +- docs/gears/streamgear/params.md | 34 +++++++++------- docs/gears/streamgear/rtfm/usage.md | 5 ++- docs/gears/streamgear/ssm/usage.md | 58 +++------------------------ docs/help/streamgear_faqs.md | 9 +---- vidgear/gears/streamgear.py | 16 +++++--- 6 files changed, 40 insertions(+), 84 deletions(-) diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index e93fc8b01..9a27fc9c1 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -38,7 +38,7 @@ With StreamGear, you can transcode source video/audio files and real-time video SteamGear currently supports both [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. -Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrates_. They are provided to the client before the streaming session begins. +Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrate_. They are provided to the client before the streaming session begins. !!! alert "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead." diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index ddaebfcce..c20b39d07 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -158,13 +158,15 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor StreamGear API provides some exclusive internal parameters to easily generate Streaming Assets and effortlessly tweak its internal properties. These parameters are discussed below: -* **`-streams`** _(list of dicts)_: This important attribute makes it simple and pretty straight-forward to define additional multiple streams as _list of dictionaries_ of different quality levels _(i.e. different bitrates or spatial resolutions)_ for streaming. +* **`-streams`** _(list of dicts)_: This important attribute makes it simple and pretty straight-forward to define additional multiple streams as _list of dictionaries_ of different quality levels _(i.e. different bitrate or spatial resolutions)_ for streaming. - !!! danger "Important `-streams` attribute facts" - * ==On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate[^1] as the input Video, at the index `0`.== - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically using `-bpps` and `-resolution` values. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. + ???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" + + * In addition to the user-defined Secondary Streams, ==StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution as the input frames and at default framerate[^1], at the index `0`.== + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. **To construct the additional stream dictionaries, you'll will need following sub-attributes:** @@ -189,7 +191,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St **Usage:** You can easily define any number of streams using `-streams` attribute as follows: - !!! tip "Usage example can be found [here ➢](../ssm/usage/#usage-with-additional-streams)" + !!! example "Usage example can be found [here ➢](../ssm/usage/#usage-with-additional-streams)" ```python stream_params = @@ -204,7 +206,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St * **`-video_source`** _(string)_: This attribute takes valid Video path as input and activates [**Single-Source Mode**](../ssm/overview), for transcoding it into multiple smaller chunks/segments for streaming after successful validation. Its value be one of the following: - !!! tip "Usage example can be found [here ➢](../ssm/usage/#bare-minimum-usage)" + !!! example "Usage example can be found [here ➢](../ssm/usage/#bare-minimum-usage)" * **Video Filename**: Valid path to Video file as follows: ```python @@ -229,7 +231,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St ```python stream_params = {"-audio": "/home/foo/foo1.aac"} # set input audio source: /home/foo/foo1.aac ``` - !!! tip "Usage example can be found [here ➢](../ssm/usage/#usage-with-custom-audio)" + !!! example "Usage example can be found [here ➢](../ssm/usage/#usage-with-custom-audio)" * **Audio URL** _(string)_: Valid URL of a network audio stream as follows: @@ -244,30 +246,32 @@ StreamGear API provides some exclusive internal parameters to easily generate St ```python stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 ``` - !!! tip "Usage example can be found [here ➢](../rtfm/usage/#usage-with-device-audio--input)" + !!! example "Usage example can be found [here ➢](../rtfm/usage/#usage-with-device-audio--input)"   -* **`-livestream`** _(bool)_: ***(optional)*** specifies whether to enable **Livestream Support**_(chunks will contain information for new frames only)_ for the selected mode, or not. You can easily set it to `True` to enable this feature, and default value is `False`. It can be used as follows: +* **`-livestream`** _(bool)_: ***(optional)*** specifies whether to enable **Low-latency Live-Streaming :material-video-wireless-outline:** in Real-time Frames Mode only, where chunks will contain information for new frames only and forget previous ones, or not. The default value is `False`. It can be used as follows: - !!! tip "Use `window_size` & `extra_window_size` FFmpeg parameters for controlling number of frames to be kept in New Chunks." + !!! warning "The `-livestream` optional parameter is **NOT** supported in [Single-Source mode](../ssm/overview)." ```python - stream_params = {"-livestream": True} # enable livestreaming + stream_params = {"-livestream": True} # enable live-streaming ``` + !!! example "Usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-live-streaming)" +   * **`-input_framerate`** _(float/int)_ : ***(optional)*** specifies the assumed input video source framerate, and only works in [Real-time Frames Mode](../usage/#b-real-time-frames-mode). It can be used as follows: - !!! tip "Usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)" - ```python stream_params = {"-input_framerate": 60.0} # set input video source framerate to 60fps ``` + !!! example "Usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)" +   * **`-bpp`** _(float/int)_: ***(optional)*** This attribute controls constant _Bits-Per-Pixel_(BPP) value, which is kind of a constant value to ensure good quality of high motion scenes ,and thereby used in calculating desired video-bitrate for streams. Higher the BPP, better will be motion quality. Its default value is `0.1`. Going over `0.1`helps to fill gaps between current bitrate and upload limit/ingest cap. Its value can be anything above `0.001`, can be used as follows: diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index e4262ad68..16458daf5 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -32,9 +32,10 @@ limitations under the License. - [x] In this mode, API **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. - [x] Always use `close()` function at the very end of the main code. -??? danger "[DEPRECATION NOTICE]: The `terminate()` method in StreamGear is now deprecated." +???+ danger "DEPRECATION NOTICES for `v0.3.3` and above" - The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. + - [ ] The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. + - [ ] The `rgb_mode` parameter in [`stream()`](../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) method, which earlier used to support RGB frames in Real-time Frames Mode is now deprecated, and will be removed in a future version. Only BGR format frames will be supported going forward. Please update your code to handle BGR format frames. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➢](../../../help/streamgear_ex/)" diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 19e5096ba..edf90464c 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -27,8 +27,11 @@ limitations under the License. - [x] In this mode, if input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams. - [x] Always use `close()` function at the very end of the main code. -??? danger "[DEPRECATION NOTICE]: The `terminate()` method in StreamGear is now deprecated." - The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. +???+ danger "DEPRECATION NOTICES for `v0.3.3` and above" + + - [ ] The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. + - [ ] The [`-livestream`](../../params/#a-exclusive-parameters) optional parameter is NOT supported in this Single-Source Mode. + !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➢](../../../help/streamgear_ex/)" @@ -80,57 +83,6 @@ Following is the bare-minimum code you need to get started with StreamGear API i   -## Bare-Minimum Usage with Live-Streaming - -You can easily activate ==Low-latency Livestreaming in Single-Source Mode== - chunks will contain information only for few new frames and forgets all previous ones, using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: - -!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." - -=== "DASH" - - !!! tip "Chunk size in DASH" - Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks in DASH stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in manifest will contain NO information of any older ones, and therefore resultant DASH stream will play only the most recent frames." - - ```python linenums="1" hl_lines="5" - # import required libraries - from vidgear.gears import StreamGear - - # activate Single-Source Mode with valid video input and enable livestreaming - stream_params = {"-video_source": 0, "-livestream": True} - # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="dash_out.mpd", **stream_params) - # transcode source - streamer.transcode_source() - # close - streamer.close() - ``` - -=== "HLS" - - !!! tip "Chunk size in HLS" - - Use `-hls_init_time` & `-hls_time` FFmpeg parameters for controlling number of frames to be kept in Chunks in HLS stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in playlist will contain NO information of any older ones, and therefore resultant HLS stream will play only the most recent frames." - - ```python linenums="1" hl_lines="5" - # import required libraries - from vidgear.gears import StreamGear - - # activate Single-Source Mode with valid video input and enable livestreaming - stream_params = {"-video_source": 0, "-livestream": True} - # describe a suitable master playlist location/name and assign params - streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # transcode source - streamer.transcode_source() - # close - streamer.close() - ``` - -  - ## Usage with Additional Streams > In addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrates or spatial resolutions, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. diff --git a/docs/help/streamgear_faqs.md b/docs/help/streamgear_faqs.md index a91be9ddc..31cdf9c65 100644 --- a/docs/help/streamgear_faqs.md +++ b/docs/help/streamgear_faqs.md @@ -77,14 +77,7 @@ limitations under the License.   -## How to use StreamGear API with RGB Frames? - -**Answer:** The `rgb_mode` parameter in [`stream()`](../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) method, which earlier used to support RGB frames in Real-time Frames Mode is now deprecated, and will be removed in a future version. Only BGR format frames will be supported going forward. Please update your code to handle BGR format frames. - -  - - -## How to use Hardware/GPU encoder for StreamGear trancoding? +## How to use Hardware/GPU encoder for transcoding in StreamGear API? **Answer:** [See this example ➢](../../gears/streamgear/rtfm/usage/#usage-with-hardware-video-encoder) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 87026a63a..c1cc01c14 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -224,11 +224,17 @@ def __init__( # handle whether to livestream? livestreaming = self.__params.pop("-livestream", False) if isinstance(livestreaming, bool): - self.__livestreaming = livestreaming - # log if live streaming is enabled - livestreaming and logger.info( - "Live-Streaming Mode is enabled for this run." - ) + # NOTE: `livestream` is only available with real-time mode. + self.__livestreaming = livestreaming if not (self.__video_source) else False + if self.__video_source: + logger.error( + "Live-Streaming is only available with Real-time Mode. Refer docs for more information." + ) + else: + # log if live streaming is enabled + livestreaming and logger.info( + "Live-Streaming is successfully enabled for this run." + ) else: # reset improper values self.__livestreaming = False From bfc521d824b64268db23976701be4f34c87bfea0 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 31 May 2024 00:40:41 +0530 Subject: [PATCH 12/29] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Docs:=20Refactored?= =?UTF-8?q?=20the=20StreamGear=20API=20Parameters=20documentation=20to=20e?= =?UTF-8?q?nhance=20clarity=20and=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ§‘β€πŸ’» Simplified and clarified descriptions for `output`, `format`, `custom_ffmpeg`, and `stream_params` parameters. - πŸ“ Improved examples for defining output paths, filenames, and URLs. - 🚸 Enhanced warnings, tips, and information admonitions for better user guidance. - 🎨 Reformatted code examples to provide clearer usage patterns. - ✏️ Updated formatting and grammar for consistency and precision. --- docs/gears/streamgear/params.md | 203 ++++++++++++++++------------ docs/gears/streamgear/rtfm/usage.md | 4 +- docs/gears/streamgear/ssm/usage.md | 5 +- 3 files changed, 120 insertions(+), 92 deletions(-) diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index c20b39d07..6f7a9b131 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -24,15 +24,13 @@ limitations under the License. ## **`output`** -This parameter sets the valid filename/path for storing the StreamGear assets _(Manifest file (such as MPD in-case of DASH) or a Master Playlist (such as M3U8 in-case of Apple HLS) & Transcoded sequence of segments)_. +This parameter sets the valid filename/path for storing the StreamGear assets, including Manifest file _(such as MPD in case of DASH)_ or a Master Playlist _(such as M3U8 in case of Apple HLS)_ and generated sequence of chunks/segments. -!!! warning "StreamGear API will throw `ValueError` if `output` provided is empty or invalid." +!!! warning "StreamGear API will throw `ValueError` if the provided `output` is empty or invalid." -!!! failure "Make sure to provide _valid filename with valid file-extension_ for selected [`format`](#format) value _(such as `.mpd` in case of MPEG-DASH and `.m3u8` in case of APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." +!!! failure "Make sure to provide a valid filename with a valid file extension for the selected `format` value _(such as `.mpd` for MPEG-DASH and `.m3u8` for APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." -!!! note "StreamGear generated sequence of multiple chunks/segments are also stored in the same directory." - -!!! tip "You can easily delete all previous assets at `output` location, by using [`-clear_prev_assets`](#a-exclusive-parameters) attribute of [`stream_params`](#stream_params) dictionary parameter." +!!! tip "You can easily delete all previous assets at the `output` location by using the [`-clear_prev_assets`](#a-exclusive-parameters) attribute of the [`stream_params`](#stream_params) dictionary parameter." **Data-Type:** String @@ -40,60 +38,63 @@ This parameter sets the valid filename/path for storing the StreamGear assets _( Its valid input can be one of the following: -* **Path to directory**: Valid path of the directory. In this case, StreamGear API will automatically assign a unique filename for Manifest file. This can be defined as follows: +* **Path to directory**: Valid path of the directory. In this case, StreamGear API will automatically assign a unique filename for the Manifest file. This can be defined as follows: === "DASH" ```python - streamer = StreamGear(output = "/home/foo/foo1") # Define streamer with manifest saving directory path + # Define streamer with output directory path for saving DASH assets + streamer = StreamGear(output = "/home/foo/bar") ``` === "HLS" ```python - streamer = StreamGear(output = "/home/foo/foo1", format="hls") # Define streamer with playlist saving directory path + # Define streamer with output directory path for saving HLS assets + streamer = StreamGear(output = "/home/foo/bar", format="hls") ``` -* **Filename** _(with/without path)_: Valid filename(_with valid extension_) of the output Manifest file. In case filename is provided without path, then current working directory will be used. +* **Filename** _(with/without path)_: Valid filename _(with a valid extension)_ of the output Manifest or Playlist file. If the filename is provided without a path, the current working directory will be used. This can be defined as follows: === "DASH" ```python - streamer = StreamGear(output = "output_foo.mpd") # Define streamer with manifest file name + # Define streamer with output manifest filename + streamer = StreamGear(output = "output_dash.mpd") ``` === "HLS" ```python - streamer = StreamGear(output = "output_foo.m3u8", format="hls") # Define streamer with playlist file name + # Define streamer with output playlist filename + streamer = StreamGear(output = "output_hls.m3u8", format="hls") ``` -* **URL**: Valid URL of a network stream with a protocol supported by installed FFmpeg _(verify with command `ffmpeg -protocols`)_ only. This is useful for directly storing assets to a network server. For example, you can use a `http` protocol URL as follows: - +* **URL**: Valid URL of a network stream with a protocol supported by the installed FFmpeg _(verify with the `ffmpeg -protocols` command)_. This is useful for directly storing assets to a network server. For example, you can use an `HTTP` protocol URL as follows: === "DASH" ```python - streamer = StreamGear(output = "http://195.167.1.101/live/test.mpd") #Define streamer + # Define streamer with output manifest URL + streamer = StreamGear(output = "http://some_dummy_serverip/live/output_dash.mpd") ``` === "HLS" ```python - streamer = StreamGear(output = "http://195.167.1.101/live/test.m3u8", format="hls") #Define streamer + # Define streamer with output playlist URL + streamer = StreamGear(output = "http://some_dummy_serverip/live/output_hls.m3u8", format="hls") ```   ## **`format`** +This parameter enables the adaptive HTTP streaming format. This parameter currently supported these formats: `dash` _(i.e [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/))_ and `hls` _(i.e [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming))_. -This parameter select the adaptive HTTP streaming formats. For now, the supported format are: `dash` _(i.e [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/))_ and `hls` _(i.e [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming))_. - -!!! warning "Any invalid value to `format` parameter will result in ValueError!" - -!!! failure "Make sure to provide _valid filename with valid file-extension_ in [`output`](#output) for selected `format` value _(such as `.mpd` in case of MPEG-DASH and `.m3u8` in case of APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." +!!! danger "Make sure to provide a valid filename with a valid file extension in the [`output`](#output) parameter for the selected `format` value _(i.e., `.mpd` for MPEG-DASH and `.m3u8` for APPLE-HLS)_, otherwise StreamGear will throw an `AssertionError`." +!!! warning "Any improper value assigned to `format` parameter will result in a `ValueError`!" **Data-Type:** String @@ -104,16 +105,17 @@ This parameter select the adaptive HTTP streaming formats. For now, the supporte === "DASH" ```python - StreamGear(output = "output_foo.mpd", format="dash") + # Define streamer with DASH format + StreamGear(output = "output_dash.mpd", format="dash") ``` === "HLS" ```python - StreamGear(output = "output_foo.m3u8", format="hls") + # Define streamer with HLS format + StreamGear(output = "output_hls.m3u8", format="hls") ``` -   @@ -121,9 +123,10 @@ This parameter select the adaptive HTTP streaming formats. For now, the supporte This parameter assigns the custom _path/directory_ where the custom/downloaded FFmpeg executables are located. -!!! info "Behavior on Windows" +!!! info "Behavior on :fontawesome-brands-windows: Windows Systems" + + On Windows, if a custom FFmpeg executable's path/directory is not provided through this `custom_ffmpeg` parameter, the StreamGear API will automatically attempt to download and extract suitable Static FFmpeg binaries at a suitable location on your Windows machine. More information can be found [here ➢](../ffmpeg_install/#a-auto-installation). - If a custom FFmpeg executable's path | directory is not provided through `custom_ffmpeg` parameter on Windows machine, then StreamGear API will ==automatically attempt to download and extract suitable Static FFmpeg binaries at suitable location on your windows machine==. More information can be found [here ➢](../ffmpeg_install/#a-auto-installation). **Data-Type:** String @@ -132,8 +135,8 @@ This parameter assigns the custom _path/directory_ where the custom/downloaded F **Usage:** ```python -# If ffmpeg executables are located at "/foo/foo1/ffmpeg" -StreamGear(output = 'output_foo.mpd', custom_ffmpeg="/foo/foo1/ffmpeg") +# Define streamer with custom ffmpeg binary +StreamGear(output = 'output_foo.mpd', custom_ffmpeg="C://foo//bar//ffmpeg.exe") ```   @@ -141,11 +144,9 @@ StreamGear(output = 'output_foo.mpd', custom_ffmpeg="/foo/foo1/ffmpeg") ## **`stream_params`** -This parameter allows us to exploit almost all FFmpeg supported parameters effortlessly and flexibly change its internal settings for transcoding and seamlessly generating high-quality streams. All [supported parameters](#supported-parameters) can formatting as attributes for this dictionary parameter: - - -!!! danger "Kindly read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully, before passing any additional values to `stream_params` parameter. Wrong values may result in undesired errors or no output at all." +This parameter allows developers to leverage nearly all FFmpeg options, providing effortless and flexible control over its internal settings for transcoding and generating high-quality streams. All [supported parameters](#supported-parameters) can be formatted as attributes within this dictionary parameter. +!!! danger "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." **Data-Type:** Dictionary @@ -169,86 +170,102 @@ StreamGear API provides some exclusive internal parameters to easily generate St * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. - **To construct the additional stream dictionaries, you'll will need following sub-attributes:** + **To construct the additional stream dictionaries, you will need the following sub-attributes::** - * `-resolution` _(string)_: It is **compulsory** to define the required resolution/dimension/size for the stream, otherwise given stream will be rejected. Its value can be a `"{width}x{height}"` as follows: + * `-resolution` _(string)_: It is **compulsory** to define the required resolution/dimension/size for the stream, otherwise, the given stream will be rejected. Its value should be in the format `"{width}x{height}"`, as shown below: ```python - "-streams" = [{"-resolution": "1280x720"}] # to produce a 1280x720 resolution/scale + # produce a 1280x720 resolution/scale stream + "-streams" = [{"-resolution": "1280x720"}] ``` - * `-video_bitrate` _(string)_: It is an **optional** _(can be ignored if `-framerate` parameter is defined)_ sub-attribute that generally determines the bandwidth and quality of stream, i.e. the higher the bitrate, the better the quality and the larger will be bandwidth and more will be strain on network. It value is generally in `kbps` _(kilobits per second)_ for OBS (Open Broadcasting Softwares). You can easily define this attribute as follows: + * `-video_bitrate` _(string)_: This is an **optional** sub-attribute _(can be ignored if the `-framerate` parameter is defined)_ that generally determines the bandwidth and quality of the stream. The higher the bitrate, the better the quality and the larger the bandwidth, which can place more strain on the network. Its value is typically in `k` _(kilobits per second)_ or `M` _(Megabits per second)_. Define this attribute as follows: ```python - "-streams" : [{"-resolution": "1280x720", "-video_bitrate": "2000k"}] # to produce a 1280x720 resolution and 2000kbps bitrate stream + # produce a 1280x720 resolution and 2000 kbps bitrate stream + "-streams" : [{"-resolution": "1280x720", "-video_bitrate": "2000k"}] ``` - * `-framerate` _(float/int)_: It is another **optional** _(can be ignored if `-video_bitrate` parameter is defined)_ sub-attribute that defines the assumed framerate for the stream. It's value can be float/integer as follows: + * `-framerate` _(float/int)_: This is another **optional** sub-attribute _(can be ignored if the `-video_bitrate` parameter is defined)_ that defines the assumed framerate for the stream. Its value can be a float or integer, as shown below: ```python - "-streams" : [{"-resolution": "1280x720", "-framerate": "60.0"}] # to produce a 1280x720 resolution and 60fps framerate stream + # produce a 1280x720 resolution and 60fps framerate stream + "-streams" : [{"-resolution": "1280x720", "-framerate": "60.0"}] ``` **Usage:** You can easily define any number of streams using `-streams` attribute as follows: - !!! example "Usage example can be found [here ➢](../ssm/usage/#usage-with-additional-streams)" - ```python stream_params = {"-streams": - [{"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": "30.0"}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": "60.0"}, # Stream3: 640x360 at 60fps - ]} + [ + {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate + {"-resolution": "1280x720", "-framerate": 30}, # Stream2: 1280x720 at 30fps + {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + ] + } ``` + !!! example "Its usage example can be found [here ➢](../ssm/usage/#usage-with-additional-streams)" +   -* **`-video_source`** _(string)_: This attribute takes valid Video path as input and activates [**Single-Source Mode**](../ssm/overview), for transcoding it into multiple smaller chunks/segments for streaming after successful validation. Its value be one of the following: +* **`-video_source`** _(string)_: This attribute takes a valid video path as input and activates [**Single-Source Mode**](../ssm/overview), for transcoding it into multiple smaller chunks/segments for streaming after successful validation. Its value can be one of the following: - !!! example "Usage example can be found [here ➢](../ssm/usage/#bare-minimum-usage)" + * **Video Filename**: Valid path to a video file as follows: - * **Video Filename**: Valid path to Video file as follows: ```python - stream_params = {"-video_source": "/home/foo/foo1.mp4"} # set input video source: /home/foo/foo1.mp4 + # set video source as `/home/foo/bar.mp4` + stream_params = {"-video_source": "/home/foo/bar.mp4"} ``` * **Video URL**: Valid URL of a network video stream as follows: - !!! danger "Make sure given Video URL has protocol that is supported by installed FFmpeg. _(verify with `ffmpeg -protocols` terminal command)_" + !!! danger "Ensure the given video URL uses a protocol supported by the installed FFmpeg _(verify with `ffmpeg -protocols` terminal command)_." ```python - stream_params = {"-video_source": "http://livefeed.com:5050"} # set input video source: http://livefeed.com:5050 + # set video source as `http://livefeed.com:5050` + stream_params = {"-video_source": "http://livefeed.com:5050"} ``` + !!! example "Its usage example can be found [here ➢](../ssm/usage/#bare-minimum-usage)" +   +* **`-audio`** _(string/list)_: This attribute takes an external custom audio path _(as a string)_ or an audio device name followed by a suitable demuxer _(as a list)_ as the audio source input for all StreamGear streams. Its value can be one of the following: -* **`-audio`** _(string/list)_: This attribute takes external custom audio path _(as `string`)_ or audio device name followed by suitable demuxer _(as `list`)_ as audio source input for all StreamGear streams. Its value be one of the following: + !!! failure "Ensure the provided `-audio` audio source is compatible with the input video source. Incompatibility can cause multiple errors or result in no output at all." - !!! failure "Make sure this audio-source is compatible with provided video -source, otherwise you could encounter multiple errors, or even no output at all!" + * **Audio Filename** _(string)_: Valid path to an audio file as follows: - * **Audio Filename** _(string)_: Valid path to Audio file as follows: ```python - stream_params = {"-audio": "/home/foo/foo1.aac"} # set input audio source: /home/foo/foo1.aac + # set audio source as `/home/foo/foo1.aac` + stream_params = {"-audio": "/home/foo/foo1.aac"} ``` - !!! example "Usage example can be found [here ➢](../ssm/usage/#usage-with-custom-audio)" + + !!! example "Its usage examples can be found [here ➢](../ssm/usage/#usage-with-custom-audio) and [here ➢](../ssm/usage/#usage-with-file-audio-input)" * **Audio URL** _(string)_: Valid URL of a network audio stream as follows: - !!! danger "Make sure given Video URL has protocol that is supported by installed FFmpeg. _(verify with `ffmpeg -protocols` terminal command)_" + !!! danger "Ensure the given audio URL uses a protocol supported by the installed FFmpeg _(verify with `ffmpeg -protocols` terminal command)_." ```python - stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 + # set input audio source as `https://exampleaudio.org/example-160.mp3` + stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} ``` - * **Device name and Demuxer** _(list)_: Valid audio device name followed by suitable demuxer as follows: + * **Device name and Demuxer** _(list)_: Valid audio device name followed by a suitable demuxer as follows: ```python - stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 + # Assign appropriate input audio-source device (compatible with video source) and its demuxer + stream_params = {"-audio": [ + "-f", + "dshow", + "-i", + "audio=Microphone (USB2.0 Camera)", + ]} ``` - !!! example "Usage example can be found [here ➢](../rtfm/usage/#usage-with-device-audio--input)" - + !!! example "Its usage example can be found [here ➢](../rtfm/usage/#usage-with-device-audio--input)"   @@ -260,68 +277,74 @@ StreamGear API provides some exclusive internal parameters to easily generate St stream_params = {"-livestream": True} # enable live-streaming ``` - !!! example "Usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-live-streaming)" + !!! example "Its usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-live-streaming)"   -* **`-input_framerate`** _(float/int)_ : ***(optional)*** specifies the assumed input video source framerate, and only works in [Real-time Frames Mode](../usage/#b-real-time-frames-mode). It can be used as follows: +* **`-input_framerate`** _(float/int)_ : ***(optional)*** This parameter specifies the assumed input video source framerate and only works in [Real-time Frames Mode](../usage/#b-real-time-frames-mode). Its default value is `25.0` fps. Its value can be a float or integer, as shown below: ```python - stream_params = {"-input_framerate": 60.0} # set input video source framerate to 60fps + # set input video source framerate to 60fps + stream_params = {"-input_framerate": 60.0} ``` - !!! example "Usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)" + !!! example "Its usage example can be found [here ➢](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)"   -* **`-bpp`** _(float/int)_: ***(optional)*** This attribute controls constant _Bits-Per-Pixel_(BPP) value, which is kind of a constant value to ensure good quality of high motion scenes ,and thereby used in calculating desired video-bitrate for streams. Higher the BPP, better will be motion quality. Its default value is `0.1`. Going over `0.1`helps to fill gaps between current bitrate and upload limit/ingest cap. Its value can be anything above `0.001`, can be used as follows: +* **`-bpp`** _(float/int)_: ***(optional)*** This attribute controls the constant **BPP** _(Bits-Per-Pixel)_ value, which helps ensure good quality in high motion scenes by determining the desired video bitrate for streams. A higher BPP value improves motion quality. The default value is `0.1`. Increasing the BPP value helps fill the gaps between the current bitrate and the upload limit/ingest cap. Its value can be anything above `0.001` and can be used as follows: - !!! tip "Important BPP tips for streaming" - * `-bpp` a sensitive value, try 0.001, and then make increments in 0.0001 to fine tune - * If your desired resolution/fps/audio combination is below maximum service bitrate, raise BPP to match it for extra quality. - * It is generally better to lower resolution (and/or fps) and raise BPP than raise resolution and loose on BPP. + !!! tip "Important points while tweaking BPP" + * BPP is a sensitive value; start with `0.001` and make small increments (`0.0001`) to fine-tune. + * If your desired resolution/fps/audio combination is below the maximum service bitrate, raise BPP to match it for extra quality. + * It is generally better to lower resolution _(and/or `fps`)_ and raise BPP than to raise resolution and lose BPP. ```python - stream_params = {"-bpp": 0.05} # sets BPP to 0.05 + # sets BPP to 0.05 + stream_params = {"-bpp": 0.05} ```   -* **`-gop`** _(float/int)_ : ***(optional)*** specifies the number of frames between two I-frames for accurate GOP length. By increasing the length of the GOP, there will be fewer I-frames per time frame, which minimizes bandwidth consumption. So, for example, with extremely complex subjects such as water sports or action mode, you’ll want to use a shorter GOP length such as 15 or below that results in excellent video quality. For more static video such as talking heads, then much longer GOP sizes are not only sufficient but also more efficient. It can be used as follows: +* **`-gop`** _(float/int)_ : ***(optional)*** This parameter specifies the number of frames between two I-frames for accurate **GOP** _(Group of Pictures)_ length. Increasing the GOP length reduces the number of I-frames per time frame, minimizing bandwidth consumption. For example, with complex subjects such as water sports or action scenes, a shorter GOP length _(e.g., `15` or below)_ results in excellent video quality. For more static video, such as talking heads, much longer GOP sizes are not only sufficient but also more efficient. It can be used as follows: - !!! tip "The larger the GOP size, the more efficient the compression and the less bandwidth you will need" + !!! tip "The larger the GOP size, the more efficient the compression and the less bandwidth you will need." - !!! info "By default, StreamGear automatically sets recommended fixed GOP value _(i.e. every two seconds)_ w.r.t input framerate and selected encoder." + !!! info "By default, StreamGear automatically sets a recommended fixed GOP value _(i.e., every two seconds)_ based on the input framerate and selected encoder." ```python - stream_params = {"-gop": 70} # set GOP length to 70 + # set GOP length to 70 + stream_params = {"-gop": 70} ```   -* **`-clones`** _(list)_: ***(optional)*** sets the special FFmpeg parameters that are repeated more than once in the command _(For more info., see [this issue](https://github.com/abhiTronix/vidgear/issues/141))_ as **list** only. Usage is as follows: +* **`-clones`** _(list)_: ***(optional)*** This parameter sets special FFmpeg options that need to be repeated more than once in the command. For more information, see [this issue](https://github.com/abhiTronix/vidgear/issues/141). It accepts values as a **list** only. Usage is as follows: ```python + # sets special FFmpeg options repeated multiple times stream_params = {"-clones": ['-map', '0:v:0', '-map', '1:a?']} ```   -* **`-ffmpeg_download_path`** _(string)_: ***(optional)*** sets the custom directory for downloading FFmpeg Static Binaries in Compression Mode, during the [Auto-Installation](../ffmpeg_install/#a-auto-installation) on Windows Machines Only. If this parameter is not altered, then these binaries will auto-save to the default temporary directory (for e.g. `C:/User/temp`) on your windows machine. It can be used as follows: +* **`-ffmpeg_download_path`** _(string)_: ***(optional)*** This parameter sets a custom directory for downloading FFmpeg static binaries in Compression Mode during the [**Auto-Installation**](../ffmpeg_install/#a-auto-installation) step on Windows machines only. If this parameter is not altered, the binaries will be saved to the default temporary directory _(e.g., `C:/User/foo/temp`)_ on your Windows machine. It can be used as follows: ```python - stream_params = {"-ffmpeg_download_path": "C:/User/foo/foo1"} # will be saved to "C:/User/foo/foo1" + # download FFmpeg static binaries to `C:/User/foo/bar` + stream_params = {"-ffmpeg_download_path": "C:/User/foo/bar"} ```   -* **`-clear_prev_assets`** _(bool)_: ***(optional)*** specify whether to force-delete any previous copies of StreamGear Assets _(i.e. Manifest files(.mpd) & streaming chunks(.m4s) etc.)_ present at path specified by [`output`](#output) parameter. You can easily set it to `True` to enable this feature, and default value is `False`. It can be used as follows: +* **`-clear_prev_assets`** _(bool)_: ***(optional)*** This parameter specifies whether to force-delete any previous copies of StreamGear assets _(i.e., manifest (`mpd`), playlist (`mu38`), and streaming chunks (`.m4s`), etc. files)_ present at the path specified by the [`output`](#output) parameter. The default value is `False`. It can be used as follows: - !!! info "In Single-Source Mode, additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." + !!! info "Additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." ```python - stream_params = {"-clear_prev_assets": True} # will delete all previous assets + # delete all previous assets + stream_params = {"-clear_prev_assets": True} ```   @@ -331,22 +354,24 @@ StreamGear API provides some exclusive internal parameters to easily generate St !!! warning "The `-enable_force_termination` flag can potentially cause unexpected behavior or corrupted output in certain scenarios. It is recommended to use this flag with caution." ```python - stream_params = {"-enable_force_termination": True} # enables forced-termination behavior + # enables forced termination of FFmpeg process + stream_params = {"-enable_force_termination": True} ```   #### B. FFmpeg Parameters -Almost all FFmpeg parameter can be passed as dictionary attributes in `stream_params`. For example, for using `libx264 encoder` to produce a lossless output video, we can pass required FFmpeg parameters as dictionary attributes, as follows: +Almost all FFmpeg parameters can be passed as dictionary attributes in `stream_params`. For example, to use the `libx264` encoder to produce a lossless output video, you can pass the required FFmpeg parameters as dictionary attributes as follows: -!!! tip "Kindly check [H.264 docs ➢](https://trac.ffmpeg.org/wiki/Encode/H.264) and other [FFmpeg Docs ➢](https://ffmpeg.org/documentation.html) for more information on these parameters" +!!! tip "Please check the [H.264 documentation ➢](https://trac.ffmpeg.org/wiki/Encode/H.264) and [FFmpeg Documentation ➢](https://ffmpeg.org/documentation.html) for more information on following parameters." -!!! failure "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." +!!! failure "All FFmpeg parameters are case-sensitive. Double-check each parameter if any errors occur." -!!! note "In addition to these parameters, almost any FFmpeg parameter _(supported by installed FFmpeg)_ is also supported. But make sure to read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully first." +!!! note "In addition to these parameters, almost any FFmpeg parameter _(supported by the installed FFmpeg)_ is also supported. Be sure to read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully first." ```python +# libx264 encoder and its supported parameters stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "zerolatency"} ``` @@ -354,9 +379,9 @@ stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "ze ### Supported Encoders and Decoders -All the encoders and decoders that are compiled with FFmpeg in use, are supported by StreamGear API. You can easily check the compiled encoders by running following command in your terminal: +All encoders and decoders compiled with the FFmpeg in use are supported by the StreamGear API. You can check the compiled encoders by running the following command in your terminal: -!!! info "Similarly, supported Demuxers and Filters depends upon compiled FFmpeg in use." +!!! info "Similarly, supported audio/video demuxers and filters depend on the FFmpeg binaries in use." ```sh # for checking encoder diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 16458daf5..34c153bed 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -499,7 +499,7 @@ To generate Secondary Streams, add each desired resolution and bitrate/framerate !!! info "A more detailed information on `-streams` attribute can be found [here ➢](../../params/#a-exclusive-parameters)" -!!! alert "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." +!!! alert "In this mode, StreamGear DOES NOT automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." ???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" @@ -1051,6 +1051,8 @@ In this example, we will be using `h264_vaapi` as our Hardware Encoder and speci V..... vp8_vaapi VP8 (VAAPI) (codec vp8) ``` +!!! failure "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." + === "DASH" diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index edf90464c..4f1753113 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -85,7 +85,7 @@ Following is the bare-minimum code you need to get started with StreamGear API i ## Usage with Additional Streams -> In addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrates or spatial resolutions, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. +> In addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrate or spatial resolutions, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. To generate Secondary Streams, add each desired resolution and bitrate/framerate as a list of dictionaries to the `-streams` attribute. StreamGear will handle the rest automatically. The complete example is as follows: @@ -227,7 +227,8 @@ In this example, we'll use the [H.265/HEVC](https://trac.ffmpeg.org/wiki/Encode/ !!! info "This example is just conveying the idea on how to use FFmpeg's internal encoders/parameters with StreamGear API. You can use any FFmpeg parameter in the similar manner." -!!! danger "Refer to the FFmpeg Documentation (https://ffmpeg.org/documentation.html) before passing FFmpeg values to `stream_params`. Incorrect values may result in errors or no output." +!!! danger "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." + === "DASH" From abeb9550ad54707e352a178f745657715f09bef6 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 31 May 2024 20:40:03 +0530 Subject: [PATCH 13/29] =?UTF-8?q?=E2=98=82=EF=B8=8F=20CI:=20Improved=20cod?= =?UTF-8?q?e=20coverage=20for=20StreamGear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ”₯ Removed unused imports. --- vidgear/tests/streamer_tests/test_IO_rtf.py | 6 +++--- vidgear/tests/streamer_tests/test_IO_ss.py | 8 +++++--- .../tests/streamer_tests/test_streamgear_modes.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index fe3bd5bd7..b968ecb77 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -17,10 +17,10 @@ limitations under the License. =============================================== """ -# import the necessary packages -import numpy as np +# import the necessary packages import pytest +import numpy as np from vidgear.gears import StreamGear @@ -91,7 +91,7 @@ def test_invalid_params_rtf(format): random_data = np.random.random(size=(480, 640, 3)) * 255 input_data = random_data.astype(np.uint8) - stream_params = {"-vcodec": "unknown"} + stream_params = {"-vcodec": "unknown", "-livestream": "invalid"} streamer = StreamGear( output="output{}".format(".mpd" if format == "dash" else ".m3u8"), format=format, diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index 0e0226b8c..e129bcea9 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -17,10 +17,9 @@ limitations under the License. =============================================== """ -# import the necessary packages +# import the necessary packages import os -import numpy as np import pytest import tempfile import subprocess @@ -76,7 +75,10 @@ def test_paths_ss(path, format): """ streamer = None try: - stream_params = {"-video_source": return_testvideo_path()} + stream_params = { + "-video_source": return_testvideo_path(), + "-livestream": "invalid", + } streamer = StreamGear(output=path, format=format, logging=True, **stream_params) except Exception as e: if isinstance(e, ValueError): diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index a8c34d983..6cf7887c3 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -17,8 +17,8 @@ limitations under the License. =============================================== """ -# import the necessary packages +# import the necessary packages import os import cv2 import queue @@ -257,6 +257,7 @@ def test_ss_livestream(format): stream_params = { "-video_source": return_testvideo_path(), "-livestream": True, + "-clear_prev_assets": "invalid", "-remove_at_exit": 1, } streamer = StreamGear( @@ -337,6 +338,7 @@ def test_rtf_livestream(format): stream = CamGear(source=return_testvideo_path(), **options).start() stream_params = { "-livestream": True, + "-enable_force_termination": True, } streamer = StreamGear(output=assets_file_path, format=format, **stream_params) while True: @@ -346,7 +348,7 @@ def test_rtf_livestream(format): break streamer.stream(frame) stream.stop() - streamer.close() + streamer.terminate() except Exception as e: if not isinstance(e, queue.Empty): pytest.fail(str(e)) @@ -367,6 +369,9 @@ def test_input_framerate_rtf(format): stream_params = { "-clear_prev_assets": True, "-input_framerate": test_framerate, + "-vcodec": "copy", + "-vf": "format=yuv420p", + "-aspect": "4:3", } if format == "hls": stream_params.update( @@ -416,6 +421,7 @@ def test_input_framerate_rtf(format): "-bpp": 0.2000, "-gop": 125, "-vcodec": "libx265", + "-enable_force_termination": "invalid", }, "hls", ), @@ -698,7 +704,7 @@ def test_audio(stream_params, format): ) def test_multistreams(format, stream_params): """ - Testing Support for additional Secondary Streams of variable bitrates or spatial resolutions. + Testing Support for additional Secondary Streams of variable bitrate or spatial resolutions. """ assets_file_path = os.path.join( return_assets_path(False if format == "dash" else True), From 6b2532270571a7e9dc8d95aa88411071b17f16ec Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 31 May 2024 22:26:37 +0530 Subject: [PATCH 14/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Maintenance:=20Ha?= =?UTF-8?q?ndled=20signal=20interruption=20for=20non-Windows=20systems?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’¬ The `close()` methods in StreamGear and WriteGear were sending the `CTRL_BREAK_EVENT` signal to terminate the underlying process, which only works on Windows systems. This commit modifies the code to send the appropriate signal based on the operating system: - For Windows, it sends the `CTRL_BREAK_EVENT` signal - For non-Windows systems, it sends the `SIGINT` signal --- vidgear/gears/streamgear.py | 6 ++++-- vidgear/gears/writegear.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index c1cc01c14..11599c469 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -1209,8 +1209,10 @@ def close(self): if self.__forced_termination: self.__process.terminate() else: - # send CTRL_BREAK_EVENT signal - self.__process.send_signal(signal.CTRL_BREAK_EVENT) + # send `CTRL_BREAK_EVENT` signal if Windows else `SIGINT` + self.__process.send_signal( + signal.CTRL_BREAK_EVENT if self.__os_windows else signal.SIGINT + ) # wait if process is still processing self.__process.wait() # discard process diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 5bd217fd1..b2584d766 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -776,8 +776,10 @@ def close(self): if self.__forced_termination: self.__process.terminate() else: - # send CTRL_BREAK_EVENT signal - self.__process.send_signal(signal.CTRL_BREAK_EVENT) + # send `CTRL_BREAK_EVENT` signal if Windows else `SIGINT` + self.__process.send_signal( + signal.CTRL_BREAK_EVENT if self.__os_windows else signal.SIGINT + ) # wait if process is still processing self.__process.wait() else: From b9b0a4b6a2c9f54f131465a7fb5fc1e0422eb6ba Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 31 May 2024 22:54:49 +0530 Subject: [PATCH 15/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20StreamGear:=20Fix?= =?UTF-8?q?ed=20`UnboundLocalError`=20for=20`seg=5Fduration`=20in=20`gener?= =?UTF-8?q?ate=5Fdash=5Fstream`=20method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ”Š Updated logging message format in `evaluate_streams` method of for consistency. WriteGear: - πŸ› Fixed dictionary comprehension logic to strip only string values. CI: - πŸ’š Fixed expected duration value in parameterized test case from `8` to `8.44` since `test_extract_time` function now supports floating point values. --- vidgear/gears/streamgear.py | 4 ++-- vidgear/gears/writegear.py | 2 +- vidgear/tests/test_helper.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 11599c469..28d858ef9 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -731,7 +731,7 @@ def __evaluate_streams(self, streams, output_params, bpp): # iterate over given streams for idx, stream in enumerate(streams): # log stream processing - self.__logging and logger.debug("Processing #{} stream ::".format(idx)) + self.__logging and logger.debug("Processing Stream: #{}".format(idx)) # make copy stream_copy = stream.copy() # handle intermediate stream data as dictionary @@ -999,7 +999,7 @@ def __generate_dash_stream(self, input_params, output_params): output_params["-use_timeline"] = 0 else: # `seg_duration` must be greater than or equal to 0 - output_params["-seg_duration"] = self.__params.pop("-seg_duration", 5) + seg_duration = self.__params.pop("-seg_duration", 5) if isinstance(seg_duration, int) and seg_duration >= 0: output_params["-seg_duration"] = seg_duration else: diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index b2584d766..8a7f0b721 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -166,7 +166,7 @@ def __init__( # cleans and reformat output parameters self.__output_parameters = { - str(k).strip(): (v.strip() if not isinstance(v, str) else v) + str(k).strip(): (v.strip() if isinstance(v, str) else v) for k, v in output_params.items() } # log it if specified diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 19f63a9be..1862ea5bd 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -437,7 +438,7 @@ def test_create_blank_frame(frame, text): @pytest.mark.parametrize( "value, result", [ - ("Duration: 00:00:08.44, start: 0.000000, bitrate: 804 kb/s", 8), + ("Duration: 00:00:08.44, start: 0.000000, bitrate: 804 kb/s", 8.44), ("Duration: 00:07:08 , start: 0.000000, bitrate: 804 kb/s", 428), ("", False), ], From 428621dac7cfd19185393c57b6d4360f74174ec2 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 31 May 2024 23:45:39 +0530 Subject: [PATCH 16/29] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20StreamGear:=20Fix?= =?UTF-8?q?ed=20stream=20`copy`=20incompatible=20with=20Real-time=20Frames?= =?UTF-8?q?=20Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸš‘οΈ This commit addresses an bug where the `-vcodec copy` parameter was incorrectly defined when using the Real-time Frames Mode in StreamGear. Stream copy is not compatible with this mode since it requires encoding the frames before streaming. Additionally, If the Real-time Frames Mode is active and `-vcodec copy` is specified, a warning log message is printed to notify the user that the stream copy parameter is being discarded. --- vidgear/gears/streamgear.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 28d858ef9..e84c949cb 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -466,14 +466,24 @@ def __PreProcess(self, channels=0, rgb=False): output_parameters = OrderedDict() # pre-assign default codec parameters (if not assigned by user). default_codec = "libx264rgb" if rgb else "libx264" - output_parameters["-vcodec"] = self.__params.pop("-vcodec", default_codec) - - # enforce compatibility + output_vcodec = self.__params.pop("-vcodec", default_codec) + # enforce default encoder if stream copy specified + # in Real-time Frames Mode + output_parameters["-vcodec"] = ( + default_codec + if output_vcodec == "copy" and not (self.__video_source) + else output_vcodec + ) + # enforce compatibility with stream copy if output_parameters["-vcodec"] != "copy": # NOTE: these parameters only supported when stream copy not defined output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") # Non-essential `-aspect` parameter is removed from the default pipeline. else: + # log warnings if stream copy specified in Real-time Frames Mode + not (self.__video_source) and logger.error( + "Stream copy is not compatible with Real-time Frames Mode as it requires encoding incoming frames. Discarding the `-vcodec copy` parameter!" + ) # log warnings for these parameters self.__params.pop("-vf", False) and logger.warning( "Filtering and stream copy cannot be used together. Discarding specified `-vf` parameter!" From e7f887a47390037f37101f403aeb01d379b94580 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 11:30:01 +0530 Subject: [PATCH 17/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20NetGear:=20Isolat?= =?UTF-8?q?ed=20contexts=20for=20Secure=20Modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ› Updated logic to use `zmq.Context()` instead of `zmq.Context.instance()` to isolate contexts in order to fix `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')` bug. - ♻️ Refactored code. CI: - πŸ’š Fixed typos in NetGear Tests method names. --- vidgear/gears/netgear.py | 103 ++++++++++++-------- vidgear/tests/network_tests/test_netgear.py | 31 +++--- 2 files changed, 80 insertions(+), 54 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 17b5aeab8..be41e1eb2 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os import time @@ -528,8 +529,10 @@ def __init__( ) ) - # define messaging context instance - self.__msg_context = zmq.Context.instance() + # define ZMQ messaging context instance + self.__msg_context = ( + zmq.Context() if self.__secure_mode > 0 else zmq.Context.instance() + ) # initialize and assign receive mode to global variable self.__receive_mode = receive_mode @@ -680,9 +683,11 @@ def __init__( if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( - "Multi-Server" - if self.__multiserver_mode - else "Multi-Client", + ( + "Multi-Server" + if self.__multiserver_mode + else "Multi-Client" + ), (protocol + "://" + str(address) + ":" + str(port)), pattern, ) @@ -723,12 +728,16 @@ def __init__( "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( self.__jpeg_compression_colorspace, self.__jpeg_compression_quality, - "enabled" - if self.__jpeg_compression_fastdct - else "disabled", - "enabled" - if self.__jpeg_compression_fastupsample - else "disabled", + ( + "enabled" + if self.__jpeg_compression_fastdct + else "disabled" + ), + ( + "enabled" + if self.__jpeg_compression_fastupsample + else "disabled" + ), ) ) if self.__secure_mode: @@ -895,9 +904,11 @@ def __init__( if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( - "Multi-Server" - if self.__multiserver_mode - else "Multi-Client", + ( + "Multi-Server" + if self.__multiserver_mode + else "Multi-Client" + ), (protocol + "://" + str(address) + ":" + str(port)), pattern, ) @@ -931,12 +942,16 @@ def __init__( "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( self.__jpeg_compression_colorspace, self.__jpeg_compression_quality, - "enabled" - if self.__jpeg_compression_fastdct - else "disabled", - "enabled" - if self.__jpeg_compression_fastupsample - else "disabled", + ( + "enabled" + if self.__jpeg_compression_fastdct + else "disabled" + ), + ( + "enabled" + if self.__jpeg_compression_fastupsample + else "disabled" + ), ) ) if self.__secure_mode: @@ -1103,19 +1118,25 @@ def __recv_handler(self): return_dict.update( dict( return_type=(type(self.__return_data).__name__), - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - "colorspace": self.__jpeg_compression_colorspace, - } - if self.__jpeg_compression - else False, - array_dtype=str(self.__return_data.dtype) - if not (self.__jpeg_compression) - else "", - array_shape=self.__return_data.shape - if not (self.__jpeg_compression) - else "", + compression=( + { + "dct": self.__jpeg_compression_fastdct, + "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, + } + if self.__jpeg_compression + else False + ), + array_dtype=( + str(self.__return_data.dtype) + if not (self.__jpeg_compression) + else "" + ), + array_shape=( + self.__return_data.shape + if not (self.__jpeg_compression) + else "" + ), data=None, ) ) @@ -1302,13 +1323,15 @@ def send(self, frame, message=None): msg_dict.update( dict( terminate_flag=exit_flag, - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - "colorspace": self.__jpeg_compression_colorspace, - } - if self.__jpeg_compression - else False, + compression=( + { + "dct": self.__jpeg_compression_fastdct, + "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, + } + if self.__jpeg_compression + else False + ), message=message, pattern=str(self.__pattern), dtype=str(frame.dtype) if not (self.__jpeg_compression) else "", diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 6875dc76a..e302f0a9f 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -256,9 +257,11 @@ def test_compression(options_server): ( 1, 2, - os.path.abspath(os.sep) - if platform.system() == "Linux" - else "unknown://invalid.com/", + ( + os.path.abspath(os.sep) + if platform.system() == "Linux" + else "unknown://invalid.com/" + ), False, ), ] @@ -510,25 +513,25 @@ def test_multiserver_mode(pattern, options): # send frame from Server-1 to client and save it in dict server_1.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame # send frame from Server-2 to client and save it in dict server_2.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame # send frame from Server-3 to client and save it in dict server_3.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame @@ -657,7 +660,7 @@ def test_multiclient_mode(pattern): {"subscriber_timeout": 4}, ], ) -def test_client_reliablity(options): +def test_client_reliability(options): """ Testing validation function of NetGear API """ @@ -713,7 +716,7 @@ def test_client_reliablity(options): }, ], ) -def test_server_reliablity(options): +def test_server_reliability(options): """ Testing validation function of NetGear API """ From 3fbd610d40db6c9337962662cf996a5d7178bf81 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 12:15:50 +0530 Subject: [PATCH 18/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8FNetGear:=20Handle=20gra?= =?UTF-8?q?ceful=20termination=20of=20ZMQ=20AuthenticationThread?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/netgear.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index be41e1eb2..7df45c29c 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -186,6 +186,9 @@ def __init__( # Handle NetGear's internal exclusive modes and params + # define Secure Mode + self.__z_auth = None + # define SSH Tunneling Mode self.__ssh_tunnel_mode = None # handles ssh_tunneling mode state self.__ssh_tunnel_pwd = None @@ -584,19 +587,19 @@ def __init__( # activate secure_mode threaded authenticator if self.__secure_mode > 0: # start an authenticator for this context - z_auth = ThreadAuthenticator(self.__msg_context) - z_auth.start() - z_auth.allow(str(address)) # allow current address + self.__z_auth = ThreadAuthenticator(self.__msg_context) + self.__z_auth.start() + self.__z_auth.allow(str(address)) # allow current address # check if `IronHouse` is activated if self.__secure_mode == 2: # tell authenticator to use the certificate from given valid dir - z_auth.configure_curve( + self.__z_auth.configure_curve( domain="*", location=self.__auth_publickeys_dir ) else: # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - z_auth.configure_curve( + self.__z_auth.configure_curve( domain="*", location=auth.CURVE_ALLOW_ANY ) @@ -675,11 +678,13 @@ def __init__( # otherwise log and raise error logger.exception(str(e)) if self.__secure_mode: + # Handle Secure Mode logger.critical( "Failed to activate Secure Mode: `{}` for this connection!".format( valid_security_mech[self.__secure_mode] ) ) + self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( @@ -798,19 +803,19 @@ def __init__( # activate secure_mode threaded authenticator if self.__secure_mode > 0: # start an authenticator for this context - z_auth = ThreadAuthenticator(self.__msg_context) - z_auth.start() - z_auth.allow(str(address)) # allow current address + self.__z_auth = ThreadAuthenticator(self.__msg_context) + self.__z_auth.start() + self.__z_auth.allow(str(address)) # allow current address # check if `IronHouse` is activated if self.__secure_mode == 2: # tell authenticator to use the certificate from given valid dir - z_auth.configure_curve( + self.__z_auth.configure_curve( domain="*", location=self.__auth_publickeys_dir ) else: # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - z_auth.configure_curve( + self.__z_auth.configure_curve( domain="*", location=auth.CURVE_ALLOW_ANY ) @@ -896,11 +901,13 @@ def __init__( # otherwise log and raise error logger.exception(str(e)) if self.__secure_mode: + # Handle Secure Mode logger.critical( "Failed to activate Secure Mode: `{}` for this connection!".format( valid_security_mech[self.__secure_mode] ) ) + self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( @@ -1521,6 +1528,8 @@ def close(self, kill=False): self.__terminate = True # properly close the socket self.__logging and logger.debug("Terminating. Please wait...") + # Handle Secure Mode Thread + self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() # wait until stream resources are released # (producer thread might be still grabbing frame) if self.__thread is not None: @@ -1542,6 +1551,8 @@ def close(self, kill=False): kill and logger.warning( "`kill` parmeter is only available in the receive mode." ) + # Handle Secure Mode Thread + self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() # check if all attempts of reconnecting failed, then skip to closure if (self.__pattern < 2 and not self.__max_retries) or ( self.__multiclient_mode and not self.__port_buffer From 0a0105cb0074865e9d81ac9cfcc5b17459041e6a Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 12:31:50 +0530 Subject: [PATCH 19/29] =?UTF-8?q?=E2=9A=A1=EF=B8=8FNetGear:=20Handle=20gra?= =?UTF-8?q?ceful=20termination=20of=20ZMQ=20Context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - βͺ️ Reverted Isolated contexts for Secure Modes --- vidgear/gears/netgear.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 7df45c29c..1d83dc3f1 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -533,9 +533,7 @@ def __init__( ) # define ZMQ messaging context instance - self.__msg_context = ( - zmq.Context() if self.__secure_mode > 0 else zmq.Context.instance() - ) + self.__msg_context = zmq.Context.instance() # initialize and assign receive mode to global variable self.__receive_mode = receive_mode @@ -1542,6 +1540,7 @@ def close(self, kill=False): else: self.__thread.join() self.__msg_socket.close(linger=0) + self.__msg_context.term() self.__thread = None self.__logging and logger.debug("Terminated Successfully!") else: @@ -1564,6 +1563,7 @@ def close(self, kill=False): except ZMQError: pass finally: + self.__msg_context.term() # exit return @@ -1594,4 +1594,5 @@ def close(self, kill=False): # properly close the socket self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() + self.__msg_context.term() self.__logging and logger.debug("Terminated Successfully!") From 2873e75012e09de66f5c8ca2e94d27de130cbcad Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 19:40:49 +0530 Subject: [PATCH 20/29] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20warning=20?= =?UTF-8?q?for=20Secure=20Mode=20issues=20with=20PyZMQ=20`versions=20>=202?= =?UTF-8?q?4.0.1`=20on=20Windows.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ§‘β€πŸ’» Added warnings about Secure Mode issues with PyZMQ versions > 24.0.1 on Windows in NetGear API: - πŸ’¬ Secure Mode functionality is broken on Windows with PyZMQ versions > 24.0.1 due to changes in `zmq.auth` module. - πŸ’¬ Users should downgrade to PyZMQ version <= 24.0.1 to use Secure Mode on Windows. - πŸ§‘β€πŸ’» Added warnings about Stream copy (`-vcodec copy`) is not compatible with Real-time Frames Mode as this mode requires re-encoding of incoming frames in StreamGear API. - πŸ’‘ Refined warning message for stream copy compatibility for clarity. CI: - πŸ‘· Added a skip condition in `test_netgear.py` for Windows platform when PyZMQ version > 24.0.1: - Ensured relevant tests are skipped on incompatible PyZMQ versions to prevent test failures. NetGear: - βͺ️ Reverted Handle graceful termination of ZMQ Context. --- docs/gears/netgear/advanced/secure_mode.md | 17 ++++++++++++++++- docs/gears/streamgear/params.md | 2 ++ docs/gears/streamgear/rtfm/usage.md | 7 +++++-- vidgear/gears/netgear.py | 12 ++++++++---- vidgear/gears/streamgear.py | 2 +- vidgear/tests/network_tests/test_netgear.py | 3 +++ 6 files changed, 35 insertions(+), 8 deletions(-) diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 2f1dddaed..6dccfa82d 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -33,7 +33,6 @@ Secure Mode uses a new wire protocol, [**ZMTP 3.0**](http://zmtp.org/) that adds Secure Mode can be easily activated in NetGear API through `secure_mode` attribute of its [`options`](../../params/#options) dictionary parameter, during initialization. Furthermore, for managing this mode, NetGear API provides additional `custom_cert_location` & `overwrite_cert` like attribute too. -   ## Supported ZMQ Security Layers @@ -47,6 +46,11 @@ Secure mode supports the two most powerful ZMQ security layers:   +???+ warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" + + The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. + + !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" !!! danger "Important Information regarding Secure Mode" @@ -125,6 +129,12 @@ For implementing Secure Mode, NetGear API currently provide following exclusive Following is the bare-minimum code you need to get started with Secure Mode in NetGear API: +??? warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" + + The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. + + !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" + #### Server's End Open your favorite terminal and execute the following python code: @@ -222,6 +232,11 @@ client.close() ### Using Secure Mode with Variable Parameters +??? warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" + + The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. + + !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" #### Client's End diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 6f7a9b131..3512b074d 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -381,6 +381,8 @@ stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "ze All encoders and decoders compiled with the FFmpeg in use are supported by the StreamGear API. You can check the compiled encoders by running the following command in your terminal: +!!! warning "Stream copy (`-vcodec copy`) is not compatible with Real-time Frames Mode as this mode requires re-encoding of incoming frames." + !!! info "Similarly, supported audio/video demuxers and filters depend on the FFmpeg binaries in use." ```sh diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 34c153bed..55e84ae5a 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -30,6 +30,7 @@ limitations under the License. - [x] StreamGear API **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➢](../../ffmpeg_install/) for its installation. API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - [x] In this mode, ==API by default generates a primary stream _(at the index `0`)_ of same resolution as the input frames and at default framerate[^1].== - [x] In this mode, API **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. + - [x] In this mode, Stream copy (`-vcodec copy`) is not compatible as this mode requires re-encoding of incoming frames. - [x] Always use `close()` function at the very end of the main code. ???+ danger "DEPRECATION NOTICES for `v0.3.3` and above" @@ -1035,9 +1036,11 @@ To stream live audio, format your audio device name followed by a suitable demux In this example, we will be using `h264_vaapi` as our Hardware Encoder and specifying the device hardware's location and compatible video filters by formatting them as attributes in the `stream_params` dictionary parameter. -!!! warning "This example is just conveying the idea of how to use FFmpeg's hardware encoders with the StreamGear API in Real-time Frames Mode, which MAY OR MAY NOT suit your system. Please use suitable parameters based on your supported system and FFmpeg configurations only." +!!! danger "This example is just conveying the idea of how to use FFmpeg's hardware encoders with the StreamGear API in Real-time Frames Mode, which MAY OR MAY NOT suit your system. Please use suitable parameters based on your supported system and FFmpeg configurations only." -??? danger "Check VAAPI support" +!!! warning "Stream copy (`-vcodec copy`) is not compatible with this Mode as it requires re-encoding of incoming frames." + +??? info "Check VAAPI support" To use `h264_vaapi` encoder, remember to check if its available and your FFmpeg compiled with VAAPI support. You can easily do this by executing following one-liner command in your terminal, and observing if output contains something similar as follows: diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 1d83dc3f1..d0b7ebfda 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1527,7 +1527,10 @@ def close(self, kill=False): # properly close the socket self.__logging and logger.debug("Terminating. Please wait...") # Handle Secure Mode Thread - self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() + if self.__z_auth: + self.__z_auth.stop() + while self.__z_auth.is_alive(): + pass # wait until stream resources are released # (producer thread might be still grabbing frame) if self.__thread is not None: @@ -1551,7 +1554,10 @@ def close(self, kill=False): "`kill` parmeter is only available in the receive mode." ) # Handle Secure Mode Thread - self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() + if self.__z_auth: + self.__z_auth.stop() + while self.__z_auth.is_alive(): + pass # check if all attempts of reconnecting failed, then skip to closure if (self.__pattern < 2 and not self.__max_retries) or ( self.__multiclient_mode and not self.__port_buffer @@ -1563,7 +1569,6 @@ def close(self, kill=False): except ZMQError: pass finally: - self.__msg_context.term() # exit return @@ -1594,5 +1599,4 @@ def close(self, kill=False): # properly close the socket self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() - self.__msg_context.term() self.__logging and logger.debug("Terminated Successfully!") diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index e84c949cb..439bfa0b3 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -482,7 +482,7 @@ def __PreProcess(self, channels=0, rgb=False): else: # log warnings if stream copy specified in Real-time Frames Mode not (self.__video_source) and logger.error( - "Stream copy is not compatible with Real-time Frames Mode as it requires encoding incoming frames. Discarding the `-vcodec copy` parameter!" + "Stream copy is not compatible with Real-time Frames Mode as it requires re-encoding of incoming frames. Discarding the `-vcodec copy` parameter!" ) # log warnings for these parameters self.__params.pop("-vf", False) and logger.warning( diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index e302f0a9f..901de3b6e 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -432,6 +432,9 @@ def test_bidirectional_mode(pattern, target_data, options): client.close(kill=True) +@pytest.mark.skipif( + platform.system() == "Windows", reason="Not supported with pyzmq>24.0.1" +) @pytest.mark.parametrize( "pattern, options", [ From 355596af4efa46f5c2b6a78076a7bc7f6f7ef826 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 19:48:52 +0530 Subject: [PATCH 21/29] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20skip=20condi?= =?UTF-8?q?tion=20on=20wrong=20NetGear=20test.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/network_tests/test_netgear.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 901de3b6e..53527bf58 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -267,6 +267,9 @@ def test_compression(options_server): ] +@pytest.mark.skipif( + platform.system() == "Windows", reason="Not supported with pyzmq>24.0.1" +) @pytest.mark.parametrize( "pattern, security_mech, custom_cert_location, overwrite_cert", test_data_class ) @@ -432,9 +435,6 @@ def test_bidirectional_mode(pattern, target_data, options): client.close(kill=True) -@pytest.mark.skipif( - platform.system() == "Windows", reason="Not supported with pyzmq>24.0.1" -) @pytest.mark.parametrize( "pattern, options", [ From cbff3322e940f9797697dfaa4b159b63af043602 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 21:28:35 +0530 Subject: [PATCH 22/29] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20typos=20in?= =?UTF-8?q?=20NetGear=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/network_tests/test_netgear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 53527bf58..97a329b1f 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -641,9 +641,9 @@ def test_multiclient_mode(pattern): if not (client_1 is None): client_1.close() if not (client_2 is None): - client_1.close() + client_2.close() if not (client_3 is None): - client_1.close() + client_3.close() @pytest.mark.parametrize( From 40881325429a5728e3d13da981b3ea233eec7c48 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 21:46:12 +0530 Subject: [PATCH 23/29] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Enabled=20`kill=3DTr?= =?UTF-8?q?ue`=20in=20`close()`=20in=20NetGear=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/network_tests/test_netgear.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 97a329b1f..94ade8f85 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -552,13 +552,13 @@ def test_multiserver_mode(pattern, options): if not (stream is None): stream.release() if not (server_1 is None): - server_1.close() + server_1.close(kill=True) if not (server_2 is None): - server_2.close() + server_2.close(kill=True) if not (server_3 is None): - server_3.close() + server_3.close(kill=True) if not (client is None): - client.close() + client.close(kill=True) @pytest.mark.parametrize("pattern", [0, 1]) @@ -637,13 +637,13 @@ def test_multiclient_mode(pattern): if not (stream is None): stream.stop() if not (server is None): - server.close() + server.close(kill=True) if not (client_1 is None): - client_1.close() + client_1.close(kill=True) if not (client_2 is None): - client_2.close() + client_2.close(kill=True) if not (client_3 is None): - client_3.close() + client_3.close(kill=True) @pytest.mark.parametrize( @@ -691,7 +691,7 @@ def test_client_reliability(options): finally: # clean resources if not (client is None): - client.close() + client.close(kill=True) @pytest.mark.parametrize( @@ -755,7 +755,7 @@ def test_server_reliability(options): if not (stream is None): stream.release() if not (server is None): - server.close() + server.close(kill=True) @pytest.mark.parametrize( From 15c4914a34f338a773e2f82cbb2fe76b6e0677ab Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 22:14:13 +0530 Subject: [PATCH 24/29] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20NetGear:=20Reverted?= =?UTF-8?q?=20Handle=20graceful=20termination=20of=20ZMQ=20Context.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ‘· CI: Applied short-circuiting to simplify code in NetGear Tests. --- vidgear/gears/netgear.py | 1 - vidgear/tests/network_tests/test_netgear.py | 87 +++++++-------------- 2 files changed, 29 insertions(+), 59 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index d0b7ebfda..ab3670764 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1543,7 +1543,6 @@ def close(self, kill=False): else: self.__thread.join() self.__msg_socket.close(linger=0) - self.__msg_context.term() self.__thread = None self.__logging and logger.debug("Terminated Successfully!") else: diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 94ade8f85..175f11355 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -85,12 +85,9 @@ def test_playback(address, port): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close() - if not (client is None): - client.close() + not (stream is None) and stream.release() + not (server is None) and server.close() + not (client is None) and client.close() @pytest.mark.parametrize("receive_mode", [True, False]) @@ -120,10 +117,8 @@ def test_primary_mode(receive_mode): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (conn is None): - conn.close() + not (stream is None) and stream.stop() + not (conn is None) and conn.close() @pytest.mark.parametrize( @@ -171,12 +166,9 @@ def test_patterns(pattern): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -243,12 +235,9 @@ def test_compression(options_server): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.stop() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) test_data_class = [ @@ -313,12 +302,9 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -427,12 +413,9 @@ def test_bidirectional_mode(pattern, target_data, options): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.stop() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -549,16 +532,11 @@ def test_multiserver_mode(pattern, options): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server_1 is None): - server_1.close(kill=True) - if not (server_2 is None): - server_2.close(kill=True) - if not (server_3 is None): - server_3.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.release() + not (server_1 is None) and server_1.close(kill=True) + not (server_2 is None) and server_2.close(kill=True) + not (server_3 is None) and server_3.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize("pattern", [0, 1]) @@ -636,14 +614,10 @@ def test_multiclient_mode(pattern): # clean resources if not (stream is None): stream.stop() - if not (server is None): - server.close(kill=True) - if not (client_1 is None): - client_1.close(kill=True) - if not (client_2 is None): - client_2.close(kill=True) - if not (client_3 is None): - client_3.close(kill=True) + not (server is None) and server.close(kill=True) + not (client_1 is None) and client_1.close(kill=True) + not (client_2 is None) and client_2.close(kill=True) + not (client_3 is None) and client_3.close(kill=True) @pytest.mark.parametrize( @@ -690,8 +664,7 @@ def test_client_reliability(options): logger.exception(str(e)) finally: # clean resources - if not (client is None): - client.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -752,10 +725,8 @@ def test_server_reliability(options): logger.exception(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close(kill=True) + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) @pytest.mark.parametrize( From 46c9897c1257626e6133ba6e57012e41d074fd61 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 Jun 2024 23:18:51 +0530 Subject: [PATCH 25/29] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20StreamGear:=20Res?= =?UTF-8?q?tricted=20terminating=20the=20FFmpeg=20process=20to=20device=20?= =?UTF-8?q?audio=20streams=20only.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteGear: - βͺ️ Reverted terminating the FFmpeg process in the `close` method, handled by `terminate()` --- vidgear/gears/streamgear.py | 3 ++- vidgear/gears/writegear.py | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 439bfa0b3..e470899c1 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -1218,7 +1218,8 @@ def close(self): # forced termination if specified. if self.__forced_termination: self.__process.terminate() - else: + # handle device audio streams + elif self.__audio and isinstance(self.__audio, list): # send `CTRL_BREAK_EVENT` signal if Windows else `SIGINT` self.__process.send_signal( signal.CTRL_BREAK_EVENT if self.__os_windows else signal.SIGINT diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 8a7f0b721..2a8b7ee1d 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -773,13 +773,7 @@ def close(self): # close `stdout` output self.__process.stdout and self.__process.stdout.close() # forced termination if specified. - if self.__forced_termination: - self.__process.terminate() - else: - # send `CTRL_BREAK_EVENT` signal if Windows else `SIGINT` - self.__process.send_signal( - signal.CTRL_BREAK_EVENT if self.__os_windows else signal.SIGINT - ) + self.__forced_termination and self.__process.terminate() # wait if process is still processing self.__process.wait() else: From 0b0de0b14a7eb8cef72313b8de565772aca57f98 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 Jun 2024 00:48:29 +0530 Subject: [PATCH 26/29] =?UTF-8?q?=E2=98=82=EF=B8=8F=20CI:=20Improved=20cod?= =?UTF-8?q?e=20coverage=20for=20StreamGear=20and=20WriteGear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚑️ StreamGear: Discarded invalid audio stream. --- vidgear/gears/streamgear.py | 3 + vidgear/tests/streamer_tests/test_IO_rtf.py | 2 +- vidgear/tests/streamer_tests/test_IO_ss.py | 1 + .../streamer_tests/test_streamgear_modes.py | 63 ++++++++++++------- .../writer_tests/test_compression_mode.py | 24 +++---- .../writer_tests/test_non_compression_mode.py | 26 ++++---- 6 files changed, 74 insertions(+), 45 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index e470899c1..7fd89a1ae 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -540,9 +540,11 @@ def __PreProcess(self, channels=0, rgb=False): ["-map", "1:a:0"] if self.__format == "dash" else [] ) else: + # discard invalid audio logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) ) + self.__audio = False # validate input video's audio source if available elif self.__video_source: bitrate = validate_audio(self.__ffmpeg, source=self.__video_source) @@ -1208,6 +1210,7 @@ def close(self): """ # log termination self.__logging and logger.debug("Terminating StreamGear Processes.") + # return if no process was initiated at first place if self.__process is None or not (self.__process.poll() is None): return diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index b968ecb77..a9afb08f7 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -81,7 +81,7 @@ def test_method_call_rtf(): @pytest.mark.xfail(raises=ValueError) -@pytest.mark.parametrize("format", ["dash", "hls"]) +@pytest.mark.parametrize("format", ["dash", "hls", "invalid"]) def test_invalid_params_rtf(format): """ Invalid parameter Failure Test - Made to fail by calling invalid parameters diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index e129bcea9..601f9aab6 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -77,6 +77,7 @@ def test_paths_ss(path, format): try: stream_params = { "-video_source": return_testvideo_path(), + "-ffmpeg_download_path": 12345, "-livestream": "invalid", } streamer = StreamGear(output=path, format=format, logging=True, **stream_params) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index 6cf7887c3..b979cc600 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -220,6 +220,10 @@ def test_ss_stream(format): try: stream_params = { "-video_source": return_testvideo_path(), + "-vcodec": "copy", + "-aspect": "4:3", + "-vf": "format=yuv420p", + "-streams": "invalid", "-clear_prev_assets": True, } if format == "hls": @@ -260,11 +264,10 @@ def test_ss_livestream(format): "-clear_prev_assets": "invalid", "-remove_at_exit": 1, } - streamer = StreamGear( + with StreamGear( output=assets_file_path, format=format, logging=True, **stream_params - ) - streamer.transcode_source() - streamer.close() + ) as streamer: + streamer.transcode_source() except Exception as e: pytest.fail(str(e)) @@ -298,18 +301,19 @@ def test_rtf_stream(conversion, format): + os.sep } ) - streamer = StreamGear(output=assets_file_path, format=format, **stream_params) - while True: - frame = stream.read() - # check if frame is None - if frame is None: - break - if conversion == "COLOR_BGR2RGBA": - streamer.stream(frame, rgb_mode=True) - else: - streamer.stream(frame) - stream.stop() - streamer.close() + with StreamGear( + output=assets_file_path, format=format, **stream_params + ) as streamer: + while True: + frame = stream.read() + # check if frame is None + if frame is None: + break + if conversion == "COLOR_BGR2RGBA": + streamer.stream(frame, rgb_mode=True) + else: + streamer.stream(frame) + stream.stop() asset_file = [ os.path.join(assets_file_path, f) for f in os.listdir(assets_file_path) @@ -421,7 +425,13 @@ def test_input_framerate_rtf(format): "-bpp": 0.2000, "-gop": 125, "-vcodec": "libx265", - "-enable_force_termination": "invalid", + "-hls_segment_type": "invalid", + "-hls_init_time": -223.2, + "-hls_flags": 94884, + "-hls_list_size": -4.3, + "-hls_time": -4758.56, + "-remove_at_exit": 4.56, + "-livestream": True, }, "hls", ), @@ -433,6 +443,7 @@ def test_input_framerate_rtf(format): "-s:v:0": "unknown", "-b:v:0": "unknown", "-b:a:0": "unknown", + "-enable_force_termination": "invalid", }, "hls", ), @@ -441,13 +452,21 @@ def test_input_framerate_rtf(format): "-clear_prev_assets": True, "-bpp": 0.2000, "-gop": 125, + "-audio": ["invalid"], "-vcodec": "libx265", + "-window_size": -456.4, + "-extra_window_size": -354.45, + "-remove_at_exit": -34.34, + "-seg_duration": -334.23, + "-livestream": True, }, "dash", ), ( { "-clear_prev_assets": True, + "-seg_duration": -346.67, + "-audio": "inv/\lid", "-bpp": "unknown", "-gop": "unknown", "-s:v:0": "unknown", @@ -487,10 +506,12 @@ def test_params(stream_params, format): streamer.stream(frame) stream.release() streamer.close() - if format == "dash": - assert check_valid_mpd(assets_file_path), "Test Failed!" - else: - assert extract_meta_video(assets_file_path), "Test Failed!" + livestream = stream_params.pop("-livestream", False) + if not (livestream): + if format == "dash": + assert check_valid_mpd(assets_file_path), "Test Failed!" + else: + assert extract_meta_video(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) diff --git a/vidgear/tests/writer_tests/test_compression_mode.py b/vidgear/tests/writer_tests/test_compression_mode.py index 262877ded..108b6d81c 100644 --- a/vidgear/tests/writer_tests/test_compression_mode.py +++ b/vidgear/tests/writer_tests/test_compression_mode.py @@ -17,6 +17,7 @@ limitations under the License. ================================================ """ + # import the necessary packages import os @@ -297,14 +298,13 @@ def test_WriteGear_compression(f_name, c_ffmpeg, output_params, result): """ try: stream = cv2.VideoCapture(return_testvideo_path()) # Open stream - writer = WriteGear(output=f_name, compression_mode=True, **output_params) - while True: - (grabbed, frame) = stream.read() - if not grabbed: - break - writer.write(frame) - stream.release() - writer.close() + with WriteGear(output=f_name, compression_mode=True, **output_params) as writer: + while True: + (grabbed, frame) = stream.read() + if not grabbed: + break + writer.write(frame) + stream.release() remove_file_safe(f_name) except Exception as e: if result: @@ -332,9 +332,11 @@ def test_WriteGear_compression(f_name, c_ffmpeg, output_params, result): ( ["wrong_input", "invalid_flag", "break_things"], True, - {"-ffmpeg_download_path": 53} - if (platform.system() == "Windows") - else {"-disable_force_termination": "OK"}, + ( + {"-ffmpeg_download_path": 53} + if (platform.system() == "Windows") + else {"-disable_force_termination": "OK"} + ), ), ( "wrong_input", diff --git a/vidgear/tests/writer_tests/test_non_compression_mode.py b/vidgear/tests/writer_tests/test_non_compression_mode.py index c9c45bfd2..b9f612610 100644 --- a/vidgear/tests/writer_tests/test_non_compression_mode.py +++ b/vidgear/tests/writer_tests/test_non_compression_mode.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -152,9 +153,11 @@ def test_write(conversion): ), ( "appsrc ! videoconvert ! avenc_mpeg4 bitrate=100000 ! mp4mux ! filesink location=foo.mp4", - {"-gst_pipeline_mode": True} - if platform.system() == "Linux" - else {"-gst_pipeline_mode": "invalid"}, + ( + {"-gst_pipeline_mode": True} + if platform.system() == "Linux" + else {"-gst_pipeline_mode": "invalid"} + ), True if platform.system() == "Linux" else False, ), ] @@ -167,16 +170,15 @@ def test_WriteGear_compression(f_name, output_params, result): """ try: stream = cv2.VideoCapture(return_testvideo_path()) - writer = WriteGear( + with WriteGear( output=f_name, compression_mode=False, logging=True, **output_params - ) - while True: - (grabbed, frame) = stream.read() - if not grabbed: - break - writer.write(frame) - stream.release() - writer.close() + ) as writer: + while True: + (grabbed, frame) = stream.read() + if not grabbed: + break + writer.write(frame) + stream.release() remove_file_safe( "foo.html" if "-gst_pipeline_mode" in output_params From 491e7539206adaf7a908813667216665db8280f7 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 6 Jun 2024 11:30:37 +0530 Subject: [PATCH 27/29] =?UTF-8?q?=F0=9F=90=9B=20NetGear:=20Fixed=20Secure?= =?UTF-8?q?=20Mode=20failing=20to=20work=20on=20conflicting=20ZMQ=20Contex?= =?UTF-8?q?ts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸš‘οΈ Fix an issue where the secure mode failed to work due to conflicting ZMQ contexts, when Server and Client run on same thread. - Moved ZMQ Authenticator and Certificates handling code together, independent of mode activated in API. - Handle Authenticator and Certification errors more gracefully, by handling the "Address in use" error, and disable secure mode if errors occur. - Logged Authenticator start/stop events. - πŸ₯… Handle socket session expiration more gracefully in the `recv_handler` internal method. - Fixed `msg_json` undefined when terminating context in the `recv_handler` internal method forcefully. - 🩹 Ensure proper termination of the ZMQ context and socket when closing the NetGear instance. - ⚑️ Set the `WindowsSelectorEventLoopPolicy` for Python `3.8` and above on Windows to ensure compatibility with ZMQ event loop. - ♻️ Simplify and refactor conditional statements and variable assignments with short-circuiting and formatting. - πŸ”Š Improve logging for various events, such as Authenticator termination, thread termination, and secure mode activation. - 🚩 Added new imports. Docs: - πŸ”₯ Removed warning for Secure Mode issues with PyZMQ `versions > 24.0.1` on Windows. - πŸ“ Added Admonition for warning users about the Client's end must run before the Server's end to establish a secure connection in Secure Mode. --- docs/gears/netgear/advanced/secure_mode.md | 109 ++++--- vidgear/gears/netgear.py | 332 ++++++++++----------- 2 files changed, 206 insertions(+), 235 deletions(-) diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 6dccfa82d..37e2763e8 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -46,16 +46,12 @@ Secure mode supports the two most powerful ZMQ security layers:   -???+ warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" - - The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. - - !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" - !!! danger "Important Information regarding Secure Mode" * The `secure_mode` attribute value at the Client's end **MUST** match exactly the Server's end _(i.e. **IronHouse** security layer is only compatible with **IronHouse**, and **NOT** with **StoneHouse**)_. + * In Secure Mode, The Client's end **MUST** run before the Server's end to establish a secure connection. + * The Public+Secret Keypairs generated at the Server end **MUST** be made available at the Client's end too for successful authentication. If mismatched, connection failure will occur. * By Default, the Public+Secret Keypairs will be generated/stored at the `$HOME/.vidgear/keys` directory of your machine _(e.g. `/home/foo/.vidgear/keys` on Linux)_. But you can also use [`custom_cert_location`](../../params/#options) attribute to set your own Custom-Path for a directory to generate/store these Keypairs. @@ -64,8 +60,11 @@ Secure mode supports the two most powerful ZMQ security layers: * **IronHouse** is the strongest Security Layer available, but it involves certain security checks that lead to **ADDITIONAL LATENCY**. + * Secure Mode only supports `libzmq` library version `>= 4.0`. + +   @@ -129,15 +128,55 @@ For implementing Secure Mode, NetGear API currently provide following exclusive Following is the bare-minimum code you need to get started with Secure Mode in NetGear API: -??? warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" +!!! critical "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" + +#### Client's End + +Open your favorite terminal and execute the following python code: + +!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python linenums="1" hl_lines="6" +# import required libraries +from vidgear.gears import NetGear +import cv2 + +# activate StoneHouse security mechanism +options = {"secure_mode": 1} + +# define NetGear Client with `receive_mode = True` and defined parameter +client = NetGear(pattern=1, receive_mode=True, logging=True, **options) + +# loop over +while True: + + # receive frames from network + frame = client.recv() + + # check for received frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # Show output window + cv2.imshow("Output Frame", frame) - The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break - !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" +# close output window +cv2.destroyAllWindows() + +# safely close client +client.close() +``` #### Server's End -Open your favorite terminal and execute the following python code: +Then open another terminal on the same system and execute the following python code to send the frames to our client: !!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" @@ -181,49 +220,7 @@ stream.stop() server.close() ``` -#### Client's End - -Then open another terminal on the same system and execute the following python code and see the output: - -!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" - -```python linenums="1" hl_lines="6" -# import required libraries -from vidgear.gears import NetGear -import cv2 - -# activate StoneHouse security mechanism -options = {"secure_mode": 1} - -# define NetGear Client with `receive_mode = True` and defined parameter -client = NetGear(pattern=1, receive_mode=True, logging=True, **options) - -# loop over -while True: - # receive frames from network - frame = client.recv() - - # check for received frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # Show output window - cv2.imshow("Output Frame", frame) - - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - -# close output window -cv2.destroyAllWindows() - -# safely close client -client.close() -```   @@ -232,16 +229,12 @@ client.close() ### Using Secure Mode with Variable Parameters -??? warning "Secure Mode Issues with PyZMQ (`version > 24.0.1`) on :fontawesome-brands-windows: Windows" - - The Secure Mode functionality is currently broken with PyZMQ (`version > 24.0.1`) on the Windows platform. This issue is due to recent changes in the [`zmq.auth`](https://pyzmq.readthedocs.io/en/latest/api/zmq.auth.html#module-zmq.auth) module introduced in PyZMQ `version 25.0.0`. Attempting to use Secure Mode with these versions will result in the error: `zmq.error.ZMQError: Address in use (addr='inproc://zeromq.zap.01')`. - - !!! info "To use Secure Mode on Windows, please downgrade to PyZMQ (`version <= 24.0.1`)" - #### Client's End Open a terminal on Client System _(where you want to display the input frames received from the Server)_ and execute the following python code: +!!! critical "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" + !!! info "Note down the local IP-address of this system(required at Server's end) and also replace it in the following code. You can follow [this FAQ](../../../../help/netgear_faqs/#how-to-find-local-ip-address-on-different-os-platforms) for this purpose." !!! danger "You need to paste the Public+Secret Keypairs _(generated at the Server End)_ at the `$HOME/.vidgear/keys` directory of your Client machine for a successful authentication!" diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index ab3670764..a35d5e8a4 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -21,6 +21,8 @@ # import the necessary packages import os import time +import asyncio +import platform import string import secrets import numpy as np @@ -417,56 +419,6 @@ def __init__( else: pass - # Handle Secure mode - if self.__secure_mode: - # activate and log if overwriting is enabled - if overwrite_cert: - if not receive_mode: - self.__logging and logger.warning( - "Overwriting ZMQ Authentication certificates over previous ones!" - ) - else: - overwrite_cert = False - self.__logging and logger.critical( - "Overwriting ZMQ Authentication certificates is disabled for Client's end!" - ) - - # Validate certificate generation paths - try: - # check if custom certificates path is specified - if custom_cert_location: - ( - auth_cert_dir, - self.__auth_secretkeys_dir, - self.__auth_publickeys_dir, - ) = generate_auth_certificates( - custom_cert_location, overwrite=overwrite_cert, logging=logging - ) - else: - # otherwise auto-generate suitable path - ( - auth_cert_dir, - self.__auth_secretkeys_dir, - self.__auth_publickeys_dir, - ) = generate_auth_certificates( - os.path.join(expanduser("~"), ".vidgear"), - overwrite=overwrite_cert, - logging=logging, - ) - # log it - self.__logging and logger.debug( - "`{}` is the default location for storing ZMQ authentication certificates/keys.".format( - auth_cert_dir - ) - ) - except Exception as e: - # catch if any error occurred and disable Secure mode - logger.exception(str(e)) - self.__secure_mode = 0 - logger.critical( - "ZMQ Security Mechanism is disabled for this connection due to errors!" - ) - # Handle ssh tunneling if enabled if not (self.__ssh_tunnel_mode is None): # SSH Tunnel Mode only available for server mode @@ -532,17 +484,93 @@ def __init__( ) ) + # On Windows, NetGear requires the ``WindowsSelectorEventLoop`` but Python 3.8 and above, + # defaults to an ``ProactorEventLoop`` loop that is not compatible with it. Thereby, + # we had to set it manually. + platform.system() == "Windows" and asyncio.set_event_loop_policy( + asyncio.WindowsSelectorEventLoopPolicy() + ) + # define ZMQ messaging context instance self.__msg_context = zmq.Context.instance() # initialize and assign receive mode to global variable self.__receive_mode = receive_mode + # Handle Secure mode + if self.__secure_mode > 0: + # activate and log if overwriting is enabled + if receive_mode: + overwrite_cert = False + overwrite_cert and logger.warning( + "Overwriting ZMQ Authentication certificates is disabled for Client's end!" + ) + else: + overwrite_cert and self.__logging and logger.info( + "Overwriting ZMQ Authentication certificates over previous ones!" + ) + + # Validate certificate generation paths + # Start threaded authenticator for this context + try: + # check if custom certificates path is specified + if custom_cert_location: + ( + auth_cert_dir, + self.__auth_secretkeys_dir, + self.__auth_publickeys_dir, + ) = generate_auth_certificates( + custom_cert_location, overwrite=overwrite_cert, logging=logging + ) + else: + # otherwise auto-generate suitable path + ( + auth_cert_dir, + self.__auth_secretkeys_dir, + self.__auth_publickeys_dir, + ) = generate_auth_certificates( + os.path.join(expanduser("~"), ".vidgear"), + overwrite=overwrite_cert, + logging=logging, + ) + # log it + self.__logging and logger.debug( + "`{}` is the default location for storing ZMQ authentication certificates/keys.".format( + auth_cert_dir + ) + ) + + # start an authenticator for this context + self.__z_auth = ThreadAuthenticator(self.__msg_context) + self.__z_auth.start() + self.__z_auth.allow(str(address)) # allow current address + + # check if `IronHouse` is activated + if self.__secure_mode == 2: + # tell authenticator to use the certificate from given valid dir + self.__z_auth.configure_curve( + domain="*", location=self.__auth_publickeys_dir + ) + else: + # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated + self.__z_auth.configure_curve( + domain="*", location=auth.CURVE_ALLOW_ANY + ) + except zmq.ZMQError as e: + if "Address in use" in str(e): + logger.info("ZMQ Authenticator already running.") + else: + # catch if any error occurred and disable Secure mode + logger.exception(str(e)) + self.__secure_mode = 0 + logger.error( + "ZMQ Security Mechanism is disabled for this connection due to errors!" + ) + # check whether `receive_mode` is enabled if self.__receive_mode: # define connection address - if address is None: - address = "*" # define address + address = "*" if address is None else address # check if multiserver_mode is enabled if self.__multiserver_mode: @@ -578,35 +606,14 @@ def __init__( self.__port = port else: # otherwise assign local port address if None - if port is None: - port = "5555" + port = "5555" if port is None else port try: - # activate secure_mode threaded authenticator - if self.__secure_mode > 0: - # start an authenticator for this context - self.__z_auth = ThreadAuthenticator(self.__msg_context) - self.__z_auth.start() - self.__z_auth.allow(str(address)) # allow current address - - # check if `IronHouse` is activated - if self.__secure_mode == 2: - # tell authenticator to use the certificate from given valid dir - self.__z_auth.configure_curve( - domain="*", location=self.__auth_publickeys_dir - ) - else: - # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - self.__z_auth.configure_curve( - domain="*", location=auth.CURVE_ALLOW_ANY - ) - # define thread-safe messaging socket self.__msg_socket = self.__msg_context.socket(msg_pattern[1]) # define pub-sub flag - if self.__pattern == 2: - self.__msg_socket.set_hwm(1) + self.__pattern == 2 and self.__msg_socket.set_hwm(1) # enable specified secure mode for the socket if self.__secure_mode > 0: @@ -675,14 +682,13 @@ def __init__( except Exception as e: # otherwise log and raise error logger.exception(str(e)) - if self.__secure_mode: - # Handle Secure Mode - logger.critical( - "Failed to activate Secure Mode: `{}` for this connection!".format( - valid_security_mech[self.__secure_mode] - ) + # Handle Secure Mode + self.__secure_mode and logger.critical( + "Failed to activate Secure Mode: `{}` for this connection!".format( + valid_security_mech[self.__secure_mode] ) - self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() + ) + # raise errors for exclusive modes if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( @@ -696,10 +702,9 @@ def __init__( ) ) else: - if self.__bi_mode: - logger.critical( - "Failed to activate Bidirectional Mode for this connection!" - ) + self.__bi_mode and logger.critical( + "Failed to activate Bidirectional Mode for this connection!" + ) raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to bind address: {} and pattern: {}! Kindly recheck all parameters.".format( (protocol + "://" + str(address) + ":" + str(port)), pattern @@ -726,39 +731,31 @@ def __init__( (protocol + "://" + str(address) + ":" + str(port)), pattern ) ) - if self.__jpeg_compression: - logger.debug( - "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( - self.__jpeg_compression_colorspace, - self.__jpeg_compression_quality, - ( - "enabled" - if self.__jpeg_compression_fastdct - else "disabled" - ), - ( - "enabled" - if self.__jpeg_compression_fastupsample - else "disabled" - ), - ) + self.__jpeg_compression and logger.debug( + "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + self.__jpeg_compression_colorspace, + self.__jpeg_compression_quality, + ("enabled" if self.__jpeg_compression_fastdct else "disabled"), + ( + "enabled" + if self.__jpeg_compression_fastupsample + else "disabled" + ), ) - if self.__secure_mode: - logger.debug( - "Successfully enabled ZMQ Security Mechanism: `{}` for this connection.".format( - valid_security_mech[self.__secure_mode] - ) + ) + self.__secure_mode and logger.debug( + "Successfully enabled ZMQ Security Mechanism: `{}` for this connection.".format( + valid_security_mech[self.__secure_mode] ) + ) logger.debug("Multi-threaded Receive Mode is successfully enabled.") logger.debug("Unique System ID is {}.".format(self.__id)) logger.debug("Receive Mode is now activated.") else: # otherwise default to `Send Mode` - # define connection address - if address is None: - address = "localhost" + address = "localhost" if address is None else address # check if multiserver_mode is enabled if self.__multiserver_mode: @@ -794,29 +791,9 @@ def __init__( self.__port_buffer = [] else: # otherwise assign local port address if None - if port is None: - port = "5555" + port = "5555" if port is None else port try: - # activate secure_mode threaded authenticator - if self.__secure_mode > 0: - # start an authenticator for this context - self.__z_auth = ThreadAuthenticator(self.__msg_context) - self.__z_auth.start() - self.__z_auth.allow(str(address)) # allow current address - - # check if `IronHouse` is activated - if self.__secure_mode == 2: - # tell authenticator to use the certificate from given valid dir - self.__z_auth.configure_curve( - domain="*", location=self.__auth_publickeys_dir - ) - else: - # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - self.__z_auth.configure_curve( - domain="*", location=auth.CURVE_ALLOW_ANY - ) - # define thread-safe messaging socket self.__msg_socket = self.__msg_context.socket(msg_pattern[0]) @@ -898,14 +875,13 @@ def __init__( except Exception as e: # otherwise log and raise error logger.exception(str(e)) - if self.__secure_mode: - # Handle Secure Mode - logger.critical( - "Failed to activate Secure Mode: `{}` for this connection!".format( - valid_security_mech[self.__secure_mode] - ) + # Handle Secure Mode + self.__secure_mode and logger.critical( + "Failed to activate Secure Mode: `{}` for this connection!".format( + valid_security_mech[self.__secure_mode] ) - self.__z_auth and self.__z_auth.is_alive() and self.__z_auth.stop() + ) + # raise errors for exclusive modes if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( @@ -919,16 +895,14 @@ def __init__( ) ) else: - if self.__bi_mode: - logger.critical( - "Failed to activate Bidirectional Mode for this connection!" - ) - if self.__ssh_tunnel_mode: - logger.critical( - "Failed to initiate SSH Tunneling Mode for this server with `{}` back-end!".format( - "paramiko" if self.__paramiko_present else "pexpect" - ) + self.__bi_mode and logger.critical( + "Failed to activate Bidirectional Mode for this connection!" + ) + self.__ssh_tunnel_mode and logger.critical( + "Failed to initiate SSH Tunneling Mode for this server with `{}` back-end!".format( + "paramiko" if self.__paramiko_present else "pexpect" ) + ) raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to connect address: {} and pattern: {}! Kindly recheck all parameters.".format( (protocol + "://" + str(address) + ":" + str(port)), pattern @@ -942,29 +916,23 @@ def __init__( (protocol + "://" + str(address) + ":" + str(port)), pattern ) ) - if self.__jpeg_compression: - logger.debug( - "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( - self.__jpeg_compression_colorspace, - self.__jpeg_compression_quality, - ( - "enabled" - if self.__jpeg_compression_fastdct - else "disabled" - ), - ( - "enabled" - if self.__jpeg_compression_fastupsample - else "disabled" - ), - ) + self.__jpeg_compression and logger.debug( + "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + self.__jpeg_compression_colorspace, + self.__jpeg_compression_quality, + ("enabled" if self.__jpeg_compression_fastdct else "disabled"), + ( + "enabled" + if self.__jpeg_compression_fastupsample + else "disabled" + ), ) - if self.__secure_mode: - logger.debug( - "Enabled ZMQ Security Mechanism: `{}` for this connection.".format( - valid_security_mech[self.__secure_mode] - ) + ) + self.__secure_mode and logger.debug( + "Enabled ZMQ Security Mechanism: `{}` for this connection.".format( + valid_security_mech[self.__secure_mode] ) + ) logger.debug("Unique System ID is {}.".format(self.__id)) logger.debug( "Send Mode is successfully activated and ready to send data." @@ -975,8 +943,9 @@ def __recv_handler(self): A threaded receiver handler, that keep iterating data from ZMQ socket to a internally monitored deque, until the thread is terminated, or socket disconnects. """ - # initialize frame variable + # initialize variables frame = None + msg_json = None # keep looping infinitely until the thread is terminated while not self.__terminate: @@ -1034,7 +1003,7 @@ def __recv_handler(self): break # check if terminate_flag` received - if msg_json["terminate_flag"]: + if msg_json and msg_json["terminate_flag"]: # if multiserver_mode is enabled if self.__multiserver_mode: # check and remove from which ports signal is received @@ -1070,11 +1039,17 @@ def __recv_handler(self): ) continue - msg_data = self.__msg_socket.recv( - flags=self.__msg_flag | zmq.DONTWAIT, - copy=self.__msg_copy, - track=self.__msg_track, - ) + try: + msg_data = self.__msg_socket.recv( + flags=self.__msg_flag | zmq.DONTWAIT, + copy=self.__msg_copy, + track=self.__msg_track, + ) + except zmq.ZMQError as e: + logger.critical("Socket Session Expired. Exiting!") + self.__terminate = True + self.__queue.append(None) + break # handle data transfer in synchronous modes. if self.__pattern < 2: @@ -1498,7 +1473,6 @@ def send(self, frame, message=None): # connect normally self.__msg_socket.connect(self.__connection_address) self.__poll.register(self.__msg_socket, zmq.POLLIN) - return None # log confirmation @@ -1528,12 +1502,14 @@ def close(self, kill=False): self.__logging and logger.debug("Terminating. Please wait...") # Handle Secure Mode Thread if self.__z_auth: + self.__logging and logger.debug("Terminating Authenticator Thread.") self.__z_auth.stop() while self.__z_auth.is_alive(): pass # wait until stream resources are released # (producer thread might be still grabbing frame) if self.__thread is not None: + self.__logging and logger.debug("Terminating Main Thread.") # properly handle thread exit if self.__thread.is_alive() and kill: # force close if still alive @@ -1541,8 +1517,9 @@ def close(self, kill=False): self.__msg_context.destroy() self.__thread.join() else: - self.__thread.join() self.__msg_socket.close(linger=0) + self.__msg_context.term() + self.__thread.join() self.__thread = None self.__logging and logger.debug("Terminated Successfully!") else: @@ -1554,6 +1531,7 @@ def close(self, kill=False): ) # Handle Secure Mode Thread if self.__z_auth: + self.__logging and logger.debug("Terminating Authenticator Thread.") self.__z_auth.stop() while self.__z_auth.is_alive(): pass From 1e8d0b5621b51f4c8790dec86a74822717b84cd8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 6 Jun 2024 11:38:25 +0530 Subject: [PATCH 28/29] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20`test=5Fsecu?= =?UTF-8?q?re=5Fmode`=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ‘· Added `"127.0.0.1"` address to allow common endpoint for connection. - πŸ‘· Added `"jpeg_compression":False` to disable frame compression, allowing frame to be same while assertion. - βͺ️ Reverted skip condition for Windows platform when PyZMQ version `> 24.0.1`. - β˜‚οΈ Improved code coverage. --- vidgear/tests/network_tests/test_netgear.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 175f11355..5c73df760 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -241,8 +241,8 @@ def test_compression(options_server): test_data_class = [ - (0, 1, tempfile.gettempdir(), True), - (0, 1, ["invalid"], True), + (1, 1, tempfile.gettempdir(), True), + (1, 2, ["invalid"], True), ( 1, 2, @@ -256,9 +256,6 @@ def test_compression(options_server): ] -@pytest.mark.skipif( - platform.system() == "Windows", reason="Not supported with pyzmq>24.0.1" -) @pytest.mark.parametrize( "pattern, security_mech, custom_cert_location, overwrite_cert", test_data_class ) @@ -271,6 +268,7 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer "secure_mode": security_mech, "custom_cert_location": custom_cert_location, "overwrite_cert": overwrite_cert, + "jpeg_compression": False, } # initialize frame_server = None @@ -281,8 +279,14 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer # open stream stream = cv2.VideoCapture(return_testvideo_path()) # define params - server = NetGear(pattern=pattern, logging=True, **options) - client = NetGear(pattern=pattern, receive_mode=True, logging=True, **options) + server = NetGear(address="127.0.0.1", pattern=pattern, logging=True, **options) + client = NetGear( + address="127.0.0.1", + pattern=pattern, + receive_mode=True, + logging=True, + **options + ) # select random input frame from stream i = 0 while i < random.randint(10, 100): From c1014022531c34ed6b24c14826921d74e2688fbd Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 6 Jun 2024 12:02:37 +0530 Subject: [PATCH 29/29] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20NetGear:=20Reverted?= =?UTF-8?q?=20Handle=20graceful=20termination=20of=20ZMQ=20Context.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ“ Docs: Fixed typos in admonitions. --- docs/gears/netgear/advanced/secure_mode.md | 4 ++-- vidgear/gears/netgear.py | 1 - vidgear/tests/network_tests/test_netgear.py | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 37e2763e8..51615363b 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -128,7 +128,7 @@ For implementing Secure Mode, NetGear API currently provide following exclusive Following is the bare-minimum code you need to get started with Secure Mode in NetGear API: -!!! critical "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" +!!! alert "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" #### Client's End @@ -233,7 +233,7 @@ server.close() Open a terminal on Client System _(where you want to display the input frames received from the Server)_ and execute the following python code: -!!! critical "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" +!!! alert "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" !!! info "Note down the local IP-address of this system(required at Server's end) and also replace it in the following code. You can follow [this FAQ](../../../../help/netgear_faqs/#how-to-find-local-ip-address-on-different-os-platforms) for this purpose." diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index a35d5e8a4..7bae7d644 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1518,7 +1518,6 @@ def close(self, kill=False): self.__thread.join() else: self.__msg_socket.close(linger=0) - self.__msg_context.term() self.__thread.join() self.__thread = None self.__logging and logger.debug("Terminated Successfully!") diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 5c73df760..462056577 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -616,8 +616,7 @@ def test_multiclient_mode(pattern): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() + not (stream is None) and stream.stop() not (server is None) and server.close(kill=True) not (client_1 is None) and client_1.close(kill=True) not (client_2 is None) and client_2.close(kill=True)