From 4497fbb2a47231a80620cf65d1e4f25053d596b7 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 10 Sep 2023 20:58:44 +0530 Subject: [PATCH 01/81] =?UTF-8?q?=F0=9F=94=96=20Bumped=20version=20to=20`0?= =?UTF-8?q?.3.3`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vidgear/version.py b/vidgear/version.py index f9aa3e110..e19434e2e 100644 --- a/vidgear/version.py +++ b/vidgear/version.py @@ -1 +1 @@ -__version__ = "0.3.2" +__version__ = "0.3.3" From a4efa4b343a79718275b889d459f0ce210d5d561 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 28 Sep 2023 11:47:31 +0530 Subject: [PATCH 02/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20instructio?= =?UTF-8?q?ns=20for=20compiling=20OpenCV=20with=20GSTREAMER=20backend=20us?= =?UTF-8?q?ing=20pip.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/help/camgear_faqs.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index 208094513..d7f4fdda7 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -53,15 +53,17 @@ limitations under the License. === ":material-linux: Linux" - - [x] **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** + - [x] **Compile manually:** **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** + + - [x] **Compile using Pip:** **Follow [this GitHub issue ➶](https://github.com/opencv/opencv-python/issues/530)** === ":fontawesome-brands-windows: Windows" - - [x] **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** + - [x] **Compile manually:** **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** === ":material-apple: MacOS" - - [x] **Follow [this tutorial ➶](https://medium.com/testinium-tech/how-to-install-opencv-with-java-and-gstreamer-support-on-macos-c3c7b28d2864)** + - [x] **Compile manually:** **Follow [this tutorial ➶](https://medium.com/testinium-tech/how-to-install-opencv-with-java-and-gstreamer-support-on-macos-c3c7b28d2864)**   From 83aee850ecaa4388713c4b31a21a66d78b214aa9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 10 Apr 2024 00:04:38 +0530 Subject: [PATCH 03/81] =?UTF-8?q?=F0=9F=8E=89=20feat(PiGear):=20Add=20offi?= =?UTF-8?q?cial=20support=20for=20new=20Picamera2=20API=20(Fixes=20#342)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ This update brings official support for the new Picamera2 API, unlocking powerful features for Raspberry Pi Camera Modules and limited USB camera support. Key improvements: - 🚸 Seamless Python wrapper: A robust wrapper around Picamera2 API library simplifies integration with minimal code changes for existing PiGear users. - ⚡️ Enhanced camera control: Leverages libcamera API under the hood for Raspberry Pi Camera Modules. - ⚡️ Existing compatibility: Maintains compatibility with PiGear's existing super-charged multi-threaded and colorspace manipulation framework. - ⚡️ Resource management: Ensures proper resource release during PiGear termination. - ✨ USB camera support (limited): Provides basic functionality for USB webcams. PiGear could accurately differentiates between USB and Raspberry Pi cameras using metadata. - 🧑‍💻 Backward compatibility: Seamlessly switches to the legacy PiCamera library if Picamera2 is unavailable. - 🧑‍💻 Standalone functionalities: Separates functionalities for both legacy and newer APIs for clarity. - ⚡️ Automatic configuration management: Handles common configuration parameters and non-standard settings for various camera types. - New internal parameters: - 🚩 auto_align_output_config: Optimizes output frame configuration for the camera sensor. - 🚩 enable_verbose_logs: Enables more detailed logging through Picamera2. - Unified `format` parameter: - 🥅 Defaults to RGB888 (24-bit BGR) for Raspberry Pi cameras. - 🦺 Validates formats for USB cameras before handling. - 🥅 Requires explicit `colorspace` definition if `format` is not MPEG (USB cameras) or for specific formats in Raspberry Pi cameras. - Raspberry Pi camera exclusive parameters: - 🚩 Currently Supports `"controls," "queue," "transform," "bit_depth," "buffer_count," "sensor," and "stride"` with type checks. - ⚡️ Framerate handling: Tailored framerate handling based on camera type. - 🦺 Logging optimization: Added a warning advising users to disable common libcamera messages when logging is disabled. Additional Changes: - 💥 Dropped legacy `picamera` dependency in `Setup.py`. - 💡 Improved code efficiency with short-circuiting and formatting. - 🔊 Updated logging practices. CI: - 🏗️ Temporarily removed PiGear API from code coverage due to hardware limitations. --- codecov.yml | 1 + setup.py | 3 +- vidgear/gears/pigear.py | 310 +++++++++++++++++++++++++++++++++------- 3 files changed, 261 insertions(+), 53 deletions(-) diff --git a/codecov.yml b/codecov.yml index 0be02ec86..212a70454 100644 --- a/codecov.yml +++ b/codecov.yml @@ -32,6 +32,7 @@ ignore: - "scripts" - "vidgear/gears/__init__.py" #trivial - "vidgear/gears/asyncio/__main__.py" #trivial + - "vidgear/gears/pigear.py" #HW limits - "setup.py" - "**/*.md" - "**/*.html" diff --git a/setup.py b/setup.py index 465d60516..d1c35c53f 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import json import platform @@ -115,7 +116,6 @@ def latest_version(package_name): "mss{}".format(latest_version("mss")), "pyscreenshot{}".format(latest_version("pyscreenshot")), ] - + (["picamera"] if ("arm" in platform.uname()[4][:3]) else []) + ( ["dxcam{}".format(latest_version("dxcam"))] if (platform.system() == "Windows") # windows is only supported @@ -136,7 +136,6 @@ def latest_version(package_name): "aiortc{}".format(latest_version("aiortc")), "uvicorn{}".format(latest_version("uvicorn")), ] - + (["picamera"] if ("arm" in platform.uname()[4][:3]) else []) + ( ["dxcam{}".format(latest_version("dxcam"))] if (platform.system() == "Windows") # windows is only supported diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index 484a2adf9..5dc2fe238 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -17,9 +17,11 @@ limitations under the License. =============================================== """ + # import the necessary packages import cv2 import sys +import os import time import logging as log from threading import Thread @@ -33,11 +35,18 @@ ) # safe import critical Class modules +### LEGACY picamera API ### picamera = import_dependency_safe("picamera", error="silent") if not (picamera is None): from picamera import PiCamera from picamera.array import PiRGBArray +### NEW PiCamera2 API ### +picamera2 = import_dependency_safe("picamera2", error="silent") +if not (picamera2 is None): + from picamera2 import Picamera2 + from libcamera import Transform + # define logger logger = log.getLogger("PiGear") logger.propagate = False @@ -83,14 +92,20 @@ def __init__( logcurr_vidgear_ver(logging=logging) # raise error(s) for critical Class imports - import_dependency_safe( - "picamera" if picamera is None else "", - ) + if picamera2: + # log if picamera2 + logger.info("PiCamera2 API is currently being accessed.") + elif picamera: + # switch to picamera otherwise + logger.critical( + "PiCamera2 library not installed on this system. Defaulting to legacy picamera API." + ) + else: + # raise error if none + import_dependency_safe("picamera") # enable logging if specified - self.__logging = False - if logging: - self.__logging = logging + self.__logging = bool(logging) assert ( isinstance(framerate, (int, float)) and framerate > 5.0 @@ -108,17 +123,49 @@ def __init__( "Input camera_num value `{}` is invalid, Defaulting to index 0!" ) - # initialize the picamera stream at given index - self.__camera = PiCamera(camera_num=camera_num) - self.__camera.resolution = tuple(resolution) - self.__camera.framerate = framerate - self.__logging and logger.debug( - "Activating Pi camera at index: {} with resolution: {} & framerate: {}".format( - camera_num, resolution, framerate + if picamera2: + # handle logging + not (self.__logging) and not os.getenv( + "LIBCAMERA_LOG_LEVELS", False + ) and logger.info( + "Kindly set `LIBCAMERA_LOG_LEVELS=2` environment variable to disable common libcamera API messages." + ) + # collect metadata + cameras_metadata = Picamera2.global_camera_info() + # initialize the picamera stream at given index + self.__camera = Picamera2(camera_num=camera_num) + # extract metadata for current camera + camera_metadata = [x for x in cameras_metadata if x["Num"] == camera_num][0] + # check connected camera is USB or I2C + self.__camera_is_usb = True if "usb" in camera_metadata["Id"] else False + # handle framerate control + if not self.__camera_is_usb: + self.__camera.set_controls({"FrameRate": framerate}) + else: + logger.warning( + "USB camera detected. Setting input framerate is NOT supported with Picamera2 API!" + ) + # log + self.__logging and logger.debug( + "Activating Picamera2 API for `{}` camera at index: {} with resolution: {} & framerate: {}".format( + camera_metadata["Model"], + camera_num, + resolution if not self.__camera_is_usb else "default", + framerate, + ) + ) + else: + # initialize the picamera stream at given index + self.__camera = PiCamera(camera_num=camera_num) + self.__camera.resolution = tuple(resolution) + self.__camera.framerate = framerate + self.__logging and logger.debug( + "Activating Picamera API at index: {} with resolution: {} & framerate: {}".format( + camera_num, resolution, framerate + ) ) - ) - # initialize framerate variable + # initialize framerate (Read-only) variable self.framerate = framerate # initializing colorspace variable @@ -142,12 +189,158 @@ def __init__( self.__failure_timeout = 2.0 try: - # apply attributes to source if specified - for key, value in options.items(): + if picamera2: + # define common supported picamera2 config parameters + valid_config_options = [ + "auto_align_output_config", # internal + "enable_verbose_logs", # internal + "format", + ] + + # define non-USB supported picamera2 config parameters + non_usb_options = [ + "controls", + "queue", + "transform", + "bit_depth", + "buffer_count", + "sensor", + "stride", + ] # Less are supported (will be changed in future) + + # filter parameter supported with non-USB cameras only + if self.__camera_is_usb: + unsupported_config_keys = set(list(options.keys())).intersection( + set(non_usb_options) + ) + unsupported_config_keys and logger.warning( + "Setting parameters: `{}` for USB camera is NOT supported with Picamera2 API!".format( + "`, `".join(unsupported_config_keys) + ) + ) + else: + valid_config_options += non_usb_options + + # log all invalid keys + invalid_config_keys = set(list(options.keys())) - set( + valid_config_options + ) + invalid_config_keys and logger.warning( + "Discarding invalid options NOT supported by Picamera2 API: `{}`".format( + "`, `".join(invalid_config_keys) + ) + ) + # delete all unsupported options + options = { + x: y for x, y in options.items() if x in valid_config_options + } + + # setting size, already defined + options.update({"size": tuple(resolution)}) + + # set 24-bit, BGR format by default + if not "format" in options: + # auto defaults for USB cameras + not self.__camera_is_usb and options.update({"format": "RGB888"}) + elif self.__camera_is_usb: + # check the supported formats, if USB camera + avail_formats = [ + mode["format"] for mode in self.__camera.sensor_modes + ] + # handle unsupported formats + if not options["format"] in avail_formats: + logger.warning( + "Discarding `format={}`. `{}` are the only available formats for USB camera in use!".format( + options["format"], "`, `".join(avail_formats) + ) + ) + del options["format"] + else: + # `colorspace` parameter must define with `format` optional parameter + # unless format is MPEG (tested) + assert ( + not (colorspace is None) or options["format"] == "MPEG" + ), "[PiGear:ERROR] :: `colorspace` parameter must defined along with `format={}` in Picamera2 API!".format( + options["format"] + ) + else: + # `colorspace` parameter must define with `format` optional parameter + # unless format is either BGR or BGRA + assert not (colorspace is None) or options["format"] in [ + "RGB888", + "XRGB888", + ], "[PiGear:ERROR] :: `colorspace` parameter must defined along with `format={}` in Picamera2 API!".format( + options["format"] + ) + + # enable verbose logging mode (handled by Picamera2 API) + verbose = options.pop("enable_verbose_logs", False) + if self.__logging and isinstance(verbose, bool) and verbose: + self.__camera.set_logging(Picamera2.DEBUG) + else: + # setup logging + self.__camera.set_logging(Picamera2.WARNING) + + # handle transformations, if specified + transform = options.pop("transform", Transform()) + if not isinstance(transform, Transform): + logger.warning("`transform` value is of invalid type, Discarding!") + transform = Transform() + + # handle sensor configurations, if specified + sensor = options.pop("sensor", {}) + if isinstance(sensor, dict): + # remove size if output size is defined + if "output_size" in sensor: + del options["size"] + else: + logger.warning("`sensor` value is of invalid type, Discarding!") + sensor = {} + + # handle controls, if specified + controls = options.pop("controls", {}) + if isinstance(controls, dict): + # remove any fps controls, done already + controls.pop("FrameDuration", None) + controls.pop("FrameDurationLimits", None) + else: + logger.warning("`controls` value is of invalid type, Discarding!") + controls = {} + + # check if auto-align camera configuration is specified + auto_align_output_config = options.pop( + "auto_align_output_config", False + ) + + # create default configuration for camera + config = self.__camera.create_preview_configuration( + main=options, transform=transform, sensor=sensor, controls=controls + ) + + # auto-align camera configuration, if specified + if ( + isinstance(auto_align_output_config, bool) + and auto_align_output_config + ): + self.__logging and logger.debug( + "Re-aligning Output frames to optimal configuration supported by current Camera Sensor." + ) + self.__camera.align_configuration(config) + + # configure camera + self.__camera.configure(config) self.__logging and logger.debug( - "Setting Parameter: {} = '{}'".format(key, value) + "Setting Picamera2 API Parameters: '{}'".format( + self.__camera.camera_configuration()["main"] + ) ) - setattr(self.__camera, key, value) + else: + # apply attributes to source if specified + for key, value in options.items(): + self.__logging and logger.debug( + "Setting {} API Parameter for Picamera: '{}'".format(key, value) + ) + setattr(self.__camera, key, value) except Exception as e: # Catch if any error occurred logger.exception(str(e)) @@ -163,18 +356,26 @@ def __init__( ) # enable rgb capture array thread and capture stream - self.__rawCapture = PiRGBArray(self.__camera, size=resolution) - self.stream = self.__camera.capture_continuous( - self.__rawCapture, format="bgr", use_video_port=True - ) + if not picamera2: + self.__rawCapture = PiRGBArray(self.__camera, size=resolution) + self.stream = self.__camera.capture_continuous( + self.__rawCapture, format="bgr", use_video_port=True + ) - # frame variable initialization - self.frame = None + # initialize frame variable + # with captured frame try: - stream = next(self.stream) - self.frame = stream.array - self.__rawCapture.seek(0) - self.__rawCapture.truncate() + if picamera2: + # start camera thread + self.__camera.start() + # capture frame array + self.frame = self.__camera.capture_array("main") + else: + # capture frame array from stream + stream = next(self.stream) + self.frame = stream.array + self.__rawCapture.seek(0) + self.__rawCapture.truncate() # render colorspace if defined if not (self.frame is None) and not (self.color_space is None): self.frame = cv2.cvtColor(self.frame, self.color_space) @@ -205,7 +406,6 @@ def start(self): **Returns:** A reference to the CamGear class object. """ - # Start frame producer thread self.__thread = Thread(target=self.__update, name="PiGear", args=()) self.__thread.daemon = True @@ -222,7 +422,6 @@ def __timeit(self): """ Threaded Internal Timer that keep checks on thread execution timing """ - # assign current time self.__t_elasped = time.time() @@ -243,23 +442,27 @@ def __update(self): """ # keep looping infinitely until the thread is terminated while not (self.__terminate): - - try: - # Try to iterate next frame from generator - stream = next(self.stream) - except Exception: - # catch and save any exceptions - self.__exceptions = sys.exc_info() - break # exit + if not picamera2: + try: + # Try to iterate next frame from generator + stream = next(self.stream) + except Exception: + # catch and save any exceptions + self.__exceptions = sys.exc_info() + break # exit # __update timer self.__t_elasped = time.time() - # grab the frame from the stream and clear the stream in - # preparation for the next frame - frame = stream.array - self.__rawCapture.seek(0) - self.__rawCapture.truncate() + # grab the frame from the stream + if picamera2: + frame = self.__camera.capture_array("main") + else: + frame = stream.array + # clear the stream in preparation + # for the next frame + self.__rawCapture.seek(0) + self.__rawCapture.truncate() # apply colorspace if specified if not (self.color_space is None): @@ -293,9 +496,12 @@ def __update(self): if not (self.__terminate): self.__terminate = True - # release picamera resources - self.__rawCapture.close() - self.__camera.close() + # release resources + if picamera2: + self.__camera.stop() + else: + self.__rawCapture.close() + self.__camera.close() def read(self): """ @@ -323,7 +529,6 @@ def read(self): ) ) raise RuntimeError(error_msg).with_traceback(self.__exceptions[2]) - # return the frame return self.frame @@ -345,9 +550,12 @@ def stop(self): if not (self.__thread is None): # check if hardware failure occured if not (self.__exceptions is None) and isinstance(self.__exceptions, bool): - # force release picamera resources - self.__rawCapture.close() - self.__camera.close() + if picamera2: + self.__camera.stop() + else: + # force release picamera resources + self.__rawCapture.close() + self.__camera.close() # properly handle thread exit self.__thread.join() # wait if still process is still processing some information # remove any threads From c1076748ebb1a58ab4d97ab23383e7e256f0ab3b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 10 Apr 2024 00:28:09 +0530 Subject: [PATCH 04/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20PiGear:=20Lowered=20?= =?UTF-8?q?framerate=20minimum=20value=20to=20`0.0`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💡 Updated code comments. --- vidgear/gears/pigear.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index 5dc2fe238..e639b6590 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -108,7 +108,7 @@ def __init__( self.__logging = bool(logging) assert ( - isinstance(framerate, (int, float)) and framerate > 5.0 + isinstance(framerate, (int, float)) and framerate > 0.0 ), "[PiGear:ERROR] :: Input framerate value `{}` is a Invalid! Kindly read docs.".format( framerate ) @@ -536,6 +536,7 @@ def stop(self): """ Safely terminates the thread, and release the VideoStream resources. """ + # log termination self.__logging and logger.debug("Terminating PiGear Processes.") # make sure that the threads should be terminated @@ -551,6 +552,7 @@ def stop(self): # check if hardware failure occured if not (self.__exceptions is None) and isinstance(self.__exceptions, bool): if picamera2: + # release picamera2 resources self.__camera.stop() else: # force release picamera resources From 8e3d092881264fd94a7069f51e30fcd4dc9e121d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 11 Apr 2024 10:11:56 +0530 Subject: [PATCH 05/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20Zenodo?= =?UTF-8?q?=20badge=20and=20the=20BibTeX=20entry.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++-------- docs/index.md | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c5b8e8b71..6f7a4649a 100644 --- a/README.md +++ b/README.md @@ -650,7 +650,7 @@ It is something I am doing with my own free time. But so much more needs to be d Here is a Bibtex entry you can use to cite this project in a publication: -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8174694.svg)](https://doi.org/10.5281/zenodo.8174694) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8332548.svg)](https://doi.org/10.5281/zenodo.8332548) ```BibTeX @software{vidgear, @@ -660,18 +660,19 @@ Here is a Bibtex entry you can use to cite this project in a publication: Christian Hollinger and Ian Max Andolina and Vincent Boivin and - enarche-ahn and + Kyle Ahn and freol35241 and Benjamin Lowe and Mickaël Schoentgen and - Renaud Bouckenooghe}, - title = {abhiTronix/vidgear: VidGear v0.3.1}, - month = jul, + Renaud Bouckenooghe and + Ibtsam Ahmad}, + title = {abhiTronix/vidgear: VidGear Stable v0.3.2}, + month = sep, year = 2023, publisher = {Zenodo}, - version = {vidgear-0.3.1}, - doi = {10.5281/zenodo.8174694}, - url = {https://doi.org/10.5281/zenodo.8174694} + version = {vidgear-0.3.2}, + doi = {10.5281/zenodo.8332548}, + url = {https://doi.org/10.5281/zenodo.8332548} } ``` diff --git a/docs/index.md b/docs/index.md index a52c5346c..ecb7acd68 100644 --- a/docs/index.md +++ b/docs/index.md @@ -127,7 +127,7 @@ It is something I am doing with my own free time. But so much more needs to be d Here is a Bibtex entry you can use to cite this project in a publication: -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8174694.svg)](https://doi.org/10.5281/zenodo.8174694) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.8332548.svg)](https://doi.org/10.5281/zenodo.8332548) ```BibTeX @software{vidgear, @@ -137,18 +137,19 @@ Here is a Bibtex entry you can use to cite this project in a publication: Christian Hollinger and Ian Max Andolina and Vincent Boivin and - enarche-ahn and + Kyle Ahn and freol35241 and Benjamin Lowe and Mickaël Schoentgen and - Renaud Bouckenooghe}, - title = {abhiTronix/vidgear: VidGear v0.3.1}, - month = jul, + Renaud Bouckenooghe and + Ibtsam Ahmad}, + title = {abhiTronix/vidgear: VidGear Stable v0.3.2}, + month = sep, year = 2023, publisher = {Zenodo}, - version = {vidgear-0.3.1}, - doi = {10.5281/zenodo.8174694}, - url = {https://doi.org/10.5281/zenodo.8174694} + version = {vidgear-0.3.2}, + doi = {10.5281/zenodo.8332548}, + url = {https://doi.org/10.5281/zenodo.8332548} } ``` From 3a0d6a07312ef3b16ed33b6b85042dd18d8ac5b1 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 14 Apr 2024 18:12:24 +0530 Subject: [PATCH 06/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20instructio?= =?UTF-8?q?ns=20to=20install=20Picamera2=20library=20as=20dependency.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🧑‍💻 Replaced legacy Picamera library installation instructions with Picamera2 python library. - 📝 Added two methods of Picamera2 installation: using `apt`(recommended) and using `pip`. - 🚸 Added information admonition about Picamera2 comes pre-installed on images downloaded from Raspberry Pi. - 🚸 Added warning admonition Picamera2 is only supported on Raspberry Pi OS Bullseye (or later) images. - 🚸 Moved legacy Picamera library optional installation instructions to a Admonition. - 🔇 Added `unrecognized_links: ignore` to mkdocs.yml for validations to minimize logging. - 📝 Updated vidgear library binaries version. --- docs/installation/pip_install.md | 82 +++++++++++++++++++++++++---- docs/installation/source_install.md | 79 ++++++++++++++++++++++++++- mkdocs.yml | 4 ++ 3 files changed, 154 insertions(+), 11 deletions(-) diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index ba51b9181..a1fdb848a 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -126,16 +126,80 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y * **For StreamGear API**: Follow this dedicated [**FFmpeg Installation doc**](../../gears/streamgear/ffmpeg_install/) for its installation. -* #### Picamera +* #### Picamera2 - Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules with its [**PiGear**](../../gears/pigear/overview/) API. You can easily install it via pip: + ??? tip "Using Legacy `picamera` library with PiGear (`v0.3.3` and above)" + PiGear API _(version `0.3.3` onwards)_ prioritizes the newer Picamera2 library under the hood for Raspberry Pi :fontawesome-brands-raspberry-pi: camera modules. However, if your operating system doesn't support Picamera2, you can still use the legacy [`picamera`](https://picamera.readthedocs.io/en/release-1.13/) library. Here's how to easily install it using pip: - !!! warning "Make sure to [**enable Raspberry Pi hardware-specific settings**](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior to using this library, otherwise it won't work." + !!! warning "Make sure to [**enable Raspberry Pi hardware-specific settings**](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior to using this library, otherwise it won't work." - ```sh - pip install picamera - ``` + ```sh + pip install picamera + ``` + + ??? note "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." + - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. + - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: + + ```sh + sudo apt install -y python3-pyqt5 python3-opengl + ``` + + Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: + + ??? warning "Picamera2 is only supported on Raspberry Pi OS Bullseye (or later) images, both 32 and 64-bit." + + Picamera2 is **NOT** supported on: + + - [ ] Images based on Buster or earlier releases. + - [ ] Raspberry Pi OS Legacy images. + - [ ] Bullseye (or later) images where the legacy camera stack has been re-enabled. + + === "Installation using `apt` (Recommended)" + + If Picamera2 is not already installed, then your image is presumably older and you should start with system upgrade: + ```sh + sudo apt update && upgrade + ``` + + !!! note "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." + + Thereafter, you can install Picamera2 with all the GUI (Qt and OpenGL) dependencies using: + + ```sh + sudo apt install -y python3-picamera2 + ``` + + Or, If you **DON'T** want the GUI dependencies, use: + + ```sh + sudo apt install -y python3-picamera2 --no-install-recommends + ``` + + === "Installation using `pip`" + + !!! danger "This is **NOT** the recommended way to install Picamera2." + + However, if you wish to install Picamera2 with all the GUI (Qt and OpenGL) dependencies with pip, use: + + ```sh + sudo apt install -y python3-libcamera python3-kms++ + sudo apt install -y python3-pyqt5 python3-prctl + sudo apt install -y libatlas-base-dev ffmpeg python3-pip + pip3 install numpy --upgrade + pip3 install picamera2[gui] + ``` + + Or, If you **DON'T** want the GUI dependencies, use: + + ```sh + sudo apt install -y python3-libcamera python3-kms++ + sudo apt install -y python3-prctl libatlas-base-dev + sudo apt install -y ffmpeg libopenjp2-7 python3-pip + pip3 install numpy --upgrade + pip3 install picamera2 + ``` * #### Uvloop @@ -244,7 +308,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y | APIs | Dependencies | |:---:|:---| | CamGear | `yt_dlp` | - | PiGear | `picamera` | + | PiGear | `picamera`, `picamera2` _(see [this](#picamera2) for its installation)_ | | VideoGear | *Based on CamGear or PiGear backend in use* | | ScreenGear | `dxcam`, `mss`, `pyscreenshot`, `Pillow` | | WriteGear | **FFmpeg:** See [this doc ➶](../../gears/writegear/compression/advanced/ffmpeg_install/#ffmpeg-installation-instructions) | @@ -329,10 +393,10 @@ pip install git+git://github.com/abhiTronix/vidgear@master#egg=vidgear[asyncio] ```sh # Install latest stable release with all Core dependencies -pip install vidgear-0.3.2-py3-none-any.whl[core] +pip install vidgear-0.3.3-py3-none-any.whl[core] # Or Install latest stable release with all Core & Asyncio dependencies -pip install vidgear-0.3.2-py3-none-any.whl[asyncio] +pip install vidgear-0.3.3-py3-none-any.whl[asyncio] ```   diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index 88d49ef82..7a6a9cc24 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -26,7 +26,7 @@ limitations under the License. ## Prerequisites -When installing VidGear from source, FFmpeg is the only API specific prerequisites you need to install manually: +When installing VidGear from source, there are some API specific prerequisites you need to install manually: !!! question "What about rest of the prerequisites?" @@ -110,6 +110,81 @@ When installing VidGear from source, FFmpeg is the only API specific prerequisit * **For StreamGear API**: Follow this dedicated [**FFmpeg Installation doc**](../../gears/streamgear/ffmpeg_install/) for its installation. +* #### Picamera2 + + ??? tip "Using Legacy `picamera` library with PiGear (`v0.3.3` and above)" + + PiGear API _(version `0.3.3` onwards)_ prioritizes the newer Picamera2 library under the hood for Raspberry Pi :fontawesome-brands-raspberry-pi: camera modules. However, if your operating system doesn't support Picamera2, you can still use the legacy [`picamera`](https://picamera.readthedocs.io/en/release-1.13/) library. Here's how to easily install it using pip: + + !!! warning "Make sure to [**enable Raspberry Pi hardware-specific settings**](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior to using this library, otherwise it won't work." + + ```sh + pip install picamera + ``` + + ??? note "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." + - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. + - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: + + ```sh + sudo apt install -y python3-pyqt5 python3-opengl + ``` + + Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: + + ??? warning "Picamera2 is only supported on Raspberry Pi OS Bullseye (or later) images, both 32 and 64-bit." + + Picamera2 is **NOT** supported on: + + - [ ] Images based on Buster or earlier releases. + - [ ] Raspberry Pi OS Legacy images. + - [ ] Bullseye (or later) images where the legacy camera stack has been re-enabled. + + === "Installation using `apt` (Recommended)" + + If Picamera2 is not already installed, then your image is presumably older and you should start with system upgrade: + ```sh + sudo apt update && upgrade + ``` + + !!! note "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." + + Thereafter, you can install Picamera2 with all the GUI (Qt and OpenGL) dependencies using: + + ```sh + sudo apt install -y python3-picamera2 + ``` + + Or, If you **DON'T** want the GUI dependencies, use: + + ```sh + sudo apt install -y python3-picamera2 --no-install-recommends + ``` + + === "Installation using `pip`" + + !!! danger "This is **NOT** the recommended way to install Picamera2." + + However, if you wish to install Picamera2 with all the GUI (Qt and OpenGL) dependencies with pip, use: + + ```sh + sudo apt install -y python3-libcamera python3-kms++ + sudo apt install -y python3-pyqt5 python3-prctl + sudo apt install -y libatlas-base-dev ffmpeg python3-pip + pip3 install numpy --upgrade + pip3 install picamera2[gui] + ``` + + Or, If you **DON'T** want the GUI dependencies, use: + + ```sh + sudo apt install -y python3-libcamera python3-kms++ + sudo apt install -y python3-prctl libatlas-base-dev + sudo apt install -y ffmpeg libopenjp2-7 python3-pip + pip3 install numpy --upgrade + pip3 install picamera2 + ``` +   @@ -159,7 +234,7 @@ When installing VidGear from source, FFmpeg is the only API specific prerequisit | APIs | Dependencies | |:---:|:---| | CamGear | `yt_dlp` | - | PiGear | `picamera` | + | PiGear | `picamera`, `picamera2` _(see [this](#picamera2) for its installation)_ | | VideoGear | *Based on CamGear or PiGear backend in use* | | ScreenGear | `dxcam`, `mss`, `pyscreenshot`, `Pillow` | | WriteGear | **FFmpeg:** See [this doc ➶](https://abhitronix.github.io/vidgear/dev/gears/writegear/compression/advanced/ffmpeg_install/#ffmpeg-installation-instructions) | diff --git a/mkdocs.yml b/mkdocs.yml index c70babba6..0b923967e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,10 @@ plugins: - exclude: glob: - overrides/assets/README.md + +# Logging +validation: + unrecognized_links: ignore # Customization extra: From 37066b1c42783fe81412c976ab7d39c6a5162005 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 14 Apr 2024 18:27:51 +0530 Subject: [PATCH 07/81] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20Docs:=20Removed?= =?UTF-8?q?=20`Importing`=20section=20from=20overview=20to=20avoid=20confu?= =?UTF-8?q?sion=20while=20using=20an=20API.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gears/camgear/overview.md | 10 ---------- docs/gears/netgear/overview.md | 10 ---------- docs/gears/netgear_async/overview.md | 10 ---------- docs/gears/pigear/overview.md | 12 ------------ docs/gears/screengear/overview.md | 12 ------------ docs/gears/stabilizer/overview.md | 10 ---------- docs/gears/streamgear/introduction.md | 11 ----------- docs/gears/videogear/overview.md | 10 ---------- docs/gears/webgear/overview.md | 16 +--------------- docs/gears/webgear_rtc/overview.md | 18 +----------------- docs/gears/writegear/introduction.md | 10 ---------- 11 files changed, 2 insertions(+), 127 deletions(-) diff --git a/docs/gears/camgear/overview.md b/docs/gears/camgear/overview.md index b7cdadbed..051c414b7 100644 --- a/docs/gears/camgear/overview.md +++ b/docs/gears/camgear/overview.md @@ -46,16 +46,6 @@ CamGear internally implements [`yt_dlp`][yt_dlp] backend class for seamlessly pi   -## Importing - -You can import CamGear API in your program as follows: - -```python -from vidgear.gears import CamGear -``` - -  - ## Usage Examples
diff --git a/docs/gears/netgear/overview.md b/docs/gears/netgear/overview.md index 82c3dafe3..f2cb749c6 100644 --- a/docs/gears/netgear/overview.md +++ b/docs/gears/netgear/overview.md @@ -125,16 +125,6 @@ In addition to the primary modes, NetGear API also offers application-specific E   -## Importing - -You can import NetGear API in your program as follows: - -```python -from vidgear.gears import NetGear -``` - -  - ## Usage Examples
diff --git a/docs/gears/netgear_async/overview.md b/docs/gears/netgear_async/overview.md index 102ab6215..569b705f1 100644 --- a/docs/gears/netgear_async/overview.md +++ b/docs/gears/netgear_async/overview.md @@ -57,16 +57,6 @@ Whereas supported protocol are: `tcp` and `ipc`.   -## Importing - -You can import NetGear_Async API in your program as follows: - -```python -from vidgear.gears.asyncio import NetGear_Async -``` - -  - ## Usage Examples
diff --git a/docs/gears/pigear/overview.md b/docs/gears/pigear/overview.md index 0c40f979a..1d63d5d4e 100644 --- a/docs/gears/pigear/overview.md +++ b/docs/gears/pigear/overview.md @@ -44,16 +44,6 @@ Best of all, PiGear contains ==Threaded Internal Timer== - that silently keeps a   -## Importing - -You can import PiGear API in your program as follows: - -```python -from vidgear.gears import PiGear -``` - -  - ## Usage Examples
@@ -62,8 +52,6 @@ from vidgear.gears import PiGear !!! experiment "After going through PiGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/pigear_ex/)" - - ## Parameters
diff --git a/docs/gears/screengear/overview.md b/docs/gears/screengear/overview.md index a64d71985..328679693 100644 --- a/docs/gears/screengear/overview.md +++ b/docs/gears/screengear/overview.md @@ -43,16 +43,6 @@ ScreenGear API implements a multi-threaded wrapper around [**dxcam**](https://gi   -## Importing - -You can import ScreenGear API in your program as follows: - -```python -from vidgear.gears import ScreenGear -``` - -  - ## Usage Examples
@@ -61,8 +51,6 @@ from vidgear.gears import ScreenGear !!! experiment "After going through ScreenGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/screengear_ex/)" - - ## Parameters
diff --git a/docs/gears/stabilizer/overview.md b/docs/gears/stabilizer/overview.md index e90cf060a..68a4a72f5 100644 --- a/docs/gears/stabilizer/overview.md +++ b/docs/gears/stabilizer/overview.md @@ -67,16 +67,6 @@ The basic idea behind it is to tracks and save the salient feature array for the   -## Importing - -You can import Stabilizer Class in your program as follows: - -```python -from vidgear.gears.stabilizer import Stabilizer -``` - -  - ## Usage Examples
diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 21ebdbcf0..0bb95304a 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -81,17 +81,6 @@ StreamGear primarily operates in following independent modes for transcoding:   -## Importing - -You can import StreamGear API in your program as follows: - -```python -from vidgear.gears import StreamGear -``` - -  - - ## Watch Demo === "Watch MPEG-DASH Stream" diff --git a/docs/gears/videogear/overview.md b/docs/gears/videogear/overview.md index b37518dd9..8850c9422 100644 --- a/docs/gears/videogear/overview.md +++ b/docs/gears/videogear/overview.md @@ -43,16 +43,6 @@ VideoGear is ideal when you need to switch to different video sources without ch   -## Importing - -You can import VideoGear API in your program as follows: - -```python -from vidgear.gears import VideoGear -``` - -  - ## Usage Examples
diff --git a/docs/gears/webgear/overview.md b/docs/gears/webgear/overview.md index 347acdfb1..3b36a98f8 100644 --- a/docs/gears/webgear/overview.md +++ b/docs/gears/webgear/overview.md @@ -110,22 +110,8 @@ On initializing WebGear API, it automatically checks for three critical **data f - Finally these downloaded files thereby are verified for errors and API proceeds for instantiating the Starlette application normally. -  - -  - -## Importing - -You can import WebGear API in your program as follows: - -```python -from vidgear.gears.asyncio import WebGear -``` -   -  - ## WebGear's Default Template ??? new "New in v0.2.1" @@ -159,7 +145,7 @@ _Appears when an API Error is encountered:_ WebGear default 500 page -  +  ## Usage Examples diff --git a/docs/gears/webgear_rtc/overview.md b/docs/gears/webgear_rtc/overview.md index 35ff7b49e..2f34cfe4e 100644 --- a/docs/gears/webgear_rtc/overview.md +++ b/docs/gears/webgear_rtc/overview.md @@ -94,23 +94,8 @@ Same as [WebGear](../../webgear_rtc/overview/), WebGear_RTC API automatically ch * Finally these downloaded files thereby are verified for errors and API proceeds for instantiating the Starlette application normally. - -  - -  - -## Importing - -You can import WebGear_RTC API in your program as follows: - -```python -from vidgear.gears.asyncio import WebGear_RTC -``` -   -  - ## WebGear_RTC's Default Template The WebGear_RTC API by default uses simple & elegant [**WebGear_RTC's Default Theme**](https://github.com/abhiTronix/vidgear-vitals#webgear_rtc-default-theme) which looks like something as follows: @@ -143,7 +128,7 @@ The WebGear_RTC API by default uses simple & elegant [**WebGear_RTC's Default Th WebGear_RTC default 500 page -  +  ## Usage Examples @@ -165,7 +150,6 @@ The WebGear_RTC API by default uses simple & elegant [**WebGear_RTC's Default Th See here 🚀
- ## FAQs
diff --git a/docs/gears/writegear/introduction.md b/docs/gears/writegear/introduction.md index 3649ccc05..e1e353f80 100644 --- a/docs/gears/writegear/introduction.md +++ b/docs/gears/writegear/introduction.md @@ -59,16 +59,6 @@ WriteGear primarily operates in following modes:   -## Importing - -You can import WriteGear API in your program as follows: - -```python -from vidgear.gears import WriteGear -``` - -  - ## FAQs
From 41d2ba1c7f5b01d516aa2200e68905fde598a161 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 23 Apr 2024 00:42:44 +0530 Subject: [PATCH 08/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Overhaul=20to=20mk?= =?UTF-8?q?docs=20theme=20and=20Picamera2=20installation=20doc.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Added new `navigation.tabs.sticky`, `navigation.tracking`, `navigation.instant`, and `navigation.instant.progress` features. - 📱 Replaced docs font `Muli` with `Source Sans 3`. - 💄 Added custom admonition icons to `mkdocs.yml`. - 🚩 Added `enable_creation_date:true` and `fallback_to_build_date: true` to `git-revision-date-localized` plugin. - 🚚 Migrated to new Google Analytics 4 in mkdocs.yml. - ✨ Added new `git-authors` plugin in `mkdocs.yml`. - ✨ Added new `tables` markdown extension. - 🚩 Added `custom_fences` to `pymdownx.superfences` markdown extension. - 🚩 Added `line_spans: __span` and `pygments_lang_class: true` parameters to `pymdownx.highlight` markdown extension. - 🚩 Added `normalize_issue_symbols: true` to `pymdownx.magiclink` markdown extension. - ✨ Added complete Picamera2 library installation instructions for pip and source. - 🚚 Moved legacy picamera python library installation instructions to admonition. - ➕ Added new mkdocs feature dependency `mkdocs-git-authors-plugin`. - 👥 Added missing version contributors to changelog.md - 🧑‍💻 Added new icons to make headings more readable. - 🗑️ Replace depreciated `materialx` with supported `extensions` emoji extension. - 🩹 Fixed titles and indentations in various admonitions. - 📝 Replaced permalink icon with default one. - 🩹 Replaced wrong unsupported admonitions with supported ones. - 🔥 Removed all custom admonition icons and color CSS from `custom.css`. - 🙈 Added `.cache` directory to `.gitignore`. - 🔥 Removed Twitter section from help and site metadata. - 🩹 Fixed Ko-fi sponsorship heart hover effect in footer. - 💄 Replace twemoji `heart` with `heart-pulse` fontawesome SVG. - 🔥 Removed Gitter community chat sidecard js. - 💄 Redefined spacing between sections. - ✏️ Fixed small typos and hyperlinks. --- .github/workflows/deploy_docs.yml | 12 +- .gitignore | 1 + docs/changelog.md | 7 + docs/contribution.md | 8 +- docs/gears/camgear/overview.md | 2 +- docs/gears/camgear/usage.md | 12 +- docs/gears/netgear/advanced/ssh_tunnel.md | 6 +- docs/gears/netgear/usage.md | 2 +- docs/gears/pigear/overview.md | 4 +- docs/gears/pigear/usage.md | 4 +- docs/gears/screengear/params.md | 2 +- docs/gears/screengear/usage.md | 6 +- docs/gears/stabilizer/overview.md | 2 +- docs/gears/stabilizer/usage.md | 6 +- docs/gears/streamgear/params.md | 6 +- docs/gears/streamgear/rtfm/overview.md | 2 +- docs/gears/streamgear/rtfm/usage.md | 10 +- docs/gears/streamgear/ssm/overview.md | 2 +- docs/gears/streamgear/ssm/usage.md | 8 +- docs/gears/videogear/overview.md | 2 +- docs/gears/videogear/params.md | 2 +- docs/gears/videogear/usage.md | 2 +- docs/gears/webgear/overview.md | 17 +- docs/gears/webgear/params.md | 2 +- docs/gears/webgear_rtc/overview.md | 13 +- docs/gears/webgear_rtc/usage.md | 2 +- .../compression/advanced/ffmpeg_install.md | 2 +- docs/gears/writegear/compression/params.md | 2 +- docs/gears/writegear/compression/usage.md | 6 +- docs/help.md | 13 - docs/help/camgear_faqs.md | 2 +- docs/help/general_faqs.md | 12 +- docs/help/netgear_async_ex.md | 2 +- docs/help/netgear_async_faqs.md | 2 +- docs/help/netgear_ex.md | 4 +- docs/help/netgear_faqs.md | 2 +- docs/help/pigear_ex.md | 4 +- docs/help/pigear_faqs.md | 2 +- docs/help/screengear_ex.md | 4 +- docs/help/screengear_faqs.md | 2 +- docs/help/stabilizer_ex.md | 8 +- docs/help/stabilizer_faqs.md | 2 +- docs/help/videogear_ex.md | 2 +- docs/help/videogear_faqs.md | 2 +- docs/help/webgear_ex.md | 2 +- docs/help/webgear_faqs.md | 2 +- docs/help/webgear_rtc_ex.md | 2 +- docs/help/webgear_rtc_faqs.md | 2 +- docs/help/writegear_ex.md | 8 +- docs/help/writegear_faqs.md | 2 +- docs/installation.md | 2 +- docs/installation/pip_install.md | 42 ++- docs/installation/source_install.md | 30 +- docs/overrides/assets/stylesheets/custom.css | 276 +++++------------- docs/overrides/main.html | 87 +++--- mkdocs.yml | 47 ++- 56 files changed, 305 insertions(+), 412 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index de74a6c73..9a3b2dd59 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -47,7 +47,9 @@ jobs: if: success() - name: install_dependencies run: | - pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin + pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs-git-authors-plugin pip install jinja2==3.0.* if: success() - name: git configure @@ -88,7 +90,9 @@ jobs: if: success() - name: install_dependencies run: | - pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin + pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs-git-authors-plugin pip install jinja2==3.0.* if: success() - name: git configure @@ -131,7 +135,9 @@ jobs: if: success() - name: install_dependencies run: | - pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs mkdocs-material mkdocs-git-revision-date-localized-plugin mkdocs-minify-plugin + pip install -U mkdocs-exclude mike mkdocstrings mkdocstrings-python-legacy + pip install -U mkdocs-git-authors-plugin pip install jinja2==3.0.* if: success() - name: git configure diff --git a/.gitignore b/.gitignore index f08fe54d7..92bb22f08 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ venv Pipfile.lock env3.* env +.cache .coverage coverage.xml .netlify diff --git a/docs/changelog.md b/docs/changelog.md index 916bf42a8..631879f7e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -92,6 +92,9 @@ limitations under the License. * PR #375 * PR #370 +??? new "New Contributors" + * @ibtsam3301 +     @@ -374,6 +377,10 @@ limitations under the License. * PR #350 * PR #351 +??? new "New Contributors" + * @sueskind + +     diff --git a/docs/contribution.md b/docs/contribution.md index 19b50b66b..4c9d675b0 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -42,22 +42,22 @@ limitations under the License. ## Submission Contexts -### Got a question or problem? +### Got a question or problem? :material-lightbulb-question: For quick questions, please refrain from opening an issue, instead read our [FAQ & Troubleshooting](../help/get_help/#frequently-asked-questions) section or you can reach us on [Gitter](https://gitter.im/vidgear/community) community channel. -### Found a typo? +### Found a typo? :material-eraser: There's no need to contribute for some typos. Just reach us on [Gitter ➶](https://gitter.im/vidgear/community) community channel, We will correct them in (less than) no time. -### Found a bug? +### Found a bug? :material-bug-outline: If you encountered a bug, you can help us by [submitting an issue](../contribution/issue/) in our GitHub repository. Even better, you can submit a Pull Request(PR) with a fix, but make sure to read the [guidelines ➶](#submission-guidelines). -### Request for a feature/improvement? +### Request for a feature/improvement? :material-new-box: ??? tip "Subscribe to Github Repository" diff --git a/docs/gears/camgear/overview.md b/docs/gears/camgear/overview.md index 051c414b7..9b9dc83e6 100644 --- a/docs/gears/camgear/overview.md +++ b/docs/gears/camgear/overview.md @@ -52,7 +52,7 @@ CamGear internally implements [`yt_dlp`][yt_dlp] backend class for seamlessly pi See here 🚀
-!!! experiment "After going through CamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/camgear_ex/)" +!!! example "After going through CamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/camgear_ex/)" ## Parameters diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index 3320eb4b3..c46ea818c 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -20,7 +20,7 @@ limitations under the License. # CamGear API Usage Examples: -!!! experiment "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/camgear_ex/)" +!!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/camgear_ex/)"   @@ -79,7 +79,7 @@ The complete usage example for Dailymotion and Twitch URLs are as follows: **Checkout [this FAQ ➶](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support.** - !!! fail "Not all resolutions are supported with GStreamer Backend. See issue #244" + !!! failure "Not all resolutions are supported with GStreamer Backend. See issue #244" ???+ info "Exclusive CamGear Attributes for `yt_dlp` backend" @@ -224,7 +224,7 @@ The complete usage example for Dailymotion and Twitch URLs are as follows: CamGear API also provides out-of-the-box support for pipelining live video-frames and metadata from **:fontawesome-brands-youtube: YouTube (Livestream + Normal) Videos**. -!!! fail "YouTube Playlists :material-youtube-subscription: are not supported yet." +!!! failure "YouTube Playlists :material-youtube-subscription: are not supported yet." The complete usage example is as follows: @@ -234,7 +234,7 @@ The complete usage example is as follows: **Checkout [this FAQ ➶](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support.** - !!! fail "Not all resolutions are supported with GStreamer Backend. See issue #244" + !!! failure "Not all resolutions are supported with GStreamer Backend. See issue #244" ??? info "Exclusive CamGear Attributes for `yt_dlp` backend" @@ -377,10 +377,10 @@ CamGear API also supports **Direct Colorspace Manipulation**, which is ideal for !!! info "A more detailed information on colorspace manipulation can be found [here ➶](../../../bonus/colorspace_manipulation/)" -In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when `w` key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when `e` key is pressed, finally default colorspace _(i.e. **BGR**)_ when `s` key is pressed. Also, quit when `q` key is pressed: +In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when ++"W"++ key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when ++"E"++ key is pressed, finally default colorspace _(i.e. **BGR**)_ when ++"S"++ key is pressed. Also, quit when ++"Q"++ key is pressed: -!!! fail "Any incorrect or None-type value, will immediately revert the colorspace to default i.e. `BGR`." +!!! failure "Any incorrect or None-type value, will immediately revert the colorspace to default i.e. `BGR`." ```python hl_lines="7 30 34 38" diff --git a/docs/gears/netgear/advanced/ssh_tunnel.md b/docs/gears/netgear/advanced/ssh_tunnel.md index a3383d968..af4609427 100644 --- a/docs/gears/netgear/advanced/ssh_tunnel.md +++ b/docs/gears/netgear/advanced/ssh_tunnel.md @@ -73,7 +73,7 @@ SSH Tunnel Mode requires [`pexpect`](http://www.noah.org/wiki/pexpect) or [`para === "Pexpect" - !!! fail "`pexpect` is NOT compatible with Windows Machines." + !!! failure "`pexpect` is NOT compatible with Windows Machines." ```sh # install pexpect @@ -91,7 +91,7 @@ For implementing SSH Tunneling Mode, NetGear API currently provide following exc * **`ssh_tunnel_mode`** (_string_) : This attribute activates SSH Tunneling Mode and assigns the `"@:"` SSH URL for tunneling at Server end. Its usage is as follows: - !!! fail "On Server end, NetGear automatically validates if the `port` is open at specified Client's Public IP Address or not, and if it fails _(i.e. port is closed)_, NetGear will throw `AssertionError`!" + !!! failure "On Server end, NetGear automatically validates if the `port` is open at specified Client's Public IP Address or not, and if it fails _(i.e. port is closed)_, NetGear will throw `AssertionError`!" === "With Default Port" @@ -230,7 +230,7 @@ Open a terminal on Client System _(A Regular PC where you want to display the in For more information on Forwarding Port in Popular Home Routers. See [this document ➶](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/) -??? fail "Secsh channel X open FAILED: open failed: Administratively prohibited" +??? failure "Secsh channel X open FAILED: open failed: Administratively prohibited" **Error:** This error means that installed OpenSSH is preventing connections to forwarded ports from outside your Client Machine. diff --git a/docs/gears/netgear/usage.md b/docs/gears/netgear/usage.md index b8f9152fa..f5f1ec722 100644 --- a/docs/gears/netgear/usage.md +++ b/docs/gears/netgear/usage.md @@ -20,7 +20,7 @@ limitations under the License. # NetGear API Usage Examples: -!!! danger Important Information +!!! danger "Important Information" * Kindly go through each given examples thoroughly, any incorrect settings/parameter may result in errors or no output at all. diff --git a/docs/gears/pigear/overview.md b/docs/gears/pigear/overview.md index 1d63d5d4e..8fbd35274 100644 --- a/docs/gears/pigear/overview.md +++ b/docs/gears/pigear/overview.md @@ -33,7 +33,7 @@ PiGear provides a flexible multi-threaded framework around complete [picamera](h Best of all, PiGear contains ==Threaded Internal Timer== - that silently keeps active track of any frozen-threads/hardware-failures and exit safely, if any does occur. That means that if you're running PiGear API in your script and someone accidentally pulls the Camera-Module cable out, instead of going into possible kernel panic, API will exit safely to save resources. -!!! error "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using this API, otherwise nothing will work." +!!! failure "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using this API, otherwise nothing will work." !!! tip "Helpful Tips" @@ -50,7 +50,7 @@ Best of all, PiGear contains ==Threaded Internal Timer== - that silently keeps a See here 🚀
-!!! experiment "After going through PiGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/pigear_ex/)" +!!! example "After going through PiGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/pigear_ex/)" ## Parameters diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 69621a168..05c31f090 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -23,7 +23,7 @@ limitations under the License. !!! warning "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using this API, otherwise nothing will work." -!!! experiment "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/pigear_ex/)" +!!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/pigear_ex/)" @@ -133,7 +133,7 @@ PiGear API also supports **Direct Colorspace Manipulation**, which is ideal for !!! info "A more detailed information on colorspace manipulation can be found [here ➶](../../../bonus/colorspace_manipulation/)" -In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when `w` key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when `e` key is pressed, finally default colorspace _(i.e. **BGR**)_ when `s` key is pressed. Also, quit when `q` key is pressed: +In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when ++"W"++ key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when ++"E"++ key is pressed, finally default colorspace _(i.e. **BGR**)_ when ++"S"++ key is pressed. Also, quit when ++"Q"++ key is pressed: !!! warning "Any incorrect or None-Type value will immediately revert the colorspace to default _(i.e. `BGR`)_." diff --git a/docs/gears/screengear/params.md b/docs/gears/screengear/params.md index c12f96ad5..350c203cb 100644 --- a/docs/gears/screengear/params.md +++ b/docs/gears/screengear/params.md @@ -90,7 +90,7 @@ This parameter enables [`pyscreenshot`](https://github.com/BoboTiG/python-mss) u !!! warning "Remember to install backend library and all of its dependencies you're planning to use with ScreenGear API." -!!! error "Any value on [`monitor`](#monitor) parameter will disable the `backend` parameter. You cannot use both parameters at same time." +!!! failure "Any value on [`monitor`](#monitor) parameter will disable the `backend` parameter. You cannot use both parameters at same time." !!! info "Backend defaults to `dxcam` library on Windows _(if installed)_, and `pyscreenshot` otherwise." diff --git a/docs/gears/screengear/usage.md b/docs/gears/screengear/usage.md index 52bb5f8a8..6058b93c9 100644 --- a/docs/gears/screengear/usage.md +++ b/docs/gears/screengear/usage.md @@ -20,7 +20,7 @@ limitations under the License. # ScreenGear API Usage Examples: -!!! experiment "After going through ScreenGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/screengear_ex/)" +!!! example "After going through ScreenGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/screengear_ex/)" !!! success "Recommended: Install DXcam library on Windows :fontawesome-brands-windows: Machines" @@ -258,7 +258,7 @@ With ScreenGear API, you can select from many different backends that generates !!! note "Backend defaults to `dxcam` library on Windows _(if installed)_, and `pyscreenshot` otherwise." -!!! error "Any value on `monitor` parameter will disable the `backend` parameter. You cannot use them simultaneously." +!!! failure "Any value on `monitor` parameter will disable the `backend` parameter. You cannot use them simultaneously." ```python hl_lines="7" # import required libraries @@ -304,7 +304,7 @@ ScreenGear API also supports **Direct Colorspace Manipulation**, which is ideal !!! info "A more detailed information on colorspace manipulation can be found [here ➶](../../../bonus/colorspace_manipulation/)" -In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when `w` key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when `e` key is pressed, finally default colorspace _(i.e. **BGR**)_ when `s` key is pressed. Also, quit when `q` key is pressed: +In following example code, we will start with [**HSV**](https://en.wikipedia.org/wiki/HSL_and_HSV) as source colorspace, and then we will switch to [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) colorspace when ++"W"++ key is pressed, and then [**LAB**](https://en.wikipedia.org/wiki/CIELAB_color_space) colorspace when ++"E"++ key is pressed, finally default colorspace _(i.e. **BGR**)_ when ++"S"++ key is pressed. Also, quit when ++"Q"++ key is pressed: !!! warning "Any incorrect or None-type value, will immediately revert the colorspace to default i.e. `BGR`." diff --git a/docs/gears/stabilizer/overview.md b/docs/gears/stabilizer/overview.md index 68a4a72f5..b1c468948 100644 --- a/docs/gears/stabilizer/overview.md +++ b/docs/gears/stabilizer/overview.md @@ -73,7 +73,7 @@ The basic idea behind it is to tracks and save the salient feature array for the See here 🚀
-!!! experiment "After going through Stabilizer Class Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/stabilizer_ex/)" +!!! example "After going through Stabilizer Class Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/stabilizer_ex/)" ## Parameters diff --git a/docs/gears/stabilizer/usage.md b/docs/gears/stabilizer/usage.md index c11233a53..d31f5fff4 100644 --- a/docs/gears/stabilizer/usage.md +++ b/docs/gears/stabilizer/usage.md @@ -22,13 +22,13 @@ limitations under the License.   -!!! fail "The stabilizer may not perform well against High-frequency jitter in video. Use at your own risk!" +!!! failure "The stabilizer may not perform well against High-frequency jitter in video. Use at your own risk!" !!! warning "The stabilizer might be slower :snail: for High-Quality/Resolution :material-high-definition-box: videos-frames." !!! tip "It is advised to enable logging on the first run for easily identifying any runtime errors." -!!! experiment "After going through Stabilizer Class Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/stabilizer_ex/)" +!!! example "After going through Stabilizer Class Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/stabilizer_ex/)"   @@ -206,7 +206,7 @@ stream.stop() VideoGear's stabilizer can be used in conjunction with WriteGear API directly without any compatibility issues. The complete usage example is as follows: -!!! tip "You can also add live audio input to WriteGear pipeline. See this [bonus example](../../../help)" +!!! example "You can also add live audio input to WriteGear pipeline. See this [bonus example ➶](../../../help)" ```python # import required libraries diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index f5c7f1afb..14575db35 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -28,7 +28,7 @@ This parameter sets the valid filename/path for storing the StreamGear assets _( !!! warning "StreamGear API will throw `ValueError` if `output` provided is empty or invalid." -!!! error "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 _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`." !!! note "StreamGear generated sequence of multiple chunks/segments are also stored in the same directory." @@ -92,7 +92,7 @@ This parameter select the adaptive HTTP streaming formats. For now, the supporte !!! warning "Any invalid value to `format` parameter will result in ValueError!" -!!! error "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`." +!!! 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`." **Data-Type:** String @@ -329,7 +329,7 @@ Almost all FFmpeg parameter can be passed as dictionary attributes in `stream_pa !!! 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" -!!! error "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." +!!! 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." diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index 1f892d594..13e78154b 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -68,7 +68,7 @@ For this mode, StreamGear API provides exclusive [`stream()`](../../../../bonus/ See here 🚀
-!!! experiment "After going through StreamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" +!!! example "After going through StreamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" ## Parameters diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index a2e280ab9..9ac0a9605 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -35,7 +35,7 @@ limitations under the License. * Always use `terminate()` function at the very end of the main code. -!!! experiment "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" +!!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)"   @@ -621,7 +621,7 @@ The complete example is as follows: * 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. -!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" +!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" === "DASH" @@ -927,7 +927,7 @@ The complete example is as follows: stream_params = {"-audio": ["-f","dshow", "-i", "audio=Microphone (USB2.0 Camera)"]} ``` - !!! fail "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === ":material-linux: Linux" @@ -967,7 +967,7 @@ The complete example is as follows: stream_params = {"-audio": ["-f","alsa", "-i", "hw:1"]} ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === ":material-apple: MacOS" @@ -1003,7 +1003,7 @@ The complete example is as follows: stream_params = {"-audio": ["-f","avfoundation", "-audio_device_index", "0"]} ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! 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." diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 0fb479499..83e899b12 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -58,7 +58,7 @@ This mode can be easily activated by assigning suitable video path as input to [ See here 🚀
-!!! experiment "After going through StreamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" +!!! example "After going through StreamGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" ## Parameters diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index b9835611b..1b6958d7f 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -31,7 +31,7 @@ limitations under the License. * Always use `terminate()` function at the very end of the main code. -!!! experiment "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" +!!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)"   @@ -81,7 +81,7 @@ 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==, 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: +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." @@ -148,7 +148,7 @@ The complete example is as follows: * 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. -!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" +!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" === "DASH" @@ -275,7 +275,7 @@ For this example, let us use our own [H.265/HEVC](https://trac.ffmpeg.org/wiki/E !!! 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." -!!! fail "Always use `-streams` attribute to define additional streams safely, any duplicate or incorrect stream definition can break things!" +!!! failure "Always use `-streams` attribute to define additional streams safely, any duplicate or incorrect stream definition can break things!" === "DASH" diff --git a/docs/gears/videogear/overview.md b/docs/gears/videogear/overview.md index 8850c9422..418ee25fa 100644 --- a/docs/gears/videogear/overview.md +++ b/docs/gears/videogear/overview.md @@ -49,7 +49,7 @@ VideoGear is ideal when you need to switch to different video sources without ch See here 🚀
-!!! experiment "After going through VideoGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/videogear_ex/)" +!!! example "After going through VideoGear Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/videogear_ex/)" ## Parameters diff --git a/docs/gears/videogear/params.md b/docs/gears/videogear/params.md index fc5b3a5f0..40dda2867 100644 --- a/docs/gears/videogear/params.md +++ b/docs/gears/videogear/params.md @@ -190,7 +190,7 @@ This parameter controls the Stream Mode, .i.e if enabled(`stream_mode=True`), th !!! warning "VideoGear automatically enforce GStreamer backend _(backend=`cv2.CAP_GSTREAMER`)_ for YouTube-livestreams!" -!!! error "VideoGear will exit with `RuntimeError` for YouTube livestreams, if OpenCV is not compiled with GStreamer(`>=v1.0.0`) support. Checkout [this FAQ](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support." +!!! failure "VideoGear will exit with `RuntimeError` for YouTube livestreams, if OpenCV is not compiled with GStreamer(`>=v1.0.0`) support. Checkout [this FAQ](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support." **Data-Type:** Boolean diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index 17eb8a345..b58e5d554 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -20,7 +20,7 @@ limitations under the License. # VideoGear API Usage Examples: -!!! experiment "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/videogear_ex/)" +!!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/videogear_ex/)"   diff --git a/docs/gears/webgear/overview.md b/docs/gears/webgear/overview.md index 3b36a98f8..14eb2cbc8 100644 --- a/docs/gears/webgear/overview.md +++ b/docs/gears/webgear/overview.md @@ -48,7 +48,7 @@ In layman's terms, WebGear acts as a powerful ==**Video Broadcaster**== that tra ??? note "Customizing default video endpoint path" Starting with vidgear `v0.3.1`, you can change default `/video` video endpoint path to any alphanumeric string value, using [`custom_video_endpoint`](../params/#webgear-specific-attributes) optional string attribute. For example: - !!! error "Only alphanumeric string with no space in between are allowed as `custom_video_endpoint` value. Any other value will be discarded." + !!! failure "Only alphanumeric string with no space in between are allowed as `custom_video_endpoint` value. Any other value will be discarded." !!! warning "WebGear's Default Theme which expects only default `/video` video endpoint path, will fail to work, if it is customized to any other value using this `custom_video_endpoint` attribute." @@ -69,15 +69,16 @@ On initializing WebGear API, it automatically checks for three critical **data f ### Default Location - A _default location_ is the path of the directory where data files/folders are downloaded/generated/saved. -- By default, the `.vidgear` the folder at the home directory of your machine _(for e.g `/home/foo/.vidgear` on Linux)_ serves as the _default location_. +- By default, the `.vidgear` the folder at the home directory of your machine _(for e.g `/home/foo/.vidgear` on Linux :material-linux:)_ serves as the _default location_. - But you can also use WebGear's [`custom_data_location`](../params/#webgear-specific-attributes) dictionary attribute to change/alter _default location_ path to somewhere else. - !!! tip - You can set [`logging=True`](../params/#logging) during initialization, for easily identifying the selected _default location_, which will be something like this _(on a Linux machine)_ +!!! tip "Identifying Default location" + You can set [`logging=True`](../params/#logging) during initialization, for easily identifying the selected _default location_, which will be something like this on a Linux :material-linux: machine: + + ```sh + WebGear :: DEBUG :: `/home/foo/.vidgear` is the default location for saving WebGear data-files. + ``` - ```sh - WebGear :: DEBUG :: `/home/foo/.vidgear` is the default location for saving WebGear data-files. - ``` ### Auto-Generation process @@ -115,7 +116,7 @@ On initializing WebGear API, it automatically checks for three critical **data f ## WebGear's Default Template ??? new "New in v0.2.1" -New Standalone **WebGear's Default Theme** was added in `v0.2.1`. + New Standalone **WebGear's Default Theme** was added in `v0.2.1`. The WebGear API by default uses simple & elegant [**WebGear's Default Theme**](https://github.com/abhiTronix/vidgear-vitals#webgear-default-theme) which looks like something as follows: diff --git a/docs/gears/webgear/params.md b/docs/gears/webgear/params.md index 087a5cf31..fb3ad4353 100644 --- a/docs/gears/webgear/params.md +++ b/docs/gears/webgear/params.md @@ -69,7 +69,7 @@ This parameter can be used to pass user-defined parameter to WebGear API by form ??? new "New in v0.3.1" `custom_video_endpoint` attribute was added in `v0.3.1`. - !!! error "Only alphanumeric string with no space in between are allowed as `custom_video_endpoint` value. Any other value will be discarded." + !!! failure "Only alphanumeric string with no space in between are allowed as `custom_video_endpoint` value. Any other value will be discarded." !!! warning "WebGear's Default Theme which expects only default `/video` video endpoint path, will fail to work, if it is customized to any other value using this `custom_video_endpoint` attribute." diff --git a/docs/gears/webgear_rtc/overview.md b/docs/gears/webgear_rtc/overview.md index 2f34cfe4e..6663d1149 100644 --- a/docs/gears/webgear_rtc/overview.md +++ b/docs/gears/webgear_rtc/overview.md @@ -52,15 +52,16 @@ Same as [WebGear](../../webgear_rtc/overview/), WebGear_RTC API automatically ch ### Default Location * A _default location_ is the path of the directory where data files/folders are downloaded/generated/saved. -* By default, the `.vidgear` the folder at the home directory of your machine _(for e.g `/home/foo/.vidgear` on Linux)_ serves as the _default location_. +* By default, the `.vidgear` the folder at the home directory of your machine _(for e.g `/home/foo/.vidgear` on Linux :material-linux:)_ serves as the _default location_. * But you can also use WebGear_RTC's [`custom_data_location`](../params/#webgear_rtc-specific-attributes) dictionary attribute to change/alter *default location* path to somewhere else. - !!! tip - You can set [`logging=True`](../params/#logging) during initialization, for easily identifying the selected _default location_, which will be something like this _(on a Linux machine)_: +!!! tip "Identifying Default location" + You can set [`logging=True`](../params/#logging) during initialization, for easily identifying the selected _default location_, which will be something like this on a Linux :material-linux: machine: + + ```sh + WebGear_RTC :: DEBUG :: `/home/foo/.vidgear` is the default location for saving WebGear_RTC data-files. + ``` - ```sh - WebGear_RTC :: DEBUG :: `/home/foo/.vidgear` is the default location for saving WebGear_RTC data-files. - ``` ### Auto-Generation process diff --git a/docs/gears/webgear_rtc/usage.md b/docs/gears/webgear_rtc/usage.md index 747074a03..5301ba840 100644 --- a/docs/gears/webgear_rtc/usage.md +++ b/docs/gears/webgear_rtc/usage.md @@ -35,7 +35,7 @@ WebGear_RTC API is the part of `asyncio` package of VidGear, thereby you need to Must Required with WebGear_RTC API. You can easily install it via pip: -??? error "Microsoft Visual C++ 14.0 is required." +??? failure "Microsoft Visual C++ 14.0 is required." Installing `aiortc` on windows requires Microsoft Build Tools for Visual C++ libraries installed. You can easily fix this error by installing any **ONE** of these choices: diff --git a/docs/gears/writegear/compression/advanced/ffmpeg_install.md b/docs/gears/writegear/compression/advanced/ffmpeg_install.md index be96e4db5..9b93165e1 100644 --- a/docs/gears/writegear/compression/advanced/ffmpeg_install.md +++ b/docs/gears/writegear/compression/advanced/ffmpeg_install.md @@ -27,7 +27,7 @@ limitations under the License. WriteGear must requires FFmpeg executables for its Compression capabilities in Compression Mode. You can following machine-specific instructions for its installation: -!!! error "In case WriteGear API fails to detect valid FFmpeg executables on your system _(even if Compression Mode is enabled)_, it automatically fallbacks to [Non-Compression Mode](../../../non_compression/overview/)." +!!! failure "In case WriteGear API fails to detect valid FFmpeg executables on your system _(even if Compression Mode is enabled)_, it automatically fallbacks to [Non-Compression Mode](../../../non_compression/overview/)."   diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index d2949d451..9afe0e092 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -118,7 +118,7 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor !!! warning "While providing additional av-source with `-i` FFmpeg parameter in `output_params` make sure it don't interfere with WriteGear's frame pipeline otherwise it will break things!" - !!! error "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." + !!! failure "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." !!! 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" diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index b263d06a0..9ac7e48a1 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -489,7 +489,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === ":material-linux: Linux" @@ -535,7 +535,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === ":material-apple: MacOS" @@ -577,7 +577,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" !!! danger "Make sure this `-i` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." diff --git a/docs/help.md b/docs/help.md index 08b1874fa..57bc9b136 100644 --- a/docs/help.md +++ b/docs/help.md @@ -61,18 +61,6 @@ You can try helping solving those issues, or give valuable feedback/review on ne   - -## :material-twitter: Tweet about VidGear - -Tweet about VidGear and Spread the word 🗣: - - - -Let others know how you are using VidGear and why you like it! - -  - - ## :fontawesome-solid-gift: Helping Author > Donations help keep VidGear's development alive and motivate me _(as author)_. :heart:{ .heart } @@ -94,7 +82,6 @@ You can connect with me, the author 👋: ![Author Image](https://avatars.githubusercontent.com/u/34266896?v=4){ align=left width="160" loading=lazy } * Follow author on GitHub: [![GitHub follow](https://img.shields.io/github/followers/abhiTronix?label=Follow%20%40abhiTronix&logo=github&style=flat-square)](https://github.com/abhiTronix) -* Follow author on Twitter: * Get in touch with author on Linkedin: [![Linkedin follow](https://img.shields.io/badge/Follow-@Abhishek Thakur-orange.svg?logo=linkedin&style=flat-square)](https://in.linkedin.com/in/abhishek-abhitronix?trk=profile-badge) diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index d7f4fdda7..c841e6f2c 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # CamGear FAQs -  +  ## What is CamGear API and what does it do? diff --git a/docs/help/general_faqs.md b/docs/help/general_faqs.md index dbf4dd344..e1cefd142 100644 --- a/docs/help/general_faqs.md +++ b/docs/help/general_faqs.md @@ -20,9 +20,7 @@ limitations under the License. # General FAQs - -  - +  ## "I'm new to Python Programming or its usage in OpenCV Library", How to use vidgear in my projects? @@ -55,11 +53,11 @@ Once done, visit [Switching from OpenCV ➶](../../switch_from_cv/) to easily re ## How to log to a file in VidGear? -**Answer:** VidGear provides exclusive **`VIDGEAR_LOGFILE`** environment variable to enable logging to a file while logging is enabled _(i.e. `logging=True`)_ on respective Gear. You just have to set ==directory pathname _(automatically creates `vidgear.log` file)_== or a ==log file pathname== itself as value for this environment variable. This can be done on various platfroms/OSes as follows: +**Answer:** VidGear provides exclusive **`VIDGEAR_LOGFILE`** environment variable to enable logging to a file while logging is enabled _(i.e. `logging=True`)_ on respective Gear. You just have to set ==directory pathname _(automatically creates `vidgear.log` file)_== or a ==log file pathname== itself as value for this environment variable. This can be done on various Operating Systems as follows: !!! info "Remember enabling this logging to a file will completely disable any output on the terminal." -=== "Linux OS" +=== ":material-linux: Linux" ```sh # path to file @@ -73,7 +71,7 @@ Once done, visit [Switching from OpenCV ➶](../../switch_from_cv/) to easily re unset VIDGEAR_LOGFILE ``` -=== "Windows OS (Powershell)" +=== ":fontawesome-brands-windows: Windows (Powershell)" ```powershell # path to file @@ -87,7 +85,7 @@ Once done, visit [Switching from OpenCV ➶](../../switch_from_cv/) to easily re $Env:VIDGEAR_LOGFILE = "" ``` -=== "OSX/Mac OS" +=== ":material-apple: MacOS" ```sh # path to file diff --git a/docs/help/netgear_async_ex.md b/docs/help/netgear_async_ex.md index 1f759b84e..3e9ca26b2 100644 --- a/docs/help/netgear_async_ex.md +++ b/docs/help/netgear_async_ex.md @@ -20,7 +20,7 @@ limitations under the License. # NetGear_Async Examples -  +  ## Using NetGear_Async with WebGear diff --git a/docs/help/netgear_async_faqs.md b/docs/help/netgear_async_faqs.md index 289ca8d29..c2b34fa42 100644 --- a/docs/help/netgear_async_faqs.md +++ b/docs/help/netgear_async_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # NetGear_Async FAQs -  +  ## What is NetGear_Async API and what does it do? diff --git a/docs/help/netgear_ex.md b/docs/help/netgear_ex.md index 67cff187a..32cbaad3f 100644 --- a/docs/help/netgear_ex.md +++ b/docs/help/netgear_ex.md @@ -20,7 +20,7 @@ limitations under the License. # NetGear Examples -  +  ## Using NetGear with WebGear @@ -194,7 +194,7 @@ Open a terminal on Client System where you want to display the input frames _(an !!! 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](../netgear_faqs/#how-to-find-local-ip-address-on-different-os-platforms) for this purpose." -!!! fail "For VideoCapture APIs you also need to implement `start()` in addition to `read()` and `stop()` methods in your Custom Streaming Class as shown in following example, otherwise WebGear_RTC will fail to work!" +!!! failure "For VideoCapture APIs you also need to implement `start()` in addition to `read()` and `stop()` methods in your Custom Streaming Class as shown in following example, otherwise WebGear_RTC will fail to work!" ```python hl_lines="8-79 92-101" # import necessary libs diff --git a/docs/help/netgear_faqs.md b/docs/help/netgear_faqs.md index 33c6952a9..8531d33f7 100644 --- a/docs/help/netgear_faqs.md +++ b/docs/help/netgear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # NetGear FAQs -  +  ## What is NetGear API and what does it do? diff --git a/docs/help/pigear_ex.md b/docs/help/pigear_ex.md index 66f161b61..fb1fd9de3 100644 --- a/docs/help/pigear_ex.md +++ b/docs/help/pigear_ex.md @@ -20,13 +20,13 @@ limitations under the License. # PiGear Examples -  +  ## Setting variable `picamera` parameters for Camera Module at runtime You can use `stream` global parameter in PiGear to feed any [`picamera`](https://picamera.readthedocs.io/en/release-1.10/api_camera.html) parameters at runtime. -In this example we will set initial Camera Module's `brightness` value `80`, and will change it `50` when **`z` key** is pressed at runtime: +In this example we will set initial Camera Module's `brightness` value `80`, and will change it `50` when ++"Z"++ key is pressed at runtime: ```python hl_lines="35" # import required libraries diff --git a/docs/help/pigear_faqs.md b/docs/help/pigear_faqs.md index 3c24814da..cd15babd4 100644 --- a/docs/help/pigear_faqs.md +++ b/docs/help/pigear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # PiGear FAQs -  +  ## What is PiGear API and what does it do? diff --git a/docs/help/screengear_ex.md b/docs/help/screengear_ex.md index ce0e1f26e..8c297252a 100644 --- a/docs/help/screengear_ex.md +++ b/docs/help/screengear_ex.md @@ -20,7 +20,7 @@ limitations under the License. # ScreenGear Examples -  +  ## Using ScreenGear with NetGear and WriteGear @@ -178,7 +178,7 @@ The complete usage example is as follows: === "Advanced" - !!! fail "For VideoCapture APIs you also need to implement `start()` in addition to `read()` and `stop()` methods in your Custom Streaming Class as shown in following example, otherwise WebGear_RTC will fail to work!" + !!! failure "For VideoCapture APIs you also need to implement `start()` in addition to `read()` and `stop()` methods in your Custom Streaming Class as shown in following example, otherwise WebGear_RTC will fail to work!" ```python hl_lines="8-64 69" # import necessary libs diff --git a/docs/help/screengear_faqs.md b/docs/help/screengear_faqs.md index 7ccdb093e..947a6339c 100644 --- a/docs/help/screengear_faqs.md +++ b/docs/help/screengear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # ScreenGear FAQs -  +  ## What is ScreenGear API and what does it do? diff --git a/docs/help/stabilizer_ex.md b/docs/help/stabilizer_ex.md index 3e0e1aa72..36f050946 100644 --- a/docs/help/stabilizer_ex.md +++ b/docs/help/stabilizer_ex.md @@ -20,7 +20,7 @@ limitations under the License. # Stabilizer Class Examples -  +  ## Saving Stabilizer Class output with Live Audio Input @@ -79,7 +79,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then [checkout this troubleshooting guide ➶](https://www.maketecheasier.com/fix-microphone-not-working-windows10/) or reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === "On Linux" @@ -125,7 +125,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" === "On MacOS" @@ -167,7 +167,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W } ``` - !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" !!! danger "Make sure this `-i` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." diff --git a/docs/help/stabilizer_faqs.md b/docs/help/stabilizer_faqs.md index 7406c3795..8a4b84193 100644 --- a/docs/help/stabilizer_faqs.md +++ b/docs/help/stabilizer_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # Stabilizer Class FAQs -  +  ## What is Stabilizer Class and what does it do? diff --git a/docs/help/videogear_ex.md b/docs/help/videogear_ex.md index e544cd237..e2a5b7f3f 100644 --- a/docs/help/videogear_ex.md +++ b/docs/help/videogear_ex.md @@ -20,7 +20,7 @@ limitations under the License. # VideoGear Examples -  +  ## Using VideoGear with ROS(Robot Operating System) diff --git a/docs/help/videogear_faqs.md b/docs/help/videogear_faqs.md index 67cdb89cb..5fdd147d6 100644 --- a/docs/help/videogear_faqs.md +++ b/docs/help/videogear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # VideoGear FAQs -  +  ## What is VideoGear API and what does it do? diff --git a/docs/help/webgear_ex.md b/docs/help/webgear_ex.md index 46865e1e7..70bb2c5f5 100644 --- a/docs/help/webgear_ex.md +++ b/docs/help/webgear_ex.md @@ -20,7 +20,7 @@ limitations under the License. # WebGear Examples -  +  ## Using WebGear with RaspberryPi Camera Module diff --git a/docs/help/webgear_faqs.md b/docs/help/webgear_faqs.md index e39194337..6844f50cb 100644 --- a/docs/help/webgear_faqs.md +++ b/docs/help/webgear_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # WebGear FAQs -  +  ## What is WebGear API and what does it do? diff --git a/docs/help/webgear_rtc_ex.md b/docs/help/webgear_rtc_ex.md index 8b41c398d..47241a1b0 100644 --- a/docs/help/webgear_rtc_ex.md +++ b/docs/help/webgear_rtc_ex.md @@ -20,7 +20,7 @@ limitations under the License. # WebGear_RTC_RTC Examples -  +  ## Using WebGear_RTC with RaspberryPi Camera Module diff --git a/docs/help/webgear_rtc_faqs.md b/docs/help/webgear_rtc_faqs.md index 39330843c..ebfc8fc5a 100644 --- a/docs/help/webgear_rtc_faqs.md +++ b/docs/help/webgear_rtc_faqs.md @@ -20,7 +20,7 @@ limitations under the License. # WebGear_RTC FAQs -  +  ## What is WebGear_RTC API and what does it do? diff --git a/docs/help/writegear_ex.md b/docs/help/writegear_ex.md index 63b73abef..97a2d6193 100644 --- a/docs/help/writegear_ex.md +++ b/docs/help/writegear_ex.md @@ -30,10 +30,10 @@ In Compression Mode, you can use WriteGear for livestreaming with traditional pr ??? new "New in v0.2.6" This example was added in `v0.2.6`. -!!! alert "This example assume you already have a RTSP Server running at specified RTSP address with format *`rtsp://[RTSP_ADDRESS]:[RTSP_PORT]/[RTSP_PATH]`* for publishing video frames." +???+ tip "Creating your own RTSP Server locally" + If you want to create your RTSP Server locally, then checkout [**MediaMTX (formerly rtsp-simple-server)**](https://github.com/bluenviron/mediamtx) - ready-to-use and zero-dependency real-time media server and media proxy that allows to publish, read, proxy, record and playback video and audio streams. -??? tip "Creating your own RTSP Server locally" - If you want to create your RTSP Server locally, then checkout [**rtsp-simple-server**](https://github.com/aler9/rtsp-simple-server) - a ready-to-use and zero-dependency server and proxy that allows users to publish, read and proxy live video and audio streams through various protocols such as RTSP, RTMP etc. +!!! warning "This example assume you already have a RTSP Server running at specified RTSP address with format *`rtsp://[RTSP_ADDRESS]:[RTSP_PORT]/[RTSP_PATH]`* for publishing video frames." !!! danger "Make sure to change RTSP address `rtsp://localhost:8554/mystream` with yours in following code before running!" @@ -247,7 +247,7 @@ With WriteGear's Compression Mode, you can directly feed video-frames to [`v4l2l Now you can use `/dev/video0` Virtual Camera device path in WriteGear API. -??? fail "v4l2: open /dev/videoX: Permission denied" +??? failure "v4l2: open /dev/videoX: Permission denied" If you got this error, then you must add your username to the `video` group by running following commands: ```sh diff --git a/docs/help/writegear_faqs.md b/docs/help/writegear_faqs.md index 56161915a..105e9b53b 100644 --- a/docs/help/writegear_faqs.md +++ b/docs/help/writegear_faqs.md @@ -21,7 +21,7 @@ limitations under the License. # WriteGear FAQs -  +  ## What is WriteGear API and what does it do? diff --git a/docs/installation.md b/docs/installation.md index 16e72c879..9b34379fc 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -29,7 +29,7 @@ limitations under the License. ## Supported Systems -VidGear is well-tested and supported on the following systems(but not limited to), with [python 3.8+](https://www.python.org/downloads/) and [pip](https://pip.pypa.io/en/stable/installing/#do-i-need-to-install-pip) installed: +VidGear is well-tested and supported on the following systems(but not limited to), with [python 3.8+](https://www.python.org/downloads/) and [pip](https://pip.pypa.io/en/stable/getting-started/) installed: * Any :material-linux: Linux distro released in 2016 or later * :fontawesome-brands-windows: Windows 7 or later diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index a1fdb848a..661a3cb52 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -92,6 +92,8 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y ``` +  + ### Critical Prerequisites :warning: * #### OpenCV @@ -106,13 +108,14 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y ??? info "Other OpenCV binaries" - OpenCV mainainers also provide additional binaries via pip that contains both main modules and contrib/extra modules [`opencv-contrib-python`](https://pypi.org/project/opencv-contrib-python/), and for server (headless) environments like [`opencv-python-headless`](https://pypi.org/project/opencv-python-headless/) and [`opencv-contrib-python-headless`](https://pypi.org/project/opencv-contrib-python-headless/). You can also install ==any one of them== in similar manner. More information can be found [here](https://github.com/opencv/opencv-python#installation-and-usage). + OpenCV maintainers also provide additional binaries via pip that contains both main modules and contrib/extra modules [`opencv-contrib-python`](https://pypi.org/project/opencv-contrib-python/), and for server (headless) environments like [`opencv-python-headless`](https://pypi.org/project/opencv-python-headless/) and [`opencv-contrib-python-headless`](https://pypi.org/project/opencv-contrib-python-headless/). You can also install ==any one of them== in similar manner. More information can be found [here](https://github.com/opencv/opencv-python#installation-and-usage). ```sh pip install opencv-python ``` +  ### API Specific Prerequisites @@ -125,9 +128,12 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y * **For WriteGear API's Compression Mode**: Follow this dedicated [**FFmpeg Installation doc**](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. * **For StreamGear API**: Follow this dedicated [**FFmpeg Installation doc**](../../gears/streamgear/ffmpeg_install/) for its installation. +  * #### Picamera2 + Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: + ??? tip "Using Legacy `picamera` library with PiGear (`v0.3.3` and above)" PiGear API _(version `0.3.3` onwards)_ prioritizes the newer Picamera2 library under the hood for Raspberry Pi :fontawesome-brands-raspberry-pi: camera modules. However, if your operating system doesn't support Picamera2, you can still use the legacy [`picamera`](https://picamera.readthedocs.io/en/release-1.13/) library. Here's how to easily install it using pip: @@ -138,16 +144,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y pip install picamera ``` - ??? note "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." - - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. - - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: - - ```sh - sudo apt install -y python3-pyqt5 python3-opengl - ``` - - Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: - + ??? warning "Picamera2 is only supported on Raspberry Pi OS Bullseye (or later) images, both 32 and 64-bit." Picamera2 is **NOT** supported on: @@ -158,12 +155,21 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y === "Installation using `apt` (Recommended)" + ??? success "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." + + - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. + - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: + + ```sh + sudo apt install -y python3-pyqt5 python3-opengl + ``` + If Picamera2 is not already installed, then your image is presumably older and you should start with system upgrade: ```sh sudo apt update && upgrade ``` - !!! note "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." + !!! failure "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." Thereafter, you can install Picamera2 with all the GUI (Qt and OpenGL) dependencies using: @@ -201,21 +207,25 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y pip3 install picamera2 ``` +  + * #### Uvloop Required only if you're using the [**NetGear_Async**](../../gears/netgear_async/overview/) API on UNIX machines for maximum performance. You can easily install it via pip: - !!! fail "uvloop is **[NOT yet supported on Windows :fontawesome-brands-windows: Machines](https://github.com/MagicStack/uvloop/issues/14).**" + !!! failure "uvloop is **[NOT yet supported on Windows :fontawesome-brands-windows: Machines](https://github.com/MagicStack/uvloop/issues/14).**" ```sh pip install uvloop ``` +  + * #### DXcam Required only if you're using the [**ScreenGear**](../../gears/screengear/overview/) API on Windows machines for better FPS performance. You can easily install it via pip: - !!! fail "FYI, DXcam is **ONLY supported on Windows :fontawesome-brands-windows: Machines.**" + !!! failure "FYI, DXcam is **ONLY supported on Windows :fontawesome-brands-windows: Machines.**" ```sh pip install dxcam @@ -239,7 +249,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y === "Older" - !!! fail "`[core]` keyword isn't available in versions older than `v0.2.4`" + !!! failure "`[core]` keyword isn't available in versions older than `v0.2.4`" ```sh # Install older stable release with all Core dependencies @@ -254,7 +264,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y **Installation is as simple as:** -??? experiment "Installing vidgear with only selective dependencies" +??? example "Installing vidgear with only selective dependencies" Starting with version `v0.2.2`, you can now run any VidGear API by installing only just specific dependencies required by the API in use(except for some Core dependencies). diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index 7a6a9cc24..d1ab17565 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -26,7 +26,7 @@ limitations under the License. ## Prerequisites -When installing VidGear from source, there are some API specific prerequisites you need to install manually: +When installing VidGear from source, following are some API specific prerequisites you may need to install manually: !!! question "What about rest of the prerequisites?" @@ -110,8 +110,12 @@ When installing VidGear from source, there are some API specific prerequisites y * **For StreamGear API**: Follow this dedicated [**FFmpeg Installation doc**](../../gears/streamgear/ffmpeg_install/) for its installation. +  + * #### Picamera2 + Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: + ??? tip "Using Legacy `picamera` library with PiGear (`v0.3.3` and above)" PiGear API _(version `0.3.3` onwards)_ prioritizes the newer Picamera2 library under the hood for Raspberry Pi :fontawesome-brands-raspberry-pi: camera modules. However, if your operating system doesn't support Picamera2, you can still use the legacy [`picamera`](https://picamera.readthedocs.io/en/release-1.13/) library. Here's how to easily install it using pip: @@ -122,16 +126,7 @@ When installing VidGear from source, there are some API specific prerequisites y pip install picamera ``` - ??? note "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." - - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. - - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: - - ```sh - sudo apt install -y python3-pyqt5 python3-opengl - ``` - - Required only if you're using Raspberry Pi :fontawesome-brands-raspberry-pi: Camera Modules _(or USB webcams)_ with the [**PiGear**](../../gears/pigear/overview/) API. Here's how to install [Picamera2](https://github.com/raspberrypi/picamera2) python library: - + ??? warning "Picamera2 is only supported on Raspberry Pi OS Bullseye (or later) images, both 32 and 64-bit." Picamera2 is **NOT** supported on: @@ -142,12 +137,21 @@ When installing VidGear from source, there are some API specific prerequisites y === "Installation using `apt` (Recommended)" + ??? success "As of September 2022, Picamera2 is pre-installed on images downloaded from Raspberry Pi. So you don't have to install it manually." + + - [x] On **Raspberry Pi OS images**, Picamera2 is now installed with all the GUI (Qt and OpenGL) dependencies. + - [x] On **Raspberry Pi OS Lite**, it is installed without the GUI dependencies, although preview images can still be displayed using DRM/KMS. If these users wish to use the additional X-Windows GUI features, they will need to run: + + ```sh + sudo apt install -y python3-pyqt5 python3-opengl + ``` + If Picamera2 is not already installed, then your image is presumably older and you should start with system upgrade: ```sh sudo apt update && upgrade ``` - !!! note "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." + !!! failure "If you have installed Picamera2 previously using pip, then you should also uninstall this (`pip3 uninstall picamera2`)." Thereafter, you can install Picamera2 with all the GUI (Qt and OpenGL) dependencies using: @@ -198,7 +202,7 @@ When installing VidGear from source, there are some API specific prerequisites y !!! danger "DO NOT clone or install any other branch other than `testing` unless advised, as it is not tested with CI environments and possibly very unstable or unusable." -??? experiment "Installing vidgear with only selective dependencies" +??? example "Installing vidgear with only selective dependencies" Starting with version `v0.2.2`, you can now run any VidGear API by installing only just specific dependencies required by the API in use(except for some Core dependencies). diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 18580d4c6..98a3be818 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -21,209 +21,58 @@ limitations under the License. :root { --md-admonition-icon--new: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M13 2V3H12V9H11V10H9V11H8V12H7V13H5V12H4V11H3V9H2V15H3V16H4V17H5V18H6V22H8V21H7V20H8V19H9V18H10V19H11V22H13V21H12V17H13V16H14V15H15V12H16V13H17V11H15V9H20V8H17V7H22V3H21V2M14 3H15V4H14Z' /%3E%3C/svg%3E"); --md-admonition-icon--alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23 7v6h-2V7m0 8h2v2h-2M12 2a2 2 0 0 0-2 2 2 2 0 0 0 0 .29C7.12 5.14 5 7.82 5 11v6l-2 2v1h18v-1l-2-2v-6c0-3.18-2.12-5.86-5-6.71A2 2 0 0 0 14 4a2 2 0 0 0-2-2m-2 19a2 2 0 0 0 2 2 2 2 0 0 0 2-2Z'/%3E%3C/svg%3E"); - --md-admonition-icon--xquote: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M20 2H4C2.9 2 2 2.9 2 4V16C2 17.1 2.9 18 4 18H8V21C8 21.6 8.4 22 9 22H9.5C9.7 22 10 21.9 10.2 21.7L13.9 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2M11 13H7V8.8L8.3 6H10.3L8.9 9H11V13M17 13H13V8.8L14.3 6H16.3L14.9 9H17V13Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xwarning: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M23,12L20.56,9.22L20.9,5.54L17.29,4.72L15.4,1.54L12,3L8.6,1.54L6.71,4.72L3.1,5.53L3.44,9.21L1,12L3.44,14.78L3.1,18.47L6.71,19.29L8.6,22.47L12,21L15.4,22.46L17.29,19.28L20.9,18.46L20.56,14.78L23,12M13,17H11V15H13V17M13,13H11V7H13V13Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xdanger: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M12,2A9,9 0 0,0 3,11C3,14.03 4.53,16.82 7,18.47V22H9V19H11V22H13V19H15V22H17V18.46C19.47,16.81 21,14 21,11A9,9 0 0,0 12,2M8,11A2,2 0 0,1 10,13A2,2 0 0,1 8,15A2,2 0 0,1 6,13A2,2 0 0,1 8,11M16,11A2,2 0 0,1 18,13A2,2 0 0,1 16,15A2,2 0 0,1 14,13A2,2 0 0,1 16,11M12,14L13.5,17H10.5L12,14Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xtip: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xfail: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41' /%3E%3C/svg%3E"); - --md-admonition-icon--xsuccess: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19M5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5M21.61 2.39C21.61 2.39 16.66 .269 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.79 17.45 10.61 17.63 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39M14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63S16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46M8.88 16.53L7.47 15.12L8.88 16.53M6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L4.83 22H6.24M2 22H3.41L8.18 17.24L6.76 15.83L2 20.59V22M2 19.17L6.09 15.09C5.88 14.79 5.73 14.47 5.64 14.12L2 17.76V19.17Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xexample: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M5,9.5L7.5,14H2.5L5,9.5M3,4H7V8H3V4M5,20A2,2 0 0,0 7,18A2,2 0 0,0 5,16A2,2 0 0,0 3,18A2,2 0 0,0 5,20M9,5V7H21V5H9M9,19H21V17H9V19M9,13H21V11H9V13Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xquestion: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M20 4H18V3H20.5C20.78 3 21 3.22 21 3.5V5.5C21 5.78 20.78 6 20.5 6H20V7H19V5H20V4M19 9H20V8H19V9M17 3H16V7H17V3M23 15V18C23 18.55 22.55 19 22 19H21V20C21 21.11 20.11 22 19 22H5C3.9 22 3 21.11 3 20V19H2C1.45 19 1 18.55 1 18V15C1 14.45 1.45 14 2 14H3C3 10.13 6.13 7 10 7H11V5.73C10.4 5.39 10 4.74 10 4C10 2.9 10.9 2 12 2S14 2.9 14 4C14 4.74 13.6 5.39 13 5.73V7H14C14.34 7 14.67 7.03 15 7.08V10H19.74C20.53 11.13 21 12.5 21 14H22C22.55 14 23 14.45 23 15M10 15.5C10 14.12 8.88 13 7.5 13S5 14.12 5 15.5 6.12 18 7.5 18 10 16.88 10 15.5M19 15.5C19 14.12 17.88 13 16.5 13S14 14.12 14 15.5 15.12 18 16.5 18 19 16.88 19 15.5M17 8H16V9H17V8Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xbug: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M13 2V7.08A5.47 5.47 0 0 0 12 7A5.47 5.47 0 0 0 11 7.08V2M16.9 15A5 5 0 0 1 16.73 15.55L20 17.42V22H18V18.58L15.74 17.29A4.94 4.94 0 0 1 8.26 17.29L6 18.58V22H4V17.42L7.27 15.55A5 5 0 0 1 7.1 15H5.3L2.55 16.83L1.45 15.17L4.7 13H7.1A5 5 0 0 1 7.37 12.12L5.81 11.12L2.24 12L1.76 10L6.19 8.92L8.5 10.45A5 5 0 0 1 15.5 10.45L17.77 8.92L22.24 10L21.76 12L18.19 11.11L16.63 12.11A5 5 0 0 1 16.9 13H19.3L22.55 15.16L21.45 16.82L18.7 15M11 14A1 1 0 1 0 10 15A1 1 0 0 0 11 14M15 14A1 1 0 1 0 14 15A1 1 0 0 0 15 14Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xabstract: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M3,3H21V5H3V3M3,7H15V9H3V7M3,11H21V13H3V11M3,15H15V17H3V15M3,19H21V21H3V19Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xnote: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M20.71,7.04C20.37,7.38 20.04,7.71 20.03,8.04C20,8.36 20.34,8.69 20.66,9C21.14,9.5 21.61,9.95 21.59,10.44C21.57,10.93 21.06,11.44 20.55,11.94L16.42,16.08L15,14.66L19.25,10.42L18.29,9.46L16.87,10.87L13.12,7.12L16.96,3.29C17.35,2.9 18,2.9 18.37,3.29L20.71,5.63C21.1,6 21.1,6.65 20.71,7.04M3,17.25L12.56,7.68L16.31,11.43L6.75,21H3V17.25Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xinfo: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M18 2H12V9L9.5 7.5L7 9V2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V4C20 2.89 19.1 2 18 2M17.68 18.41C17.57 18.5 16.47 19.25 16.05 19.5C15.63 19.79 14 20.72 14.26 18.92C14.89 15.28 16.11 13.12 14.65 14.06C14.27 14.29 14.05 14.43 13.91 14.5C13.78 14.61 13.79 14.6 13.68 14.41S13.53 14.23 13.67 14.13C13.67 14.13 15.9 12.34 16.72 12.28C17.5 12.21 17.31 13.17 17.24 13.61C16.78 15.46 15.94 18.15 16.07 18.54C16.18 18.93 17 18.31 17.44 18C17.44 18 17.5 17.93 17.61 18.05C17.72 18.22 17.83 18.3 17.68 18.41M16.97 11.06C16.4 11.06 15.94 10.6 15.94 10.03C15.94 9.46 16.4 9 16.97 9C17.54 9 18 9.46 18 10.03C18 10.6 17.54 11.06 16.97 11.06Z' /%3E%3C/svg%3E"); - --md-admonition-icon--xadvance: url("data:image/svg+xml,%3C%3Fxml version='1.0' standalone='no'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 20010904//EN' 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'%3E%3Csvg version='1.0' xmlns='http://www.w3.org/2000/svg' width='512.000000pt' height='512.000000pt' viewBox='0 0 512.000000 512.000000' preserveAspectRatio='xMidYMid meet'%3E%3Cg transform='translate(0.000000,512.000000) scale(0.100000,-0.100000)'%0Afill='%23000000' stroke='none'%3E%3Cpath d='M2372 5103 c-39 -19 -70 -59 -78 -102 -12 -57 4 -98 63 -162 l57 -60%0A-280 -278 -279 -278 -774 -329 c-426 -181 -797 -342 -824 -359 -57 -34 -111%0A-105 -128 -168 -17 -62 -7 -173 20 -227 16 -32 215 -237 679 -702 590 -590%0A663 -660 717 -684 144 -66 328 -6 396 129 12 22 167 391 345 821 l324 781 274%0A274 275 275 60 -57 c41 -37 73 -58 96 -63 130 -24 225 99 166 214 -24 46 -934%0A954 -978 976 -41 20 -89 20 -131 -1z m421 -701 l157 -157 -263 -263 -262 -262%0A-488 2 -488 3 293 124 293 124 295 293 c162 162 297 294 300 294 3 0 76 -71%0A163 -158z m-545 -999 c-3 -10 -132 -325 -288 -701 l-284 -683 -628 628 c-345%0A345 -628 631 -628 634 0 3 69 35 153 72 l152 66 764 0 c722 1 763 0 759 -16z'/%3E%3Cpath d='M3742 4560 c-42 -26 -76 -99 -68 -144 13 -68 65 -121 126 -126 62 -5%0A82 1 123 39 41 39 42 41 42 106 0 65 -1 67 -42 106 -38 35 -47 39 -95 39 -36%0A0 -64 -7 -86 -20z'/%3E%3Cpath d='M3980 3922 c-104 -57 -108 -201 -8 -262 95 -58 222 17 222 130 0 110%0A-120 184 -214 132z'/%3E%3Cpath d='M3052 3270 c-99 -61 -95 -203 8 -257 34 -18 59 -23 117 -23 l73 0 0%0A-419 0 -419 -57 -27 c-275 -135 -509 -401 -617 -703 -87 -243 -91 -546 -10%0A-799 33 -105 104 -246 170 -339 61 -87 217 -244 266 -267 33 -16 94 -17 753%0A-17 658 0 720 1 753 17 51 24 206 182 269 273 151 221 223 454 223 719 0 261%0A-68 485 -213 701 -102 152 -281 313 -448 403 l-79 42 0 418 0 417 62 0 c81 0%0A137 27 166 79 40 70 20 154 -47 198 l-34 23 -661 0 c-642 0 -662 -1 -694 -20z%0Am908 -763 c0 -600 -16 -546 182 -636 201 -91 332 -212 443 -407 25 -43 45 -82%0A45 -86 0 -5 -394 -8 -875 -8 -481 0 -875 2 -875 5 0 3 12 29 26 58 97 191 249%0A338 446 432 53 25 100 45 105 45 20 0 75 70 84 106 4 22 9 249 9 507 l0 467%0A205 0 205 0 0 -483z m737 -1554 c-10 -227 -95 -428 -251 -591 l-59 -62 -632 0%0A-632 0 -60 63 c-156 162 -240 359 -250 585 l-6 122 948 0 948 0 -6 -117z'/%3E%3C/g%3E%3C/svg%3E%0A"); -} - - -/* "Advance" admonition*/ -.md-typeset .admonition.advance, -.md-typeset details.advance { - border-color: rgb(27, 77, 62); -} - -.md-typeset .advance > .admonition-title, -.md-typeset .advance > summary, -.md-typeset .experiment > .admonition-title, -.md-typeset .experiment > summary { - background-color: rgba(0, 57, 166, 0.1); - border-color: rgb(0, 57, 166); -} - -.md-typeset .advance > .admonition-title::before, -.md-typeset .advance > summary::before, -.md-typeset .experiment > .admonition-title::before, -.md-typeset .experiment > summary::before { - background-color: rgb(0, 57, 166); - -webkit-mask-image: var(--md-admonition-icon--xadvance); - mask-image: var(--md-admonition-icon--xadvance); } /* "New" admonition*/ .md-typeset .admonition.new, .md-typeset details.new { - border-color: rgb(57,255,20); + border-color: rgb(57, 255, 20); } -.md-typeset .new > .admonition-title, -.md-typeset .new > summary { - background-color: rgb(57,255,20,0.1); - border-color: rgb(57,255,20); +.md-typeset .new>.admonition-title, +.md-typeset .new>summary { + background-color: rgb(57, 255, 20, 0.1); } -.md-typeset .new > .admonition-title::before, -.md-typeset .new > summary::before { - background-color: rgb(57,255,20); +.md-typeset .new>.admonition-title::before, +.md-typeset .new>summary::before { + background-color: rgb(57, 255, 20); -webkit-mask-image: var(--md-admonition-icon--new); mask-image: var(--md-admonition-icon--new); } - /* "Alert" admonition*/ .md-typeset .admonition.alert, .md-typeset details.alert { border-color: rgb(255, 0, 255); } -.md-typeset .alert > .admonition-title, -.md-typeset .alert > summary { +.md-typeset .alert>.admonition-title, +.md-typeset .alert>summary { background-color: rgba(255, 0, 255, 0.1); - border-color: rgb(255, 0, 255); } -.md-typeset .alert > .admonition-title::before, -.md-typeset .alert > summary::before { +.md-typeset .alert>.admonition-title::before, +.md-typeset .alert>summary::before { background-color: rgb(255, 0, 255); -webkit-mask-image: var(--md-admonition-icon--alert); mask-image: var(--md-admonition-icon--alert); } -/* Custom "Warning" admonition*/ -.md-typeset .attention > .admonition-title::before, -.md-typeset .attention > summary::before, -.md-typeset .caution > .admonition-title::before, -.md-typeset .caution > summary::before, -.md-typeset .warning > .admonition-title::before, -.md-typeset .warning > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xwarning); - mask-image: var(--md-admonition-icon--xwarning); -} - -/* Custom "Tip" admonition*/ -.md-typeset .hint > .admonition-title::before, -.md-typeset .hint > summary::before, -.md-typeset .important > .admonition-title::before, -.md-typeset .important > summary::before, -.md-typeset .tip > .admonition-title::before, -.md-typeset .tip > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xtip) !important; - mask-image: var(--md-admonition-icon--xtip) !important; -} - -/* Custom "Info" admonition*/ -.md-typeset .info > .admonition-title::before, -.md-typeset .info > summary::before, -.md-typeset .todo > .admonition-title::before, -.md-typeset .todo > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xinfo); - mask-image: var(--md-admonition-icon--xinfo); -} - -/* Custom "Danger" admonition*/ -.md-typeset .danger > .admonition-title::before, -.md-typeset .danger > summary::before, -.md-typeset .error > .admonition-title::before, -.md-typeset .error > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xdanger); - mask-image: var(--md-admonition-icon--xdanger); -} - -/* Custom "Note" admonition*/ -.md-typeset .note > .admonition-title::before, -.md-typeset .note > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xnote); - mask-image: var(--md-admonition-icon--xnote); -} - -/* Custom "Abstract" admonition*/ -.md-typeset .abstract > .admonition-title::before, -.md-typeset .abstract > summary::before, -.md-typeset .summary > .admonition-title::before, -.md-typeset .summary > summary::before, -.md-typeset .tldr > .admonition-title::before, -.md-typeset .tldr > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xabstract); - mask-image: var(--md-admonition-icon--xabstract); -} - -/* Custom "Question" admonition*/ -.md-typeset .faq > .admonition-title::before, -.md-typeset .faq > summary::before, -.md-typeset .help > .admonition-title::before, -.md-typeset .help > summary::before, -.md-typeset .question > .admonition-title::before, -.md-typeset .question > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xquestion); - mask-image: var(--md-admonition-icon--xquestion); -} - -/* Custom "Success" admonition*/ -.md-typeset .check > .admonition-title::before, -.md-typeset .check > summary::before, -.md-typeset .done > .admonition-title::before, -.md-typeset .done > summary::before, -.md-typeset .success > .admonition-title::before, -.md-typeset .success > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xsuccess); - mask-image: var(--md-admonition-icon--xsuccess); -} - -/* Custom "Fail" admonition*/ -.md-typeset .fail > .admonition-title::before, -.md-typeset .fail > summary::before, -.md-typeset .failure > .admonition-title::before, -.md-typeset .failure > summary::before, -.md-typeset .missing > .admonition-title::before, -.md-typeset .missing > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xfail); - mask-image: var(--md-admonition-icon--xfail); -} - -/* Custom "bug" admonition*/ -.md-typeset .bug > .admonition-title::before, -.md-typeset .bug > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xbug); - mask-image: var(--md-admonition-icon--xbug); -} - -/* Custom "Example" admonition*/ -.md-typeset .example > .admonition-title::before, -.md-typeset .example > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xexample); - mask-image: var(--md-admonition-icon--xexample); -} - -/* Custom "Summary" admonition*/ -.md-typeset .cite > .admonition-title::before, -.md-typeset .cite > summary::before, -.md-typeset .quote > .admonition-title::before, -.md-typeset .quote > summary::before { - -webkit-mask-image: var(--md-admonition-icon--xquote); - mask-image: var(--md-admonition-icon--xquote); -} - - -/* Handles DeFFcode UI */ -.md-nav__item--active > .md-nav__link { + + +/* Handles UI */ +.md-nav__item--active>.md-nav__link { font-weight: bold; } + .center { display: block; margin-left: auto; margin-right: auto; width: 80%; } + .doc-heading { padding-top: 50px; } @@ -261,18 +110,18 @@ limitations under the License. /* Handles Gitter Sidecard UI */ .gitter-open-chat-button { - background-color: var(--md-primary-fg-color) !important; - font-family: inherit !important; - font-size: 12px; - -webkit-filter: none !important; - filter: none !important; + background-color: var(--md-primary-fg-color) !important; + font-family: inherit !important; + font-size: 12px; + -webkit-filter: none !important; + filter: none !important; } /* Custom Blockquotes */ blockquote { padding: 0.5em 10px; - quotes: "\201C""\201D""\2018""\2019"; + quotes: "\201C" "\201D" "\2018" "\2019"; } blockquote:before { @@ -414,62 +263,73 @@ footer.sponsorship:not(:hover) .twemoji.heart-throb-hover svg { /* Heart Animation */ @keyframes heart { - 0%, 40%, 80%, 100% { - transform: scale(1); - } - 20%, 60% { - transform: scale(1.15); - } + + 0%, + 40%, + 80%, + 100% { + transform: scale(1); + } + + 20%, + 60% { + transform: scale(1.15); + } } + .heart { - animation: heart 1000ms infinite; + animation: heart 1000ms infinite; } /* Footer Sponsorship Block */ footer.sponsorship { - text-align: center; + text-align: center; } - footer.sponsorship hr { - display: inline-block; - width: px2rem(32px); - margin: 0 px2rem(14px); - vertical-align: middle; - border-bottom: 2px solid var(--md-default-fg-color--lighter); + +footer.sponsorship hr { + display: inline-block; + width: px2rem(32px); + margin: 0 px2rem(14px); + vertical-align: middle; + border-bottom: 2px solid var(--md-default-fg-color--lighter); } - footer.sponsorship:hover hr { - border-color: var(--md-accent-fg-color); + +footer.sponsorship:hover hr { + border-color: var(--md-accent-fg-color); } - footer.sponsorship:not(:hover) .twemoji.heart-throb-hover svg { - color: var(--md-default-fg-color--lighter) !important; + +footer.sponsorship:not(:hover) .twemoji.heart-throb-hover svg { + color: var(--md-default-fg-color--lighter) !important; } /* Dark Theme Changes */ [data-md-color-scheme="slate"] { - --md-hue: 260; + --md-hue: 260; } -body[data-md-color-scheme="slate"] img[class="shadow"]{ - -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); - filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); + +body[data-md-color-scheme="slate"] img[class="shadow"] { + -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); + filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); } -body[data-md-color-scheme="slate"] div[class="btn-container"]{ - -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); - filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); +body[data-md-color-scheme="slate"] div[class="btn-container"] { + -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); + filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); } -body[data-md-color-scheme="slate"] div[class="highlight"]{ - -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); - filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); +body[data-md-color-scheme="slate"] div[class="highlight"] { + -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); + filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); } -body[data-md-color-scheme="slate"] div[class^="admonition"]{ - -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); - filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); +body[data-md-color-scheme="slate"] div[class^="admonition"] { + -webkit-filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); + filter: drop-shadow(2px 2px 1px rgba(0, 0, 0, 0.5)); } -body[data-md-color-scheme="slate"] img[class="shadow2"]{ - -webkit-filter: drop-shadow(1px 1px 0 black); - filter: drop-shadow(1px 1px 0 black); +body[data-md-color-scheme="slate"] img[class="shadow2"] { + -webkit-filter: drop-shadow(1px 1px 0 black); + filter: drop-shadow(1px 1px 0 black); } \ No newline at end of file diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 5a5377738..9537c6866 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,60 +1,53 @@ {% extends "base.html" %} {% block extrahead %} - {% set title = config.site_name %} - {% if page and page.meta and page.meta.title %} - {% set title = title ~ " - " ~ page.meta.title %} - {% elif page and page.title and not page.is_homepage %} - {% set title = title ~ " - " ~ page.title | striptags %} - {% endif %} - {% set image = config.site_url ~ '/assets/images/banner_link.png' %} - - - - - - - - - - - - - - +{% set title = config.site_name %} +{% if page and page.meta and page.meta.title %} +{% set title = title ~ " - " ~ page.meta.title %} +{% elif page and page.title and not page.is_homepage %} +{% set title = title ~ " - " ~ page.title | striptags %} +{% endif %} +{% set image = config.site_url ~ '/assets/images/banner_link.png' %} + + + + + + + + {% endblock %} {% block announce %} - - {% include ".icons/material/message-alert.svg" %} Hey, checkout our new Deffcode library which will added as vidgear backend soon. We’d love to hear your feedback! + + {% include ".icons/material/message-alert.svg" %} Hey, checkout our new Deffcode library + which will added as vidgear backend soon. We’d love to hear your feedback! {% endblock %} {% block outdated %} - You're not viewing the latest version. - - Click here to go to latest. - +You're not viewing the latest version. + + Click here to go to latest. + {% endblock %} {% block content %} - {{ super() }} - +{{ super() }} + {% endblock %} {% block libs %} - - - - - - + + + + {% endblock %} - {% include ".icons/material/message-alert.svg" %} Hey, checkout our new Deffcode library - which will added as vidgear backend soon. We’d love to hear your feedback! + {% include ".icons/material/message-alert.svg" %} We're excited to announce our new Deffcode library, + which will be integrated with VidGear soon. We value your feedback and would love to hear your thoughts! {% endblock %} {% block outdated %} You're not viewing the latest version. diff --git a/mkdocs.yml b/mkdocs.yml index 61b8f40e7..463b7680d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -125,6 +125,7 @@ extra: link: https://dev.to/abhitronix version: provider: mike + default: latest analytics: # Google analytics provider: google property: G-XMZCQ3KBNJ From ee11937370afe165b664b1d8e28de7be92e334fc Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 15 May 2024 00:32:49 +0530 Subject: [PATCH 19/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20workaround?= =?UTF-8?q?=20for=20'AttributeError:=20'DXCamera'=20object=20has=20no=20at?= =?UTF-8?q?tribute=20'is=5Fcapturing''=20error=20on=20Windows.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💄 Added new `screengear_error11.png` asset. --- docs/help/screengear_faqs.md | 25 ++++++++++++++++++ .../assets/images/screengear_error11.png | Bin 0 -> 78263 bytes 2 files changed, 25 insertions(+) create mode 100644 docs/overrides/assets/images/screengear_error11.png diff --git a/docs/help/screengear_faqs.md b/docs/help/screengear_faqs.md index 59f191260..c83dff16e 100644 --- a/docs/help/screengear_faqs.md +++ b/docs/help/screengear_faqs.md @@ -50,4 +50,29 @@ limitations under the License. **Answer:** With `mss` backend, see ScreenGear's [`monitor`](../../gears/screengear/params/#monitor) parameter that sets the index of the monitor to grab a frame from. If its value is `-1`, it will record from all monitors. _More information can be found [here ➶](https://python-mss.readthedocs.io/examples.html#a-screen-shot-to-grab-them-all)_ +  + +## I'm getting "AttributeError: 'DXCamera' object has no attribute 'is_capturing'" Error? + +**Answer:** This is a [well-known error](https://github.com/ra1nty/DXcam/issues/38) in backend `dxcam` library which occurs when you've multiple GPUs on your Windows machine. To workaround this, you need select Internal GPU in settings as follows: + +=== "On :fontawesome-brands-windows: Windows 11" + + In **Settings**, go to `System > Display > Graphics` and add your `Python.exe` as _"Desktop App"_, then select _"Power saving"_ as follows: +
+ AttributeError: 'DXCamera' +
+ + And finally press **Save** button. + +=== "On :fontawesome-brands-windows: Windows 10" + + In **Settings**, go to `Graphics Settings` and add your `Python.exe` as _"Desktop App"_, then select _"Power saving"_ as follows: + +
+ AttributeError: 'DXCamera' +
+ + And finally press **Save** button. +   \ No newline at end of file diff --git a/docs/overrides/assets/images/screengear_error11.png b/docs/overrides/assets/images/screengear_error11.png new file mode 100644 index 0000000000000000000000000000000000000000..0728f9e404653bf443a267565d258c3e937dd980 GIT binary patch literal 78263 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 literal 0 HcmV?d00001 From 08b583d02a7fc27859bd98ad77713e42e971006f Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 15 May 2024 21:30:43 +0530 Subject: [PATCH 20/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Maintenance:=20Refac?= =?UTF-8?q?tor=20WebGear=20and=20WebGear=20RTC=20shutdown=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🏗️ Replaced deprecated Starlette's `on_shutdown` parameter with an async context manager `lifespan` in WebGear and WebGear_RTC APIs. - 🚚 Moved shutdown logic for VideoGear and peer RTC connections to this new `lifespan` context manager. - 📦️ Added new `contextlib` import for using `asynccontextmanager`. Docs: - 📝 Update README.md with changes to JPEG compression options - ✏️ Replaces deprecated options (`frame_jpeg_quality`, `frame_jpeg_optimize`, `frame_jpeg_progressive`) with their newer equivalents (`jpeg_compression_quality,` `jpeg_compression_fastdct`, `jpeg_compression_fastupsample`) in WebGear usage example. --- README.md | 6 +++--- vidgear/gears/asyncio/webgear.py | 11 ++++++++++- vidgear/gears/asyncio/webgear_rtc.py | 24 +++++++++++++----------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index be7466358..ac9429546 100644 --- a/README.md +++ b/README.md @@ -515,9 +515,9 @@ from vidgear.gears.asyncio import WebGear # various performance tweaks options = { "frame_size_reduction": 40, - "frame_jpeg_quality": 80, - "frame_jpeg_optimize": True, - "frame_jpeg_progressive": False, + "jpeg_compression_quality": 80, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": False, } # initialize WebGear app diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index 29df9d4d4..7a5f7bb0f 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -22,6 +22,7 @@ import os import asyncio import inspect +import contextlib import numpy as np import logging as log from os.path import expanduser @@ -421,7 +422,7 @@ def __call__(self): routes=self.routes, middleware=self.middleware, exception_handlers=self.__exception_handlers, - on_shutdown=[self.shutdown], + lifespan=self.__lifespan, ) async def __producer(self): @@ -535,6 +536,14 @@ async def __server_error(self, request, exc): ) ) + @contextlib.asynccontextmanager + async def __lifespan(self, context): + try: + yield + finally: + # close Video Server + self.shutdown() + def shutdown(self): """ Implements a Callable to be run on application shutdown diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index b428c6665..4b3c3e4a4 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -21,6 +21,7 @@ # import the necessary packages import os import time +import contextlib import fractions import asyncio import logging as log @@ -531,7 +532,7 @@ def __call__(self): routes=self.routes, middleware=self.middleware, exception_handlers=self.__exception_handlers, - on_shutdown=[self.__on_shutdown], + lifespan=self.__lifespan, ) async def __offer(self, request): @@ -636,16 +637,17 @@ async def __reset_connections(self, request): # if does, then do nothing return PlainTextResponse("DISABLED") - async def __on_shutdown(self): - """ - Implements a Callable to be run on application shutdown - """ - # close Video Server - self.shutdown() - # collects peer RTC connections - coros = [pc.close() for pc in self.__pcs] - await asyncio.gather(*coros) - self.__pcs.clear() + @contextlib.asynccontextmanager + async def __lifespan(self, context): + try: + yield + finally: + # close Video Server + self.shutdown() + # collects peer RTC connections + coros = [pc.close() for pc in self.__pcs] + await asyncio.gather(*coros) + self.__pcs.clear() def shutdown(self): """ From af6d6592dcee1e42bf0e136ea8bad5c8780871cc Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 17 May 2024 00:42:58 +0530 Subject: [PATCH 21/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Deprecated=20event?= =?UTF-8?q?=5Floop=20fixture=20overrides.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 👷 Added new recommended approach of using `pytest.mark.asyncio(scope="module")` to mark all WebGear_RTC and NetGear_Async tests as asynchronous and utilize the same event loop throughout the module. - 🗑️ Deprecated custom `event_loop` fixture overrides in WebGear_RTC and NetGear_Async tests. - 🔥 Removed redundant `pytest.mark.asyncio` decorators from several test functions. --- .../asyncio_tests/test_netgear_async.py | 24 +++++++------------ .../asyncio_tests/test_webgear_rtc.py | 15 +----------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py index 3d7a8c6a3..3e8a07bcf 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -41,12 +42,7 @@ logger.setLevel(log.DEBUG) -@pytest.fixture(scope="module") -def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = asyncio.SelectorEventLoop() - yield loop - loop.close() +pytestmark = pytest.mark.asyncio(scope="module") def return_testvideo_path(): @@ -137,7 +133,7 @@ async def client_iterator(client, data=False): # Create a async function made to test bidirectional mode async def client_dataframe_iterator(client, data=""): # loop over Client's Asynchronous Data and Frame Generator - async for (recvd_data, frame) in client.recv_generator(): + async for recvd_data, frame in client.recv_generator(): if not (recvd_data is None): # {do something with received server recv_data here} logger.debug(recvd_data) @@ -152,7 +148,6 @@ async def client_dataframe_iterator(client, data=""): await asyncio.sleep(0) -@pytest.mark.asyncio @pytest.mark.parametrize( "pattern", [1, 2, 3, 4], @@ -192,7 +187,6 @@ async def test_netgear_async_playback(pattern): ] -@pytest.mark.asyncio @pytest.mark.parametrize("generator, result", test_data_class) async def test_netgear_async_custom_server_generator(generator, result): try: @@ -248,7 +242,6 @@ async def test_netgear_async_custom_server_generator(generator, result): ] -@pytest.mark.asyncio @pytest.mark.parametrize( "generator, data, options_server, options_client, result", test_data_class, @@ -280,7 +273,6 @@ async def test_netgear_async_bidirectionalmode( client.close(skip_loop=True) -@pytest.mark.asyncio @pytest.mark.parametrize( "address, port", [("172.31.11.15.77", "5555"), ("172.31.11.33.44", "5555"), (None, "5555")], @@ -331,7 +323,6 @@ async def test_netgear_async_addresses(address, port): client.close(skip_loop=True) -@pytest.mark.asyncio async def test_netgear_async_recv_generator(): server = None try: @@ -351,7 +342,6 @@ async def test_netgear_async_recv_generator(): server.close(skip_loop=True) -@pytest.mark.asyncio @pytest.mark.parametrize( "pattern, options", [ @@ -366,9 +356,11 @@ async def test_netgear_async_options(pattern, options): try: # define and launch server client = NetGear_Async( - source=None - if options["bidirectional_mode"] != True - else return_testvideo_path(), + source=( + None + if options["bidirectional_mode"] != True + else return_testvideo_path() + ), receive_mode=True, timeout=5.0, pattern=pattern, diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py index 5477ce796..fbeaa893c 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -53,13 +53,7 @@ logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) - -@pytest.fixture -def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = asyncio.SelectorEventLoop() - yield loop - loop.close() +pytestmark = pytest.mark.asyncio(scope="module") def return_testvideo_path(): @@ -238,7 +232,6 @@ def stop(self): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio @pytest.mark.parametrize("source, stabilize, colorspace, time_delay", test_data) async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ @@ -308,7 +301,6 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio @pytest.mark.parametrize("options", test_data) async def test_webgear_rtc_options(options): """ @@ -368,7 +360,6 @@ async def test_webgear_rtc_options(options): ), reason="Random Failures!", ) -@pytest.mark.asyncio @pytest.mark.parametrize("options", test_data) async def test_webpage_reload(options): """ @@ -459,7 +450,6 @@ async def test_webpage_reload(options): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio @pytest.mark.parametrize("stream_class, result", test_stream_classes) async def test_webgear_rtc_custom_stream_class(stream_class, result): """ @@ -513,7 +503,6 @@ async def test_webgear_rtc_custom_stream_class(stream_class, result): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio @pytest.mark.parametrize("middleware, result", test_data_class) async def test_webgear_rtc_custom_middleware(middleware, result): """ @@ -537,7 +526,6 @@ async def test_webgear_rtc_custom_middleware(middleware, result): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes @@ -586,7 +574,6 @@ async def test_webgear_rtc_routes(): platform.python_version_tuple()[:2] >= ("3", "11"), reason="Random Failures!", ) -@pytest.mark.asyncio async def test_webgear_rtc_routes_validity(): """ Test WebGear_RTC Routes From 993cdea66b0e4ddba20d2f506abef69a43fc0c1a Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 17 May 2024 01:11:30 +0530 Subject: [PATCH 22/81] =?UTF-8?q?=F0=9F=A9=B9=20NetGear=5FAsync:=20Fixed?= =?UTF-8?q?=20event=20loop=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️Modified `__init__` method to handle event loop more robustly: - Try to get the running event loop using `asyncio.get_running_loop()` - If no running event loop found, create a new one with `asyncio.new_event_loop()` - Log if creating a new event loop - 🧑‍💻 Changed launch method to use `self.loop.create_task()` instead of `asyncio.ensure_future()` - Ensures the task is created using the correct event loop instance. --- vidgear/gears/asyncio/netgear_async.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index d91c335ea..a785cda7c 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -304,7 +304,12 @@ def __init__( import_dependency_safe("uvloop", error="log") # Retrieve event loop and assign it - self.loop = asyncio.get_event_loop() + try: + self.loop = asyncio.get_running_loop() + except RuntimeError: + # otherwise create one + logger.critical("No running event loop found. Creating a new one.") + self.loop = asyncio.new_event_loop() # create asyncio queue if bidirectional mode activated self.__queue = asyncio.Queue() if self.__bi_mode else None # log eventloop for debugging @@ -331,7 +336,7 @@ def launch(self): "Creating NetGear_Async asynchronous server handler!" ) # create task for Server Handler - self.task = asyncio.ensure_future(self.__server_handler()) + self.task = self.loop.create_task(self.__server_handler()) # return instance return self From d035e4acde892673c8e734ac96f4da153a735433 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 18 May 2024 19:08:48 +0530 Subject: [PATCH 23/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20NetGear=5FAsync:?= =?UTF-8?q?=20Fix=20event=20loop=20initialization=20on=20Windows=20platfor?= =?UTF-8?q?ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Moved the event loop initialization code to an earlier point before setting event loop policy to ensure it is set up correctly before selecting `WindowsSelectorEventLoop` policy. - 💬 Background: On Windows, vidgear requires the `WindowsSelectorEventLoop`, but Python 3.8 and above defaults to the `ProactorEventLoop` which is not compatible. - 🔥 Removed redundant python version check to set `WindowsSelectorEventLoop` policy, as minimum supported version is already `3.8`. - 💡 Updated code comments. --- vidgear/gears/asyncio/netgear_async.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index a785cda7c..a50c75f4b 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -288,13 +288,20 @@ def __init__( # add server task handler self.task = None + # Retrieve event loop and assign it + try: + self.loop = asyncio.get_running_loop() + except RuntimeError: + # otherwise create one + logger.critical("No running event loop found. Creating a new one.") + self.loop = asyncio.new_event_loop() + # Setup and assign event loop policy if platform.system() == "Windows": - # On Windows, VidGear requires the ``WindowsSelectorEventLoop``, and this is - # the default in Python 3.7 and older, but new Python 3.8, defaults to an - # event loop that is not compatible with it. Thereby, we had to set it manually. - if sys.version_info[:2] >= (3, 8): - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # On Windows, VidGear 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. + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) else: if not (uvloop is None): # Latest uvloop eventloop is only available for UNIX machines. @@ -303,13 +310,6 @@ def __init__( # log if not present import_dependency_safe("uvloop", error="log") - # Retrieve event loop and assign it - try: - self.loop = asyncio.get_running_loop() - except RuntimeError: - # otherwise create one - logger.critical("No running event loop found. Creating a new one.") - self.loop = asyncio.new_event_loop() # create asyncio queue if bidirectional mode activated self.__queue = asyncio.Queue() if self.__bi_mode else None # log eventloop for debugging From d8e0149e287403174a2d66637b10ecf3d0a3d804 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 19 May 2024 13:52:03 +0530 Subject: [PATCH 24/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Refactor=20docs=20?= =?UTF-8?q?and=20site=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐛 Replaced buggy kofi widget with a button image in index.md - 🏗️ Remove script tags from main.html and use a custom hook for adding JS - 🔥 Remove site.webmanifest file - 💄 Update mkdocs.yml with new configuration settings: - Set edit_uri for GitHub edit links - Add new theme features like content actions, tooltips, etc. - Update palette settings for light/dark mode - Enable new markdown extensions - Add custom JS hook for adding scripts on certain pages - ♻️ Other minor tweaks and cleanups --- docs/gears/streamgear/introduction.md | 1 + docs/index.md | 2 +- docs/overrides/hooks/js_hook.py | 45 +++++++++++++++++++++++++++ docs/overrides/main.html | 31 ++++++++---------- docs/overrides/site.webmanifest | 1 - mkdocs.yml | 36 ++++++++++++--------- 6 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 docs/overrides/hooks/js_hook.py delete mode 100644 docs/overrides/site.webmanifest diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 0bb95304a..8a12c3b12 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -18,6 +18,7 @@ limitations under the License. =============================================== --> + # StreamGear API diff --git a/docs/index.md b/docs/index.md index 30e882753..871d87da4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -119,7 +119,7 @@ You can be a [**Stargazer** :star2:{ .heart }][stargazer] by starring us on Gith It is something I am doing with my own free time. But so much more needs to be done, and I need your help to do this. For just the price of a cup of coffee, you can make a difference :slight_smile: - +Buy Me a Coffee at ko-fi.com   diff --git a/docs/overrides/hooks/js_hook.py b/docs/overrides/hooks/js_hook.py new file mode 100644 index 000000000..27b72a194 --- /dev/null +++ b/docs/overrides/hooks/js_hook.py @@ -0,0 +1,45 @@ +""" +=============================================== +vidgear library source-code is deployed under the Apache 2.0 License: + +Copyright (c) 2019 Abhishek Thakur(@abhiTronix) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +=============================================== +""" + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.pages import Page + +js_scripts = """ +, + + + +""" + + +# Add per-file custom javascripts. +def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files): + if not ( + page.file.src_uri + in ["gears/stabilizer/overview.md", "gears/streamgear/introduction.md"] + ): + return + + # Replace markdown + js scripts + comment, content = markdown.split("-->") + modified_markdown = comment + "-->\n" + js_scripts + content + + # Return modified + return modified_markdown diff --git a/docs/overrides/main.html b/docs/overrides/main.html index a610db256..a5a6e9d07 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,33 +1,37 @@ {% extends "base.html" %} + {% block extrahead %} {% set title = config.site_name %} {% if page and page.meta and page.meta.title %} {% set title = title ~ " - " ~ page.meta.title %} {% elif page and page.title and not page.is_homepage %} -{% set title = title ~ " - " ~ page.title | striptags %} +{% set title = title ~ " - " ~ page.title %} {% endif %} {% set image = config.site_url ~ '/assets/images/banner_link.png' %} - - - - - - - - + + + + + + + + {% endblock %} + {% block announce %} {% include ".icons/material/message-alert.svg" %} We're excited to announce our new Deffcode library, which will be integrated with VidGear soon. We value your feedback and would love to hear your thoughts! {% endblock %} + {% block outdated %} You're not viewing the latest version. Click here to go to latest. {% endblock %} + {% block content %} {{ super() }}
@@ -40,15 +44,6 @@
{% endblock %} -{% block libs %} - - - - -{% endblock %} - {% include ".icons/material/message-alert.svg" %} We're excited to announce our new Deffcode library, + {% include ".icons/fontawesome/solid/bullhorn.svg" %} We're excited to announce our + new Deffcode + library, which will be integrated with VidGear soon. We value your feedback and would love to hear your thoughts! {% endblock %} diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index d39dba131..276c4020f 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -61,7 +61,7 @@ Let's compare a bare-minimum python code for extracting frames out of any Webcam === "OpenCV VideoCapture Class" - ```python hl_lines="5 11 14-15 33" + ```python hl_lines="5 11 14 33" # import required libraries import cv2 @@ -99,7 +99,7 @@ Let's compare a bare-minimum python code for extracting frames out of any Webcam === "VidGear's CamGear API" - ```python hl_lines="6 12 15-16 34" + ```python hl_lines="6 12 15 34" # import required libraries from vidgear.gears import CamGear import cv2 diff --git a/mkdocs.yml b/mkdocs.yml index d046fd715..e8f8f9759 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,7 +55,7 @@ theme: palette: - media: "(prefers-color-scheme)" toggle: - icon: material/weather-sunny-off + icon: material/weather-hazy name: Switch to light mode # Light mode - media: "(prefers-color-scheme: light)" @@ -72,7 +72,7 @@ theme: accent: orange toggle: icon: material/weather-night - name: Switch to light mode + name: Switch to system mode font: text: Source Sans 3 code: Fira Code From 6285688e5d2d4c924ca9f37095c39ab130004c63 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 20 May 2024 20:30:54 +0530 Subject: [PATCH 27/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20PiGear:=20Modify?= =?UTF-8?q?=20`PiGear`=20class=20behavior=20when=20`enforce=5Flegacy=5Fpic?= =?UTF-8?q?amera=3DTrue`=20on=20unsupported=20system?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Instead of silently disabling `picamera2` API directly, PiGear now raises an error if `picamera` is unavailable or unsupported - 🥅 Prevented incorrect initialization of `PiGear` class on unsupported 64-bit OS systems. Docs: - 📝 Add failure warning in various docs about `picamera` incompatibility on 64-bit OS --- .../netgear/advanced/bidirectional_mode.md | 2 ++ docs/gears/netgear/advanced/multi_client.md | 4 +++ docs/gears/netgear/advanced/multi_server.md | 4 +++ .../advanced/bidirectional_mode.md | 2 ++ docs/gears/pigear/usage.md | 6 +++++ docs/gears/videogear/usage.md | 4 +++ docs/help/pigear_ex.md | 27 ++++++++++++------- docs/help/streamgear_ex.md | 4 +++ docs/help/webgear_ex.md | 2 ++ docs/help/webgear_rtc_ex.md | 2 ++ vidgear/gears/pigear.py | 10 ++++--- 11 files changed, 54 insertions(+), 13 deletions(-) diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index 0deaef448..8cbcb56fd 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -374,6 +374,8 @@ Now, Open the terminal on another Server System _(a Raspberry Pi with Camera Mod !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="25-30" diff --git a/docs/gears/netgear/advanced/multi_client.md b/docs/gears/netgear/advanced/multi_client.md index c41241bc9..101465cb6 100644 --- a/docs/gears/netgear/advanced/multi_client.md +++ b/docs/gears/netgear/advanced/multi_client.md @@ -591,6 +591,8 @@ Now, Open the terminal on a Server System _(with a webcam connected to it at ind !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="47-60" @@ -913,6 +915,8 @@ Now, Open the terminal on a Server System _(with a webcam connected to it at ind !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="19 48-64" diff --git a/docs/gears/netgear/advanced/multi_server.md b/docs/gears/netgear/advanced/multi_server.md index 205c31c57..c7ca3914a 100644 --- a/docs/gears/netgear/advanced/multi_server.md +++ b/docs/gears/netgear/advanced/multi_server.md @@ -737,6 +737,8 @@ Finally, Open the terminal on another Server System _(this time a Raspberry Pi w !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="50" @@ -1075,6 +1077,8 @@ Finally, Open the terminal on another Server System _(this time a Raspberry Pi w !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="20 46-54" diff --git a/docs/gears/netgear_async/advanced/bidirectional_mode.md b/docs/gears/netgear_async/advanced/bidirectional_mode.md index b5b38c961..0ba838c03 100644 --- a/docs/gears/netgear_async/advanced/bidirectional_mode.md +++ b/docs/gears/netgear_async/advanced/bidirectional_mode.md @@ -409,6 +409,8 @@ Now, Open the terminal on another Server System _(a Raspberry Pi with Camera Mod !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 4cbae0e1e..1233b60b2 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -41,6 +41,8 @@ Following is the bare-minimum code you need to get started with PiGear API: !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params/#b-user-defined-parameters) user-defined optional parameter boolean attribute." ??? danger "Disabling common `libcamera` API messages in silent mode." @@ -204,6 +206,8 @@ stream.stop() It is advised to enable logging(`logging=True`) to see which backend is being used. + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params/#b-user-defined-parameters) user-defined optional parameter boolean attribute." PiGear also supports almost every parameter available within [`picamera`](https://picamera.readthedocs.io/en/release-1.13/api_camera.html) python library. These parameters can be easily applied to the source stream in PiGear API through its [`options`](../params/#options) dictionary parameter by formatting them as its attributes. The complete usage example is as follows: @@ -404,6 +408,8 @@ PiGear can be easily used with WriteGear API directly without any compatibility It is advised to enable logging(`logging=True`) to see which backend is being used. + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params/#b-user-defined-parameters) user-defined optional parameter boolean attribute." ```python linenums="1" diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index 3ef8bcb32..6ddfb1616 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -82,6 +82,8 @@ Following is the bare-minimum code you need to access PiGear API with VideoGear: !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params) user-defined optional parameter boolean attribute." !!! warning "Make sure to [complete Raspberry Pi Camera Hardware-specific settings](https://www.raspberrypi.com/documentation/accessories/camera.html#installing-a-raspberry-pi-camera) prior using this API, otherwise nothing will work." @@ -296,6 +298,8 @@ The usage example of VideoGear API with Variable Camera Properties is as follows !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="8-13" diff --git a/docs/help/pigear_ex.md b/docs/help/pigear_ex.md index 74e6ac5ea..e7b15900c 100644 --- a/docs/help/pigear_ex.md +++ b/docs/help/pigear_ex.md @@ -215,7 +215,7 @@ limitations under the License. # formulate initial configurational parameters # set brightness to -0.5 (dark) - options = "controls": {"Brightness": -0.5} + options = {"controls": {"Brightness": -0.5}} # open pi video stream with these parameters stream = PiGear(logging=True, **options).start() @@ -262,6 +262,8 @@ limitations under the License. It is advised to enable logging(`logging=True`) to see which backend is being used. + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params/#options) optional parameter boolean attribute." In this example we will set initial Camera Module's `brightness` value `80` _(brighter)_, and will change it `30` _(darker)_ when ++"Z"++ key is pressed at runtime: @@ -348,23 +350,28 @@ limitations under the License. !!! info "The PiGear API can accurately differentiate between USB and Raspberry Pi camera modules by utilizing the camera's metadata." - In this example, we will select the Camera Module connected at index `1` on the Raspberry Pi as the primary source for extracting frames in PiGear API: + In this example, we will select the USB Camera connected at index `1` on the Raspberry Pi as the primary source for extracting frames in PiGear API: + + ??? failure "Limited support for USB Cameras" + + This example also works with USB Cameras, However: - !!! alert "This example assumes a Camera Module is connected at index `1`, and some other camera connected at index `0` on your Raspberry Pi." + - Users should assume that features such as: **Camera controls** (`"controls"`), **Transformations** (`"transform"`), **Queue** (`"queue"`) , and **Buffer Count** (`"buffer_count"`) that are supported on Raspberry Pi cameras, and so forth, are not available on USB Cameras. + - Hot-plugging of USB cameras is also **NOT** supported - PiGear API should be completely shut down and restarted when cameras are added or removed. - ```python linenums="1" hl_lines="19" + !!! alert "This example assumes a USB Camera is connected at index `1`, and some other camera connected at index `0` on your Raspberry Pi." + + ```python linenums="1" hl_lines="15" # import required libraries from vidgear.gears import PiGear from libcamera import Transform import cv2 # formulate various Picamera2 API - # configurational parameters + # configurational parameters for USB camera options = { - "queue": True, - "buffer_count": 4, - "controls": {"Brightness": 0.5, "ExposureValue": 2.0}, - "transform": Transform(hflip=1), + "sensor": {"output_size": (480, 320)}, # will override `resolution` + "format": "RGB888" # BGR format for this example "auto_align_output_config": True, # auto-align camera configuration } @@ -407,6 +414,8 @@ limitations under the License. It is advised to enable logging(`logging=True`) to see which backend is being used. + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params/#options) optional parameter boolean attribute." In this example, we will select the Camera Module connected at index `1` on the Raspberry Pi as the primary source for extracting frames in PiGear API: diff --git a/docs/help/streamgear_ex.md b/docs/help/streamgear_ex.md index e1fc6a81e..253b044c7 100644 --- a/docs/help/streamgear_ex.md +++ b/docs/help/streamgear_ex.md @@ -108,6 +108,8 @@ In this example, we will be Live-Streaming video-frames from Raspberry Pi _(with !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params) user-defined optional parameter boolean attribute." ```python linenums="1" @@ -242,6 +244,8 @@ In this example, we will be Live-Streaming video-frames from Raspberry Pi _(with !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../params) user-defined optional parameter boolean attribute." ```python linenums="1" diff --git a/docs/help/webgear_ex.md b/docs/help/webgear_ex.md index 232af93bb..c3dc6c447 100644 --- a/docs/help/webgear_ex.md +++ b/docs/help/webgear_ex.md @@ -77,6 +77,8 @@ Here's a bare-minimum example of using WebGear API with the Raspberry Pi camera !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="21" diff --git a/docs/help/webgear_rtc_ex.md b/docs/help/webgear_rtc_ex.md index 55f1bb78e..5caa4b83b 100644 --- a/docs/help/webgear_rtc_ex.md +++ b/docs/help/webgear_rtc_ex.md @@ -74,6 +74,8 @@ Here's a bare-minimum example of using WebGear_RTC API with the Raspberry Pi cam !!! tip "It is advised to enable logging(`logging=True`) to see which backend is being used." + !!! failure "The `picamera` library is built on the legacy camera stack that is NOT _(and never has been)_ supported on 64-bit OS builds." + !!! note "You could also enforce the legacy picamera API backend in PiGear by using the [`enforce_legacy_picamera`](../../gears/pigear/params) user-defined optional parameter boolean attribute." ```python linenums="1" hl_lines="18" diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index 09e11056e..1a77b0230 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -151,12 +151,14 @@ def __init__( logger.critical( "Enforcing legacy picamera API for this run. picamera2 API access will be disabled!" ) + # disable picamera2 + picamera2 = None else: - logger.warning( - "`picamera` is unavailable on this system, `enforce_legacy_picamera` will be discarded!" + # raise error otherwise + logger.error( + "`picamera` is unavailable or unsupported on this system, `enforce_legacy_picamera` will be discarded!" ) - # disable picamera2 - picamera2 = None + import_dependency_safe("picamera") if picamera2: # handle logging From e655f904eae9862083765d2ca65ec55c039e7606 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 21 May 2024 22:04:15 +0530 Subject: [PATCH 28/81] =?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 29/81] =?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 30/81] =?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 31/81] =?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 32/81] =?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 33/81] =?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 34/81] =?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 35/81] =?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 36/81] =?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 37/81] =?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 38/81] =?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 39/81] =?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 40/81] =?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 41/81] =?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 42/81] =?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 43/81] =?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 44/81] =?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 45/81] =?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 46/81] =?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 47/81] =?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 48/81] =?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 49/81] =?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 50/81] =?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 51/81] =?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 52/81] =?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 53/81] =?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 54/81] =?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 55/81] =?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 56/81] =?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) From 6a40e8ddde2d9614b3e540caf7d11bf4f327db28 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 8 Jun 2024 14:24:44 +0530 Subject: [PATCH 57/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20WebGear=5FRTC:=20Imp?= =?UTF-8?q?roved=20connection=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Only close peer connections that are not already in the "closed" state. - 🎨 Logged the ICE connection state change only when it's not in the "failed" state, reducing unnecessary logging. --- vidgear/gears/asyncio/webgear_rtc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 4b3c3e4a4..08e3db72a 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -559,7 +559,6 @@ async def __offer(self, request): # track ICE connection state changes @pc.on("iceconnectionstatechange") async def on_iceconnectionstatechange(): - logger.debug("ICE connection state is %s" % pc.iceConnectionState) if pc.iceConnectionState == "failed": logger.error("ICE connection state failed.") # check if Live Broadcasting is enabled @@ -567,6 +566,8 @@ async def on_iceconnectionstatechange(): # if not, close connection. await pc.close() self.__pcs.discard(pc) + else: + logger.debug("ICE connection state is %s" % pc.iceConnectionState) # Change the remote description associated with the connection. await pc.setRemoteDescription(offer) @@ -628,7 +629,9 @@ async def __reset_connections(self, request): logger.critical("Resetting Server") # close old peer connections if parameter != 0: # disable if specified explicitly - coros = [pc.close() for pc in self.__pcs] + coros = [ + pc.close() for pc in self.__pcs if pc.iceConnectionState != "closed" + ] await asyncio.gather(*coros) self.__pcs.clear() await self.__default_rtc_server.reset() @@ -645,7 +648,9 @@ async def __lifespan(self, context): # close Video Server self.shutdown() # collects peer RTC connections - coros = [pc.close() for pc in self.__pcs] + coros = [ + pc.close() for pc in self.__pcs if pc.iceConnectionState != "closed" + ] await asyncio.gather(*coros) self.__pcs.clear() From 8667e5e2e6319fb1119205c76e8945c546c1e7d4 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 9 Jun 2024 00:07:24 +0530 Subject: [PATCH 58/81] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Improved=20WebGear?= =?UTF-8?q?=20RTC=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 👷 Added a pytest fixture `event_loop_policy` to set the WindowsSelectorEventLoopPolicy on Windows platforms. - ⚡️Replace the use of `async_asgi_testclient` with `httpx.AsyncClient` and `httpx.ASGITransport` for testing the ASGI application. - 🥅 Update the test cases to use the `httpx.AsyncClient` correctly: - Use the `content` parameter instead of `data` when sending POST requests with JSON payloads. - Use the `post` method instead of `get` when sending WebRTC offers, as `get` do not support `content` parameter. - 🔥 Remove the `pytest.mark.skipif` conditions related to Python version 3.11 and above, as the compatibility issues have been addressed. - 🗑️ Add the `pytest.mark.asyncio(scope="module")` marker to the test functions to ensure proper handling of asynchronous tests. --- .../asyncio_tests/test_netgear_async.py | 2 +- .../asyncio_tests/test_webgear_rtc.py | 105 +++++++++--------- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py index 27546cdba..b094cf836 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -43,7 +43,7 @@ @pytest.fixture(scope="module") def event_loop_policy(request): if platform.system() == "Windows": - logger.critical("Setting WindowsSelectorEventLoopPolicy!!!") + logger.info("Setting WindowsSelectorEventLoopPolicy!") return asyncio.WindowsSelectorEventLoopPolicy() else: return asyncio.DefaultEventLoopPolicy() diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py index fbeaa893c..b9ea2ab87 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -33,7 +33,7 @@ from starlette.responses import PlainTextResponse from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware -from async_asgi_testclient import TestClient +from httpx import AsyncClient, ASGITransport from aiortc import ( MediaStreamTrack, RTCPeerConnection, @@ -53,7 +53,14 @@ logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) -pytestmark = pytest.mark.asyncio(scope="module") + +@pytest.fixture(scope="module") +def event_loop_policy(request): + if platform.system() == "Windows": + logger.info("Setting WindowsSelectorEventLoopPolicy!") + return asyncio.WindowsSelectorEventLoopPolicy() + else: + return asyncio.DefaultEventLoopPolicy() def return_testvideo_path(): @@ -228,10 +235,7 @@ def stop(self): ] -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") @pytest.mark.parametrize("source, stabilize, colorspace, time_delay", test_data) async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ @@ -245,7 +249,9 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): time_delay=time_delay, logging=True, ) - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 response_404 = await client.get("/test") @@ -253,15 +259,15 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -297,10 +303,7 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): ] -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") @pytest.mark.parametrize("options", test_data) async def test_webgear_rtc_options(options): """ @@ -309,7 +312,9 @@ async def test_webgear_rtc_options(options): web = None try: web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 if ( @@ -319,15 +324,15 @@ async def test_webgear_rtc_options(options): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -369,7 +374,9 @@ async def test_webpage_reload(options): web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: # run webgear_rtc - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 @@ -377,15 +384,15 @@ async def test_webpage_reload(options): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -406,15 +413,15 @@ async def test_webpage_reload(options): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -446,10 +453,7 @@ async def test_webpage_reload(options): ] -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") @pytest.mark.parametrize("stream_class, result", test_stream_classes) async def test_webgear_rtc_custom_stream_class(stream_class, result): """ @@ -463,7 +467,9 @@ async def test_webgear_rtc_custom_stream_class(stream_class, result): } try: web = WebGear_RTC(logging=True, **options) - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 response_404 = await client.get("/test") @@ -471,15 +477,15 @@ async def test_webgear_rtc_custom_stream_class(stream_class, result): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -499,10 +505,7 @@ async def test_webgear_rtc_custom_stream_class(stream_class, result): ] -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") @pytest.mark.parametrize("middleware, result", test_data_class) async def test_webgear_rtc_custom_middleware(middleware, result): """ @@ -511,7 +514,9 @@ async def test_webgear_rtc_custom_middleware(middleware, result): try: web = WebGear_RTC(source=return_testvideo_path(), logging=True) web.middleware = middleware - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 web.shutdown() @@ -522,10 +527,7 @@ async def test_webgear_rtc_custom_middleware(middleware, result): pytest.xfail(str(e)) -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes @@ -542,7 +544,9 @@ async def test_webgear_rtc_routes(): web.routes.append(Route("/hello", endpoint=hello_webpage)) # test - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: response = await client.get("/") assert response.status_code == 200 response_hello = await client.get("/hello") @@ -550,15 +554,15 @@ async def test_webgear_rtc_routes(): (offer_pc, data) = await get_RTCPeer_payload() response_rtc_answer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) await offer_pc.setRemoteDescription(answer) - response_rtc_offer = await client.get( + response_rtc_offer = await client.post( "/offer", - data=data, + content=data, headers={"Content-Type": "application/json"}, ) assert response_rtc_offer.status_code == 200 @@ -570,10 +574,7 @@ async def test_webgear_rtc_routes(): pytest.fail(str(e)) -@pytest.mark.skipif( - platform.python_version_tuple()[:2] >= ("3", "11"), - reason="Random Failures!", -) +@pytest.mark.asyncio(scope="module") async def test_webgear_rtc_routes_validity(): """ Test WebGear_RTC Routes @@ -589,7 +590,9 @@ async def test_webgear_rtc_routes_validity(): # modify route web.routes.clear() # test - async with TestClient(web()) as client: + async with AsyncClient( + transport=ASGITransport(app=web()), base_url="http://testserver" + ) as client: pass except Exception as e: if isinstance(e, (RuntimeError, MediaStreamError)): From 49cc04fa25607999df6fd062c9c813f56beebad1 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 9 Jun 2024 11:50:13 +0530 Subject: [PATCH 59/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20StreamGear:=20Update?= =?UTF-8?q?d=20support=20of=20Stream=20Copy=20in=20Single=20Source=20mode.?= =?UTF-8?q?=20(Fixes=20#396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ Ignore the stream copy parameter if Real-time Frames Mode or Custom Streams are enabled, and log appropriate warnings. - ⚡️Updated the handling of the `-acodec` parameter: - Use the default `aac` codec for Custom Streams. - Use stream copy (`-acodec copy`) for the input video's audio stream if Custom Streams are not enabled. - ♻️ Refactor the handling of the `-livestream` parameter to ensure it is only enabled for the Real-time Frames Mode. - ♻️ Refactor the video and audio bitrate assignment to skip the assignment when stream copy is enabled. - 🔊 Updated log message for `-clear_prev_assets` parameter. - ✏️ Fix a typo in comments. Docs: - 📝 Add a new tip box explaining the benefits of using stream copy (`-vcodec copy`) in the Single Source Mode for faster transcoding of HLS/DASH streams. - 💬 Highlight the limitations of stream copy, such as incompatibility with Real-time Frames Mode and Custom Streams, which require re-encoding of frames. - 💬 Clarify that the audio stream copy (`-acodec copy`) is automatically applied when using the input video's audio stream. - 🎨 Fixed various issues like typos, formatting errors, code highlighting issues, and grammar inconsistencies. --- docs/gears/pigear/usage.md | 3 -- docs/gears/streamgear/params.md | 17 ++++++--- docs/gears/streamgear/rtfm/usage.md | 14 ++++---- docs/gears/streamgear/ssm/usage.md | 12 +++++-- vidgear/gears/streamgear.py | 56 +++++++++++++++++------------ 5 files changed, 60 insertions(+), 42 deletions(-) diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 1233b60b2..fb3b36d7e 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -55,21 +55,18 @@ Following is the bare-minimum code you need to get started with PiGear API: === ":material-linux: Linux" ```sh - # path to file export LIBCAMERA_LOG_LEVELS=2 ``` === ":fontawesome-brands-windows: Windows (Powershell)" ```powershell - # path to file $Env:LIBCAMERA_LOG_LEVELS=2 ``` === ":material-apple: MacOS" ```sh - # path to file export LIBCAMERA_LOG_LEVELS=2 ``` diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 3512b074d..5e272a386 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -218,6 +218,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St # 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 "Ensure the given video URL uses a protocol supported by the installed FFmpeg _(verify with `ffmpeg -protocols` terminal command)_." @@ -269,7 +270,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St   -* **`-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: +* **`-livestream`** _(bool)_: ***(optional)*** specifies whether to enable **Low-latency Live-Streaming :material-video-wireless-outline:** in [**Real-time Frames Mode**](../rtfm/overview) 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: !!! warning "The `-livestream` optional parameter is **NOT** supported in [Single-Source mode](../ssm/overview)." @@ -338,9 +339,9 @@ StreamGear API provides some exclusive internal parameters to easily generate St   -* **`-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: +* **`-clear_prev_assets`** _(bool)_: ***(optional)*** This parameter specifies whether to remove/delete all previous copies of StreamGear assets files for selected [`format`](#format) _(i.e., manifest (`mpd`) in DASH, playlist (`mu38`) in HLS, and respective streaming chunks (`.ts`,`.m4s`), etc.)_ present at the path specified by the [`output`](#output) parameter. The default value is `False`. It can be enabled as follows: - !!! info "Additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." + !!! info "Additional segments _(such as `.webm`, `.mp4` chunks)_ are also removed automatically." ```python # delete all previous assets @@ -381,9 +382,13 @@ 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." +???+ tip "Faster Transcoding with Stream Copy in Single Source Mode" + + For faster transcoding of input video, utilize Stream copy (`-vcodec copy`) as the input video encoder in the [**Single-Source Mode**](../ssm/overview) for creating HLS/DASH chunks of the primary stream efficiently. However, consider the following points: -!!! info "Similarly, supported audio/video demuxers and filters depend on the FFmpeg binaries in use." + - :warning: Stream copy is **NOT** compatible with [**Real-time Frames Mode**](../rtfm/overview), as this mode necessitates re-encoding of incoming frames. Therefore, the `-vcodec copy` parameter will be ignored. + - :warning: Stream copying **NOT** compatible with Custom Streams ([`-streams`](#a-exclusive-parameters)), which also require re-encoding for each additional stream. Consequently, the `-vcodec copy` parameter will be ignored. + - When using the audio stream from the input video, the Audio Stream copy (`-acodec copy`) encoder will be automatically applied. ```sh # for checking encoder @@ -392,6 +397,8 @@ ffmpeg -encoders # use `ffmpeg.exe -encoders` on windows ffmpeg -decoders # use `ffmpeg.exe -decoders` on windows ``` +!!! info "Similarly, supported audio/video demuxers and filters depend on the FFmpeg binaries in use." +   ## **`logging`** diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 55e84ae5a..5833545bb 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -30,10 +30,10 @@ 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] In this mode, Stream copy (`-vcodec copy`) encoder is unsupported as it 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" +??? 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 `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. @@ -515,7 +515,7 @@ To generate Secondary Streams, add each desired resolution and bitrate/framerate === "DASH" - ```python linenums="1" hl_lines="12-14" + ```python linenums="1" hl_lines="11-15" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -571,7 +571,7 @@ To generate Secondary Streams, add each desired resolution and bitrate/framerate === "HLS" - ```python linenums="1" hl_lines="12-14" + ```python linenums="1" hl_lines="11-15" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -1038,11 +1038,9 @@ In this example, we will be using `h264_vaapi` as our Hardware Encoder and speci !!! 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." -!!! warning "Stream copy (`-vcodec copy`) is not compatible with this Mode as it requires re-encoding of incoming frames." +???+ info "Checking VAAPI Support for Hardware Encoding" -??? 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: + To use **VAAPI** (Video Acceleration API) as a hardware encoder in this example, follow these steps to ensure your FFmpeg supports VAAPI: ```sh ffmpeg -hide_banner -encoders | grep vaapi diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 4f1753113..2f881ef49 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -27,11 +27,17 @@ 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 NOTICES for `v0.3.3` and above" +??? 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. +??? tip "Faster Transcoding of Primary Stream with Stream Copy in Single Source Mode" + + For faster transcoding of input video in this mode, utilize Stream copy (`-vcodec copy`) as the input video encoder for creating HLS/DASH chunks of the primary stream efficiently. However, consider the following points: + + - :warning: Stream copying **NOT** compatible with Custom Streams ([`-streams`](../../params/#a-exclusive-parameters)), which require re-encoding for each additional stream. Therefore, the `-vcodec copy` parameter will be ignored. + - When using the audio stream from the input video, the Audio Stream copy (`-acodec copy`) encoder will be automatically applied. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" @@ -106,7 +112,7 @@ To generate Secondary Streams, add each desired resolution and bitrate/framerate === "DASH" - ```python linenums="1" hl_lines="6-12" + ```python linenums="1" hl_lines="7-12" # import required libraries from vidgear.gears import StreamGear @@ -130,7 +136,7 @@ To generate Secondary Streams, add each desired resolution and bitrate/framerate === "HLS" - ```python linenums="1" hl_lines="6-12" + ```python linenums="1" hl_lines="7-12" # import required libraries from vidgear.gears import StreamGear diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 7fd89a1ae..25e496231 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -214,8 +214,10 @@ def __init__( 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." + clear_assets and logger.info( + "The `-clear_prev_assets` parameter is enabled successfully. All previous StreamGear API assets for `{}` format will be removed for this run.".format( + self.__format.upper() + ) ) else: # reset improper values @@ -223,7 +225,7 @@ def __init__( # handle whether to livestream? livestreaming = self.__params.pop("-livestream", False) - if isinstance(livestreaming, bool): + if isinstance(livestreaming, bool) and livestreaming: # NOTE: `livestream` is only available with real-time mode. self.__livestreaming = livestreaming if not (self.__video_source) else False if self.__video_source: @@ -471,7 +473,8 @@ def __PreProcess(self, channels=0, rgb=False): # in Real-time Frames Mode output_parameters["-vcodec"] = ( default_codec - if output_vcodec == "copy" and not (self.__video_source) + if output_vcodec == "copy" + and (not (self.__video_source) or "-streams" in self.__params) else output_vcodec ) # enforce compatibility with stream copy @@ -482,7 +485,10 @@ 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 re-encoding of incoming frames. Discarding the `-vcodec copy` parameter!" + "Stream copy is not compatible with Real-time Frames Mode as it require re-encoding of incoming frames. Discarding the `-vcodec copy` parameter!" + ) + ("-streams" in self.__params) and logger.error( + "Stream copying is incompatible with Custom Streams as it require re-encoding for each additional stream. Discarding the `-vcodec copy` parameter!" ) # log warnings for these parameters self.__params.pop("-vf", False) and logger.warning( @@ -532,9 +538,7 @@ def __PreProcess(self, channels=0, rgb=False): ) ] = self.__audio # assign audio codec - output_parameters["-acodec"] = self.__params.pop( - "-acodec", "aac" if isinstance(self.__audio, list) else "copy" - ) + output_parameters["-acodec"] = self.__params.pop("-acodec", "aac") output_parameters["a_bitrate"] = bitrate # temporary handler output_parameters["-core_audio"] = ( ["-map", "1:a:0"] if self.__format == "dash" else [] @@ -549,12 +553,14 @@ def __PreProcess(self, channels=0, rgb=False): elif self.__video_source: bitrate = validate_audio(self.__ffmpeg, source=self.__video_source) if bitrate: - logger.info("Input Video's audio source will be used for this run.") + 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["-acodec"] = self.__params.pop( + "-acodec", + "aac" if ("-streams" in self.__params) else "copy", ) - output_parameters["a_bitrate"] = bitrate # temporary handler + if output_parameters["-acodec"] != "copy": + output_parameters["a_bitrate"] = bitrate # temporary handler else: logger.info( "No valid audio source available in the input video. Disabling audio while generating streams." @@ -602,7 +608,7 @@ def __PreProcess(self, channels=0, rgb=False): ), "[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): @@ -652,22 +658,26 @@ def __handle_streams(self, input_params, output_params): 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) + if output_params["-vcodec"] != "copy": + output_params["-s:v:0"] = "{}x{}".format( + self.__inputwidth, self.__inputheight + ) # assign default output video-bitrate if "-b:v:0" in self.__params: # prevent duplicates del self.__params["-b:v:0"] - output_params["-b:v:0"] = ( - str( - get_video_bitrate( - int(self.__inputwidth), - int(self.__inputheight), - self.__sourceframerate, - bpp, + if output_params["-vcodec"] != "copy": + output_params["-b:v:0"] = ( + str( + get_video_bitrate( + int(self.__inputwidth), + int(self.__inputheight), + self.__sourceframerate, + bpp, + ) ) + + "k" ) - + "k" - ) # assign default output audio-bitrate if "-b:a:0" in self.__params: From 2af68cfceb403f0f71c9e49c9d14057069956188 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 9 Jun 2024 13:05:21 +0530 Subject: [PATCH 60/81] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Core:=20Refactored?= =?UTF-8?q?=20colorspace=20handling=20in=20videocapture=20gears.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Instead of raising an exception, log a warning message and discard the invalid colorspace value. - 🎨 Consolidate the colorspace logging statement to a single line using a ternary operation. CamGear API - 🔥 Remove the check for GStreamer support, as it is not being used currently (marked as a TODO). - 🔊 Improve the readability of the livestream warning log. Maintenance - 🎨 Applied short-circuiting to simplify code across various APIs. - 🎨 Remove unnecessary parentheses and unnecessary type checks. - 🔧 Removed unused imports --- vidgear/gears/camgear.py | 70 ++++++++++++++----------------------- vidgear/gears/helper.py | 27 ++++++-------- vidgear/gears/pigear.py | 22 +++--------- vidgear/gears/screengear.py | 36 +++++++------------ vidgear/gears/stabilizer.py | 25 ++++++------- vidgear/gears/streamgear.py | 1 - vidgear/gears/writegear.py | 1 - 7 files changed, 68 insertions(+), 114 deletions(-) diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index 87d459307..8c2faec35 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -243,11 +243,11 @@ def __init__( # check if Stream-Mode is ON (True) if stream_mode: - # check GStreamer backend support - gst_support = check_gstreamer_support(logging=logging) + # TODO: check GStreamer backend support + # gst_support = check_gstreamer_support(logging=self.__logging) # handle special Stream Mode parameters stream_resolution = get_supported_resolution( - options.pop("STREAM_RESOLUTION", "best"), logging=logging + options.pop("STREAM_RESOLUTION", "best"), logging=self.__logging ) # handle Stream-Mode if not (yt_dlp is None): @@ -266,17 +266,16 @@ def __init__( ) # initialize YT_backend ytbackend = YT_backend( - source_url=source, logging=logging, **yt_stream_params + source_url=source, logging=self.__logging, **yt_stream_params ) if ytbackend: # save video metadata self.ytv_metadata = ytbackend.meta_data # handle live-streams - if ytbackend.is_livestream: - # Throw warning for livestreams - logger.warning( - "Livestream URL detected. It is advised to use GStreamer backend(`cv2.CAP_GSTREAMER`) with it." - ) + # Throw warning for livestreams + ytbackend.is_livestream and logger.warning( + "Livestream URL detected. It is strongly recommended to use the GStreamer backend (`backend=cv2.CAP_GSTREAMER`) with these URLs." + ) # check whether stream-resolution was specified and available if not (stream_resolution in ytbackend.streams.keys()): logger.warning( @@ -343,10 +342,9 @@ def __init__( "Threaded Queue Mode is disabled for the current video source!" ) - if self.__thread_timeout: - logger.debug( - "Setting Video-Thread Timeout to {}s.".format(self.__thread_timeout) - ) + self.__thread_timeout and logger.info( + "Setting Video-Thread Timeout to {}s.".format(self.__thread_timeout) + ) # stream variable initialization self.stream = None @@ -359,7 +357,7 @@ def __init__( else: # Two parameters are available since OpenCV 4+ (master branch) self.stream = cv2.VideoCapture(source, backend) - logger.debug("Setting backend `{}` for this source.".format(backend)) + logger.info("Setting backend `{}` for this source.".format(backend)) else: # initialize the camera stream self.stream = cv2.VideoCapture(source) @@ -371,18 +369,16 @@ def __init__( options = {str(k).strip(): v for k, v in options.items()} for key, value in options.items(): property = capPropId(key) - if not (property is None): - self.stream.set(property, value) + not (property is None) and self.stream.set(property, value) # handle colorspace value if not (colorspace is None): self.color_space = capPropId(colorspace.strip()) - if self.__logging and not (self.color_space is None): - logger.debug( - "Enabling `{}` colorspace for this video stream!".format( - colorspace.strip() - ) + self.__logging and not (self.color_space is None) and logger.debug( + "Enabling `{}` colorspace for this video stream!".format( + colorspace.strip() ) + ) # initialize and assign frame-rate variable self.framerate = 0.0 @@ -391,8 +387,7 @@ def __init__( self.framerate = _fps # applying time delay to warm-up webcam only if specified - if time_delay and isinstance(time_delay, (int, float)): - time.sleep(time_delay) + time_delay and isinstance(time_delay, (int, float)) and time.sleep(time_delay) # frame variable initialization (grabbed, self.frame) = self.stream.read() @@ -403,9 +398,8 @@ def __init__( if not (self.color_space is None): self.frame = cv2.cvtColor(self.frame, self.color_space) - if self.__threaded_queue_mode: - # initialize and append to queue - self.__queue.put(self.frame) + # initialize and append to queue + self.__threaded_queue_mode and self.__queue.put(self.frame) else: raise RuntimeError( "[CamGear:ERROR] :: Source is invalid, CamGear failed to initialize stream on this source!" @@ -465,32 +459,22 @@ def __update(self): # apply colorspace to frames if valid if not (self.color_space is None): + # apply colorspace to frames color_frame = None try: - if isinstance(self.color_space, int): - color_frame = cv2.cvtColor(frame, self.color_space) - else: - raise ValueError( - "Global color_space parameter value `{}` is not a valid!".format( - self.color_space - ) - ) + color_frame = cv2.cvtColor(frame, self.color_space) except Exception as e: # Catch if any error occurred + color_frame = None self.color_space = None - if self.__logging: - logger.exception(str(e)) - logger.warning("Input colorspace is not a valid colorspace!") - if not (color_frame is None): - self.frame = color_frame - else: - self.frame = frame + self.__logging and logger.exception(str(e)) + logger.warning("Assigned colorspace value is invalid. Discarding!") + self.frame = color_frame if not (color_frame is None) else frame else: self.frame = frame # append to queue - if self.__threaded_queue_mode: - self.__queue.put(self.frame) + self.__threaded_queue_mode and self.__queue.put(self.frame) # signal queue we're done self.__threaded_queue_mode and self.__queue.put(None) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 47069021d..2e0ff729f 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -833,10 +833,7 @@ def delete_file_safe(file_path): """ try: dfile = Path(file_path) - if sys.version_info >= (3, 8, 0): - dfile.unlink(missing_ok=True) - else: - dfile.exists() and dfile.unlink() + dfile.unlink(missing_ok=True) except Exception as e: logger.exception(str(e)) @@ -912,9 +909,8 @@ def capPropId(property, logging=True): try: integer_value = getattr(cv2, property) except Exception as e: - if logging: - logger.exception(str(e)) - logger.critical("`{}` is not a valid OpenCV property!".format(property)) + logging and logger.exception(str(e)) + logger.critical("`{}` is not a valid OpenCV property!".format(property)) return None return integer_value @@ -1201,18 +1197,15 @@ def validate_ffmpeg(path, logging=False): version = check_output([path, "-version"]) firstline = version.split(b"\n")[0] version = firstline.split(b" ")[2].strip() - if logging: # log if test are passed - logger.debug("FFmpeg validity Test Passed!") - logger.debug( - "Found valid FFmpeg Version: `{}` installed on this system".format( - version - ) - ) + # log if test are passed + logging and logger.info("FFmpeg validity Test Passed!") + logging and logger.debug( + "Found valid FFmpeg Version: `{}` installed on this system".format(version) + ) except Exception as e: # log if test are failed - if logging: - logger.exception(str(e)) - logger.warning("FFmpeg validity Test Failed!") + logging and logger.exception(str(e)) + logger.error("FFmpeg validity Test Failed!") return False return True diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index 1a77b0230..e457feef7 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -566,26 +566,14 @@ def __update(self): # apply colorspace to frames color_frame = None try: - if isinstance(self.color_space, int): - color_frame = cv2.cvtColor(frame, self.color_space) - else: - self.__logging and logger.warning( - "Global color_space parameter value `{}` is not a valid!".format( - self.color_space - ) - ) - self.color_space = None + color_frame = cv2.cvtColor(frame, self.color_space) except Exception as e: # Catch if any error occurred + color_frame = None self.color_space = None - if self.__logging: - logger.exception(str(e)) - logger.warning("Input colorspace is not a valid colorspace!") - - if not (color_frame is None): - self.frame = color_frame - else: - self.frame = frame + self.__logging and logger.exception(str(e)) + logger.warning("Assigned colorspace value is invalid. Discarding!") + self.frame = color_frame if not (color_frame is None) else frame else: self.frame = frame diff --git a/vidgear/gears/screengear.py b/vidgear/gears/screengear.py index d25936311..45408dac1 100644 --- a/vidgear/gears/screengear.py +++ b/vidgear/gears/screengear.py @@ -113,7 +113,7 @@ def __init__( else ("left", "top", "width", "height") ) screen_dims = OrderedDict((k, screen_dims[k]) for k in key_order) - logging and logger.debug( + self.__logging and logger.debug( "Setting Capture-Area dimensions: {}".format(json.dumps(screen_dims)) ) else: @@ -126,7 +126,7 @@ def __init__( if self.__target_fps and isinstance(self.__target_fps, (int, float)): # set values self.__target_fps = int(self.__target_fps) - logging and logger.debug( + self.__logging and logger.debug( "Setting Target FPS: {}".format(self.__target_fps) ) else: @@ -194,7 +194,7 @@ def __init__( self.__monitor_instance = self.__capture_object.monitors[monitor] # log backend - self.__backend and logging and logger.debug( + self.__backend and self.__logging and logger.debug( "Setting Backend: {}".format(self.__backend.upper()) ) @@ -202,7 +202,7 @@ def __init__( # separately handle colorspace value to int conversion if colorspace: self.color_space = capPropId(colorspace.strip()) - logging and not (self.color_space is None) and logger.debug( + self.__logging and not (self.color_space is None) and logger.debug( "Enabling `{}` colorspace for this video stream!".format( colorspace.strip() ) @@ -258,7 +258,9 @@ def __init__( except Exception as e: if isinstance(e, ScreenShotError): # otherwise catch and log errors - logging and logger.exception(self.__capture_object.get_error_details()) + self.__logging and logger.exception( + self.__capture_object.get_error_details() + ) raise ValueError( "[ScreenGear:ERROR] :: ScreenShotError caught, Wrong dimensions passed to python-mss, Kindly Refer Docs!" ) @@ -338,25 +340,14 @@ def __update(self): # apply colorspace to frames color_frame = None try: - if isinstance(self.color_space, int): - color_frame = cv2.cvtColor(frame, self.color_space) - else: - self.__logging and logger.warning( - "Global color_space parameter value `{}` is not a valid!".format( - self.color_space - ) - ) - self.color_space = None + color_frame = cv2.cvtColor(frame, self.color_space) except Exception as e: # Catch if any error occurred + color_frame = None self.color_space = None - if self.__logging: - logger.exception(str(e)) - logger.warning("Input colorspace is not a valid colorspace!") - if not (color_frame is None): - self.frame = color_frame - else: - self.frame = frame + self.__logging and logger.exception(str(e)) + logger.warning("Assigned colorspace value is invalid. Discarding!") + self.frame = color_frame if not (color_frame is None) else frame else: self.frame = frame @@ -390,5 +381,4 @@ def stop(self): self.__terminate.set() # wait until stream resources are released (producer thread might be still grabbing frame) - if self.__thread is not None: - self.__thread.join() + not (self.__thread is None) and self.__thread.join() diff --git a/vidgear/gears/stabilizer.py b/vidgear/gears/stabilizer.py index aefd9b4ea..ffd17a04c 100644 --- a/vidgear/gears/stabilizer.py +++ b/vidgear/gears/stabilizer.py @@ -103,13 +103,15 @@ def __init__( self.__crop_n_zoom = border_size # crops and zoom frame to original size self.__border_size = 0 # zero out border size self.__frame_size = None # handles frame size for zooming - if logging: - logger.debug("Setting Cropping margin {} pixels".format(border_size)) + self.__logging and logger.debug( + "Setting Cropping margin {} pixels".format(border_size) + ) else: # Add output borders to frame self.__border_size = border_size - if self.__logging and border_size: - logger.debug("Setting Border size {} pixels".format(border_size)) + self.__logging and border_size and logger.debug( + "Setting Border size {} pixels".format(border_size) + ) # define valid border modes border_modes = { @@ -124,19 +126,18 @@ def __init__( if not crop_n_zoom: # initialize global border mode variable self.__border_mode = border_modes[border_type] - if self.__logging and border_type != "black": - logger.debug("Setting Border type: {}".format(border_type)) + self.__logging and border_type != "black" and logger.info( + "Setting Border type: {}".format(border_type) + ) else: # log and reset to default - if self.__logging and border_type != "black": - logger.debug( - "Setting border type is disabled if cropping is enabled!" - ) + self.__logging and border_type != "black" and logger.debug( + "Setting border type is disabled if cropping is enabled!" + ) self.__border_mode = border_modes["black"] else: # otherwise log if not - if logging: - logger.debug("Invalid input border type!") + self.__logging and logger.debug("Invalid input border type!") self.__border_mode = border_modes["black"] # reset to default mode # define OpenCV version diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 25e496231..9d23b4894 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -27,7 +27,6 @@ import logging as log import subprocess as sp from tqdm import tqdm -from fractions import Fraction from collections import OrderedDict # import helper packages diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 2a8b7ee1d..d614b1385 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -22,7 +22,6 @@ import os import cv2 import time -import signal import platform import pathlib import logging as log From fb76a2deafa7b5c09d1c39eca957da398ddbfb03 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 9 Jun 2024 13:10:31 +0530 Subject: [PATCH 61/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20WriterGear:=20Improv?= =?UTF-8?q?e=20error=20handling=20in=20`execute=5Fffmpeg=5Fcmd`=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🎨 Instead of raising a generic `ValueError` exception, the method now raises a `ValueError` with a more descriptive error message when encountering a `BrokenPipeError` or `IOError`. - 🥅 The error handling has been updated to follow the recommendations of PEP 409 - Suppressing exception context. This ensures that the original exception context is preserved when re-raising the `ValueError`. - ⚡️ If logging is enabled (`self.__logging` is True), the `ValueError` is raised with the suppressed context (`from None`), effectively discarding the original exception context. - ⚡️ If logging is disabled, the `ValueError` is raised with the original exception context (`from e`), where `e` is the original `OSError` or `IOError` exception. --- vidgear/gears/writegear.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index d614b1385..4a8e6e9d1 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -673,12 +673,17 @@ def execute_ffmpeg_cmd(self, command=None): else: # In silent mode sp.run(cmd, stdin=sp.PIPE, stdout=sp.DEVNULL, stderr=sp.STDOUT) - except (OSError, IOError): - # raise error and log if something is wrong. - logger.error( - "BrokenPipeError caught, Wrong command passed to FFmpeg Pipe, Kindly Refer Docs!" - ) - raise ValueError # for testing purpose only + except (OSError, IOError) as e: + # re-raise error + # PEP 409 – Suppressing exception context recommendations + if self.__logging: + raise ValueError( + "BrokenPipeError caught, Wrong command passed to FFmpeg Pipe, Kindly Refer Docs!" + ) from None + else: + raise ValueError( + "BrokenPipeError caught, Wrong command passed to FFmpeg Pipe, Kindly Refer Docs!" + ) from e def __start_CVProcess(self): """ From 741222987ec7705ee7bbbde782c54bf453d0f905 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 18 Jun 2024 19:22:31 +0530 Subject: [PATCH 62/81] =?UTF-8?q?=F0=9F=A9=B9=20NetGear:=20Updated=20param?= =?UTF-8?q?eters=20and=20documentation=20(Fixes=20#390)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🥅 Added a warning message in the `NetGear` class to log potential issues when `flag=1` (NOBLOCK) is set, which may cause NetGear to not terminate gracefully. - 🔊 Included an informational log message indicating that the `track` option will be ignored when `copy=True` is defined. - 📝 Changed the default value of the `copy` option from `False` to `True` across various documentation files for NetGear. 👷 CI: Updated unit tests to reflect the new default value for the `copy` option. ✏️ Docs: Fixed few typos. --- docs/gears/netgear/params.md | 12 +++++++----- docs/gears/netgear/usage.md | 12 ++++++------ docs/help/screengear_ex.md | 4 ++-- vidgear/gears/netgear.py | 6 ++++++ vidgear/gears/videogear.py | 6 ++++-- vidgear/gears/writegear.py | 3 +-- vidgear/tests/network_tests/test_netgear.py | 2 +- 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/docs/gears/netgear/params.md b/docs/gears/netgear/params.md index 949069b2f..0be8d45f2 100644 --- a/docs/gears/netgear/params.md +++ b/docs/gears/netgear/params.md @@ -173,11 +173,13 @@ This parameter provides the flexibility to alter various NetGear API's internal * **`subscriber_timeout`**(_integer_): Similar to `request_timeout`, this internal attribute also controls the timeout value _(in seconds)_ but for non-synchronous `zmq.PUB/zmq.SUB` pattern in compression mode, after which the Client(Subscriber) exit itself with `Nonetype` value if it's unable to get any response from the socket. It's value can anything greater than `0`, and its disabled by default _(meaning the client will wait forever for response)_. - * **`flag`**(_integer_): This PyZMQ attribute value can be either `0` or `zmq.NOBLOCK`_( i.e. 1)_. More information can be found [here ➶](https://pyzmq.readthedocs.io/en/latest/api/zmq.html). + * **`flag`**(_integer_): This PyZMQ attribute value can be either `0` or `zmq.NOBLOCK`_( i.e. 1)_. More information can be found [here ➶](https://pyzmq.readthedocs.io/en/latest/api/zmq.html#zmq.Socket.recv). + + !!! warning "With flags=1 (i.e. `NOBLOCK`), NetGear raises `ZMQError` if no messages have arrived; otherwise, this waits until a message arrives." * **`copy`**(_boolean_): This PyZMQ attribute selects if message be received in a copying or non-copying manner. If `False` a object is returned, if `True` a string copy of the message is returned. - * **`track`**(_boolean_): This PyZMQ attribute check if the message is tracked for notification that ZMQ has finished with it. (_ignored if copy=True_). + * **`track`**(_boolean_): This PyZMQ attribute check if the message is tracked for notification that ZMQ has finished with it. _(ignored if `copy=True`)_. The desired attributes can be passed to NetGear API as follows: @@ -188,9 +190,9 @@ options = { "secure_mode": 2, "custom_cert_location": "/home/foo/foo1/foo2", "overwrite_cert": True, - "flag": 0, - "copy": False, - "track": False, + "flag": 0, + "copy": True, + "track": False } # assigning it NetGear(logging=True, **options) diff --git a/docs/gears/netgear/usage.md b/docs/gears/netgear/usage.md index 5f8614fdd..2fa4847c9 100644 --- a/docs/gears/netgear/usage.md +++ b/docs/gears/netgear/usage.md @@ -143,7 +143,7 @@ from vidgear.gears import NetGear import cv2 # define various tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear Client at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with yours !!! @@ -198,7 +198,7 @@ from vidgear.gears import VideoGear from vidgear.gears import NetGear # define various tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Open live video stream on webcam at first index(i.e. 0) device stream = VideoGear(source=0).start() @@ -260,7 +260,7 @@ from vidgear.gears import NetGear import cv2 # define tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear Client at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with yours !!! @@ -318,7 +318,7 @@ import cv2 stream = cv2.VideoCapture(0) # define tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear Client at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with yours !!! @@ -377,7 +377,7 @@ from vidgear.gears import NetGear import cv2 # define various tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear Client at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with yours !!! @@ -432,7 +432,7 @@ from vidgear.gears import ScreenGear from vidgear.gears import NetGear # define various tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Start capturing live Monitor screen frames with default settings stream = ScreenGear().start() diff --git a/docs/help/screengear_ex.md b/docs/help/screengear_ex.md index 6623e6dae..49a0374ed 100644 --- a/docs/help/screengear_ex.md +++ b/docs/help/screengear_ex.md @@ -44,7 +44,7 @@ from vidgear.gears import WriteGear import cv2 # define various tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear Client at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with yours !!! @@ -106,7 +106,7 @@ options = {"top": 40, "left": 0, "width": 100, "height": 100} stream = ScreenGear(logging=True, **options).start() # define various netgear tweak flags -options = {"flag": 0, "copy": False, "track": False} +options = {"flag": 0, "copy": True, "track": False} # Define Netgear server at given IP address and define parameters # !!! change following IP address '192.168.x.xxx' with client's IP address !!! diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 7bae7d644..7a95c5a4e 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -412,10 +412,16 @@ def __init__( # handle ZMQ flags elif key == "flag" and isinstance(value, int): self.__msg_flag = value + self.__msg_flag and logger.warning( + "The flag optional value is set to `1` (NOBLOCK) for this run. This might cause NetGear to not terminate gracefully." + ) elif key == "copy" and isinstance(value, bool): self.__msg_copy = value elif key == "track" and isinstance(value, bool): self.__msg_track = value + self.__msg_copy and self.__msg_track and logger.info( + "The `track` optional value will be ignored for this run because `copy=True` is also defined." + ) else: pass diff --git a/vidgear/gears/videogear.py b/vidgear/gears/videogear.py index c6a5c09e4..250ddd7ac 100644 --- a/vidgear/gears/videogear.py +++ b/vidgear/gears/videogear.py @@ -37,9 +37,11 @@ class VideoGear: """ VideoGear API provides a special internal wrapper around VidGear's exclusive Video Stabilizer class. - VideoGear also acts as a Common Video-Capture API that provides internal access for both CamGear and PiGear APIs and their parameters with an exclusive enablePiCamera boolean flag. + VideoGear also acts as a Common Video-Capture API that provides internal access for both CamGear and PiGear APIs and + their parameters with an exclusive enablePiCamera boolean flag. - VideoGear is ideal when you need to switch to different video sources without changing your code much. Also, it enables easy stabilization for various video-streams (real-time or not) + VideoGear is ideal when you need to switch to different video sources without changing your code much. Also, it enables + easy stabilization for various video-streams (real-time or not) with minimum effort and writing way fewer lines of code. """ diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 4a8e6e9d1..086804ae0 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -675,7 +675,6 @@ def execute_ffmpeg_cmd(self, command=None): sp.run(cmd, stdin=sp.PIPE, stdout=sp.DEVNULL, stderr=sp.STDOUT) except (OSError, IOError) as e: # re-raise error - # PEP 409 – Suppressing exception context recommendations if self.__logging: raise ValueError( "BrokenPipeError caught, Wrong command passed to FFmpeg Pipe, Kindly Refer Docs!" @@ -757,7 +756,7 @@ def __start_CVProcess(self): # check if OpenCV VideoCapture is opened successfully assert ( self.__process.isOpened() - ), "[WriteGear:ERROR] :: Failed to intialize OpenCV Writer!" + ), "[WriteGear:ERROR] :: Failed to initialize OpenCV Writer!" def close(self): """ diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 462056577..a6d341714 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -131,7 +131,7 @@ def test_patterns(pattern): # define parameters options = { "flag": 0, - "copy": False, + "copy": True, "track": False, "jpeg_compression": False, "subscriber_timeout": 5, From 645231c40a8d3f9cfe3d02e795b19b925fca1b8d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 18 Jun 2024 19:50:25 +0530 Subject: [PATCH 63/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20StreamGear:=20Mov?= =?UTF-8?q?ed=20handle=20streaming=20`format`=20to=20beginning=20to=20fix?= =?UTF-8?q?=20=20'StreamGear'=20object=20has=20no=20attribute=20'=5FStream?= =?UTF-8?q?Gear=5F=5Fformat'=20bug.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/streamgear.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 9d23b4894..6e504566e 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -135,6 +135,34 @@ def __init__( "[StreamGear:ERROR] :: Failed to find FFmpeg assets on this system. Kindly compile/install FFmpeg or provide a valid custom FFmpeg binary path!" ) + # 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 asset files for {} streaming format.".format( + self.__format.upper() + ) + ) + elif difflib.get_close_matches(_format, supported_formats): + raise ValueError( + "[StreamGear:ERROR] :: Incorrect `format` parameter value! Did you mean `{}`?".format( + difflib.get_close_matches(_format, supported_formats)[0] + ) + ) + else: + raise ValueError( + "[StreamGear:ERROR] :: The `format` parameter value `{}` not valid/supported!".format( + format + ) + ) + else: + raise ValueError( + "[StreamGear:ERROR] :: The `format` parameter value is Missing or Invalid!" + ) + # handle Audio-Input audio = self.__params.pop("-audio", False) if audio and isinstance(audio, str): From e4142aceaf799cdfdc4f9c3ec76d476687a26395 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 18 Jun 2024 23:53:15 +0530 Subject: [PATCH 64/81] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20CI:=20Testing=20simp?= =?UTF-8?q?lejpeg=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index a3de70ba8..a7249570a 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -47,6 +48,9 @@ def return_testvideo_path(): """ returns Test Video path """ + # TODO Remove this + import simplejpeg + path = "{}/Downloads/Test_videos/BigBuckBunny_4sec.mp4".format( tempfile.gettempdir() ) From 626b64a27ea0b932ff3d3dbe56f4aefc1b4fea08 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 00:25:42 +0530 Subject: [PATCH 65/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20CI:=20Fixed=20`si?= =?UTF-8?q?mplejpeg`=20and=20`opencv`=20not=20compatible=20with=20`numpy?= =?UTF-8?q?=3D=3D2.x.x`=20versions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📌 Pinned `numpy<2.0.0` in all CI envs. - ⏪️ Reverted testing simplejpeg import --- .github/workflows/ci_linux.yml | 3 ++- appveyor.yml | 1 + azure-pipelines.yml | 3 ++- vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py | 3 --- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 0e6d0c0c1..7fc6d2de1 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -59,7 +59,8 @@ jobs: chmod +x scripts/bash/install_opencv.sh - name: install pip_dependencies run: | - pip install -U pip wheel numpy + pip install -U pip wheel + pip install numpy<2.0.0 pip install -U .[asyncio] pip uninstall opencv-python -y pip install -U flake8 six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient diff --git a/appveyor.yml b/appveyor.yml index 4b5e528c2..e86e6e8ce 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -60,6 +60,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "python -m pip install --upgrade pip wheel" + - "python -m pip install --upgrade numpy<2.0.0" - "python -m pip install .[asyncio] six codecov httpx pytest-cov pytest-asyncio yt_dlp aiortc paramiko m3u8 async-asgi-testclient" - "python -m pip install --upgrade deffcode" - "python -m pip install cryptography==38.0.4" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8d3de4806..73e1ff9d5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -66,7 +66,8 @@ steps: displayName: "Prepare dataset" - script: | - python -m pip install -U pip wheel numpy + python -m pip install -U pip wheel + python -m pip install numpy<2.0.0 python -m pip install -U .[asyncio] python -m pip install -U six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient python -m pip install -U deffcode diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index a7249570a..7f4dad116 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -48,9 +48,6 @@ def return_testvideo_path(): """ returns Test Video path """ - # TODO Remove this - import simplejpeg - path = "{}/Downloads/Test_videos/BigBuckBunny_4sec.mp4".format( tempfile.gettempdir() ) From 17429771199789f7ae5c4a2f1db9e366ee9ccbad Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 17:22:34 +0530 Subject: [PATCH 66/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20CI:=20Fixed=20wor?= =?UTF-8?q?d=20splitting=20when=20installing=20numpy.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci_linux.yml | 2 +- appveyor.yml | 2 +- azure-pipelines.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 7fc6d2de1..57b3c6d74 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -60,7 +60,7 @@ jobs: - name: install pip_dependencies run: | pip install -U pip wheel - pip install numpy<2.0.0 + pip install "numpy<2.0.0" pip install -U .[asyncio] pip uninstall opencv-python -y pip install -U flake8 six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient diff --git a/appveyor.yml b/appveyor.yml index e86e6e8ce..5ddc91e36 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -60,7 +60,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "python -m pip install --upgrade pip wheel" - - "python -m pip install --upgrade numpy<2.0.0" + - "python -m pip install 'numpy<2.0.0'" - "python -m pip install .[asyncio] six codecov httpx pytest-cov pytest-asyncio yt_dlp aiortc paramiko m3u8 async-asgi-testclient" - "python -m pip install --upgrade deffcode" - "python -m pip install cryptography==38.0.4" diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 73e1ff9d5..d2bece7f4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -67,7 +67,7 @@ steps: - script: | python -m pip install -U pip wheel - python -m pip install numpy<2.0.0 + python -m pip install "numpy<2.0.0" python -m pip install -U .[asyncio] python -m pip install -U six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient python -m pip install -U deffcode From 3e1e10aff082f41dbcefd0b922609cd433e01c44 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 19:37:41 +0530 Subject: [PATCH 67/81] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Core:=20Improved=20e?= =?UTF-8?q?xception=20handling=20for=20module=20imports.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Updated `import_dependency_safe` in `helper.py`: - 🥅 Added specific handling for `ModuleNotFoundError`. - 🧑‍💻 Included original exception in `ImportError` for better error tracing. - 🔊 Enhanced logging to include exception traceback when error is set to "log". - ⚡️ Enhanced `import_core_dependency` in `__init__.py`: - 🥅 Added specific handling for `ModuleNotFoundError`. - 🧑‍💻 Included original exception in `ImportError` for better error tracing. --- vidgear/gears/__init__.py | 13 ++++++++----- vidgear/gears/helper.py | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/vidgear/gears/__init__.py b/vidgear/gears/__init__.py index ed17cbb7e..d4f4d0177 100644 --- a/vidgear/gears/__init__.py +++ b/vidgear/gears/__init__.py @@ -85,11 +85,14 @@ def import_core_dependency( # try importing dependency try: module = importlib.import_module(name) - if sub_class: - module = getattr(module, sub_class) - except ImportError: - # raise - raise ImportError(msg) from None + module = getattr(module, sub_class) if sub_class else module + except Exception as e: + if isinstance(e, ModuleNotFoundError): + # raise message + raise ModuleNotFoundError(msg) from None + else: + # raise error+message + raise ImportError(msg) from e # check if minimum required version if not (version) is None: diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 2e0ff729f..a7ab5ba85 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -247,14 +247,17 @@ def import_dependency_safe( # try importing dependency try: module = importlib.import_module(name) - if sub_class: - module = getattr(module, sub_class) - except Exception: - # handle errors. + module = getattr(module, sub_class) if sub_class else module + except Exception as e: if error == "raise": - raise ImportError(msg) from None + if isinstance(e, ModuleNotFoundError): + # raise message + raise ModuleNotFoundError(msg) from None + else: + # raise error+message + raise ImportError(msg) from e elif error == "log": - logger.error(msg) + logger.error(msg, exc_info=sys.exc_info()) return None else: return None From 8b765b91892a509c5ea7f52757da6ba3c2f71138 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 20:15:52 +0530 Subject: [PATCH 68/81] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Added=20tests=20for?= =?UTF-8?q?=20`import=5Fdependency=5Fsafe`=20function=20.=20-=20=E2=9A=A1?= =?UTF-8?q?=EF=B8=8F=20Included=20various=20test=20cases=20to=20validate?= =?UTF-8?q?=20different=20scenarios=20and=20error=20handling=20in=20`impor?= =?UTF-8?q?t=5Fdependency=5Fsafe`.=20-=20=E2=98=94=20Ensured=20coverage=20?= =?UTF-8?q?for=20`raise`,=20`log`,=20`silent`,=20and=20unknown=20error=20t?= =?UTF-8?q?ypes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/test_helper.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 1862ea5bd..385fae9cd 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -48,6 +48,7 @@ validate_ffmpeg, get_video_bitrate, get_valid_ffmpeg_path, + import_dependency_safe, download_ffmpeg_binaries, check_gstreamer_support, generate_auth_certificates, @@ -130,6 +131,35 @@ def getframe(): return np.zeros([500, 800, 3], dtype=np.uint8) +test_data = [ + ("XYZ", "raise", "Invalid", "0.0.0", "My message"), + ("cv2", "raise", "opencv-python", "6.0.0", ""), + ("cv2", "log", "opencv-python", "3.0.0", ""), + ("cv2", "silent", "opencv-python", "6.0.0", ""), + ("cv2", "unknown", "opencv-python", "6.0.0", ""), + ("from cv2 import XYZ", "raise", "opencv-python", "3.0.0", ""), + ("from xyz import XYZ", "raise", "Invalid", "0.0.0", "My message"), + ("from cv2 import XYZ", "log", "opencv-python", "3.0.0", ""), + ("from xyz import XYZ", "log", "Invalid", "0.0.0", "My message"), +] + + +@pytest.mark.parametrize( + "name, error, pkg_name, min_version, custom_message", test_data +) +def test_import_dependency_safe(name, error, pkg_name, min_version, custom_message): + """ + Testing import_dependency_safe helper function. + """ + try: + import_dependency_safe(name, error, pkg_name, min_version, custom_message) + except Exception as e: + if ( + error == "raise" and not isinstance(e, (ModuleNotFoundError, ImportError)) + ) or (error == "unknown" and not isinstance(e, AssertionError)): + pytest.fail(str(e)) + + test_data = [ {"-thread_queue_size": "512", "-f": "alsa", "-clones": 24}, { From cb769d3eaabff75606a595b9c8831011ec94f99e Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 20:19:29 +0530 Subject: [PATCH 69/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20invalid=20es?= =?UTF-8?q?cape=20sequence=20in=20testcase=20string.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/streamer_tests/test_streamgear_modes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index b979cc600..6c983d6db 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -466,7 +466,7 @@ def test_input_framerate_rtf(format): { "-clear_prev_assets": True, "-seg_duration": -346.67, - "-audio": "inv/\lid", + "-audio": "invAlid", "-bpp": "unknown", "-gop": "unknown", "-s:v:0": "unknown", From 9f4a1e3c7506e9ea1637e9954bda61dc54c28d5d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 21:12:05 +0530 Subject: [PATCH 70/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20invalid=20es?= =?UTF-8?q?cape=20sequence=20in=20testcase=20string.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appveyor.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5ddc91e36..19ab6cd5b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -58,13 +58,13 @@ matrix: install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "python --version" - - "python -m pip install --upgrade pip wheel" - - "python -m pip install 'numpy<2.0.0'" - - "python -m pip install .[asyncio] six codecov httpx pytest-cov pytest-asyncio yt_dlp aiortc paramiko m3u8 async-asgi-testclient" - - "python -m pip install --upgrade deffcode" - - "python -m pip install cryptography==38.0.4" - - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl" + - py --version + - py -m pip install --upgrade pip wheel + - py -m pip install "numpy<2.0.0" + - py -m pip install .[asyncio] six codecov httpx pytest-cov pytest-asyncio yt_dlp aiortc paramiko m3u8 async-asgi-testclient + - py -m pip install --upgrade deffcode + - py -m pip install cryptography==38.0.4 + - py -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl - cmd: chmod +x scripts/bash/prepare_dataset.sh - cmd: bash scripts/bash/prepare_dataset.sh From f26396025c424c424aa792326d6a5fdc890d8284 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 21:22:22 +0530 Subject: [PATCH 71/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20missing=20`p?= =?UTF-8?q?ytest`=20dependency=20in=20appveyor.yml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 19ab6cd5b..c4aeee093 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,7 +61,8 @@ install: - py --version - py -m pip install --upgrade pip wheel - py -m pip install "numpy<2.0.0" - - py -m pip install .[asyncio] six codecov httpx pytest-cov pytest-asyncio yt_dlp aiortc paramiko m3u8 async-asgi-testclient + - py -m pip install .[asyncio] six codecov httpx yt_dlp aiortc paramiko + - py -m pip install pytest pytest-cov pytest-asyncio m3u8 async-asgi-testclient - py -m pip install --upgrade deffcode - py -m pip install cryptography==38.0.4 - py -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl From 1a77e5b67ef1eeeade9ba093409bc2b99aa76557 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 21:27:29 +0530 Subject: [PATCH 72/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Removed=20pinned=20`?= =?UTF-8?q?cryptography=3D=3D38.0.4`=20dependency.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci_linux.yml | 1 - appveyor.yml | 5 ++--- azure-pipelines.yml | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 57b3c6d74..55ee9ce2c 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -65,7 +65,6 @@ jobs: pip uninstall opencv-python -y pip install -U flake8 six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient pip install -U deffcode - pip install cryptography==38.0.4 if: success() - name: run prepare_dataset_script run: bash scripts/bash/prepare_dataset.sh diff --git a/appveyor.yml b/appveyor.yml index c4aeee093..841799d8f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -64,13 +64,12 @@ install: - py -m pip install .[asyncio] six codecov httpx yt_dlp aiortc paramiko - py -m pip install pytest pytest-cov pytest-asyncio m3u8 async-asgi-testclient - py -m pip install --upgrade deffcode - - py -m pip install cryptography==38.0.4 - py -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl - cmd: chmod +x scripts/bash/prepare_dataset.sh - cmd: bash scripts/bash/prepare_dataset.sh test_script: - - cmd: python -m pytest --verbose --capture=no --cov-report term-missing --cov=vidgear vidgear/tests/ + - cmd: py -m pytest --verbose --capture=no --cov-report term-missing --cov=vidgear vidgear/tests/ after_test: - - cmd: python -m codecov + - cmd: py -m codecov diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d2bece7f4..7c1d2013d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -71,7 +71,6 @@ steps: python -m pip install -U .[asyncio] python -m pip install -U six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient python -m pip install -U deffcode - python -m pip install cryptography==38.0.4 displayName: "Install pip dependencies" - script: | From 0d9071e9168033d4bbd8c53ba0f3acb312afc288 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 19 Jun 2024 21:37:42 +0530 Subject: [PATCH 73/81] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20python=20env?= =?UTF-8?q?ironment=20bugs=20in=20`appveyor.yml`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appveyor.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 841799d8f..a8edd1388 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -58,18 +58,18 @@ matrix: install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - py --version - - py -m pip install --upgrade pip wheel - - py -m pip install "numpy<2.0.0" - - py -m pip install .[asyncio] six codecov httpx yt_dlp aiortc paramiko - - py -m pip install pytest pytest-cov pytest-asyncio m3u8 async-asgi-testclient - - py -m pip install --upgrade deffcode - - py -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl + - "python --version" + - "python -m pip install --upgrade pip wheel" + - cmd: python -m pip install "numpy<2.0.0" + - "python -m pip install .[asyncio] six codecov httpx yt_dlp aiortc paramiko" + - "python -m pip install pytest pytest-cov pytest-asyncio m3u8 async-asgi-testclient" + - "python -m pip install --upgrade deffcode" + - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl" - cmd: chmod +x scripts/bash/prepare_dataset.sh - cmd: bash scripts/bash/prepare_dataset.sh test_script: - - cmd: py -m pytest --verbose --capture=no --cov-report term-missing --cov=vidgear vidgear/tests/ + - cmd: python -m pytest --verbose --capture=no --cov-report term-missing --cov=vidgear vidgear/tests/ after_test: - - cmd: py -m codecov + - cmd: python -m codecov From 77ee81799dc8069db0f59ba055553d59e36651d9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 Jun 2024 09:54:17 +0530 Subject: [PATCH 74/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Minor=20updates=20?= =?UTF-8?q?to=20reference=20page=20titles.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index e8f8f9759..a69f60eba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -231,7 +231,7 @@ nav: - Parameters: gears/camgear/params.md - Bonus: - Source Tweak Parameters: gears/camgear/advanced/source_params.md - - API References: bonus/reference/camgear.md + - CamGear API References: bonus/reference/camgear.md - Bonus Examples: help/camgear_ex.md - FAQs: help/camgear_faqs.md - PiGear: @@ -239,7 +239,7 @@ nav: - Usage Examples: gears/pigear/usage.md - Parameters: gears/pigear/params.md - Bonus: - - API References: bonus/reference/pigear.md + - PiGear API References: bonus/reference/pigear.md - Bonus Examples: help/pigear_ex.md - FAQs: help/pigear_faqs.md - VideoGear: @@ -247,7 +247,7 @@ nav: - Usage Examples: gears/videogear/usage.md - Parameters: gears/videogear/params.md - Bonus: - - API References: bonus/reference/videogear.md + - VideoGear API References: bonus/reference/videogear.md - Bonus Examples: help/videogear_ex.md - FAQs: help/videogear_faqs.md - ScreenGear: @@ -255,7 +255,7 @@ nav: - Usage Examples: gears/screengear/usage.md - Parameters: gears/screengear/params.md - Bonus: - - API References: bonus/reference/screengear.md + - ScreenGear API References: bonus/reference/screengear.md - Bonus Examples: help/screengear_ex.md - FAQs: help/screengear_faqs.md - WriteGear: @@ -272,7 +272,7 @@ nav: - Usage Examples: gears/writegear/non_compression/usage.md - Parameters: gears/writegear/non_compression/params.md - Bonus: - - API References: bonus/reference/writegear.md + - WriteGear API References: bonus/reference/writegear.md - Bonus Examples: help/writegear_ex.md - FAQs: help/writegear_faqs.md - StreamGear: @@ -285,7 +285,7 @@ nav: - Usage Examples: gears/streamgear/rtfm/usage.md - Parameters: gears/streamgear/params.md - Bonus: - - API References: bonus/reference/streamgear.md + - StreamGear API References: bonus/reference/streamgear.md - FFmpeg Installation: gears/streamgear/ffmpeg_install.md - Bonus Examples: help/streamgear_ex.md - FAQs: help/streamgear_faqs.md @@ -301,7 +301,7 @@ nav: - Frame Compression: gears/netgear/advanced/compression.md - Parameters: gears/netgear/params.md - Bonus: - - API References: bonus/reference/netgear.md + - NetGear API References: bonus/reference/netgear.md - Bonus Examples: help/netgear_ex.md - FAQs: help/netgear_faqs.md - WebGear: @@ -310,7 +310,7 @@ nav: - Advanced Usages: gears/webgear/advanced.md - Parameters: gears/webgear/params.md - Bonus: - - API References: bonus/reference/webgear.md + - WebGear API References: bonus/reference/webgear.md - Bonus Examples: help/webgear_ex.md - FAQs: help/webgear_faqs.md - WebGear_RTC: @@ -319,7 +319,7 @@ nav: - Advanced Usages: gears/webgear_rtc/advanced.md - Parameters: gears/webgear_rtc/params.md - Bonus: - - API References: bonus/reference/webgear_rtc.md + - WebGear_RTC API References: bonus/reference/webgear_rtc.md - Bonus Examples: help/webgear_rtc_ex.md - FAQs: help/webgear_rtc_faqs.md - NetGear_Async: @@ -329,7 +329,7 @@ nav: - Bidirectional Mode: gears/netgear_async/advanced/bidirectional_mode.md - Parameters: gears/netgear_async/params.md - Bonus: - - API References: bonus/reference/netgear_async.md + - NetGear_Async API References: bonus/reference/netgear_async.md - Bonus Examples: help/netgear_async_ex.md - FAQs: help/netgear_async_faqs.md - Stabilizer Class: From 15c46fa844674fcd133d57f73a5e9a3febc25e93 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 Jun 2024 11:27:42 +0530 Subject: [PATCH 75/81] =?UTF-8?q?=F0=9F=90=9B=20CamGear:=20Fixed=20logging?= =?UTF-8?q?=20condition=20for=20yt-dlp=20(Fixes=20#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🔊 Updated `no_warnings` parameter in `CamGear` to be `False` when logging is enabled and `True` otherwise. --- vidgear/gears/camgear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index 8c2faec35..c31493c2b 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -85,7 +85,7 @@ def __init__(self, source_url, logging=False, **stream_params): "format": "best*[vcodec!=none]", "quiet": True, "prefer_insecure": False, - "no_warnings": True if logging else False, + "no_warnings": False if logging else True, "dump_single_json": True, "extract_flat": True, "skip_download": True, From 246d36df5cd931cd2f147d1b8650ab68f2ddade9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 Jun 2024 14:09:16 +0530 Subject: [PATCH 76/81] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB=20Enhan?= =?UTF-8?q?ced=20error=20messages=20for=20WebGear=20auto-generation=20work?= =?UTF-8?q?flow=20(Fixes=20#403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Updated `homepage`, `not_found`, and `server_error` methods to include more detailed JSON error messages. - 🥅 Added specific error and message prefixes to improve clarity. --- vidgear/gears/asyncio/webgear.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index 7a5f7bb0f..8955b00d7 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -505,7 +505,9 @@ async def __homepage(self, request): self.__templates.TemplateResponse(request, "index.html") if not self.__skip_generate_webdata else JSONResponse( - {"detail": "WebGear Data-Files Auto-Generation WorkFlow is disabled!"}, + { + "detail": "MESSAGE : WebGear Data-Files Auto-Generation WorkFlow is disabled!" + }, status_code=404, ) ) @@ -518,7 +520,11 @@ async def __not_found(self, request, exc): self.__templates.TemplateResponse(request, "404.html", status_code=404) if not self.__skip_generate_webdata else JSONResponse( - {"detail": "WebGear Data-Files Auto-Generation WorkFlow is disabled!"}, + { + "detail": "ERROR : {} :: MESSAGE : WebGear Data-Files Auto-Generation WorkFlow is disabled.".format( + exc.detail + ) + }, status_code=404, ) ) @@ -531,7 +537,11 @@ async def __server_error(self, request, exc): self.__templates.TemplateResponse(request, "500.html", status_code=500) if not self.__skip_generate_webdata else JSONResponse( - {"detail": "WebGear Data-Files Auto-Generation WorkFlow is disabled!"}, + { + "detail": "ERROR : {} :: MESSAGE : WebGear Data-Files Auto-Generation WorkFlow is disabled.".format( + exc.detail if hasattr(exc, "detail") else repr(exc) + ) + }, status_code=500, ) ) From c90183aa5c211e0f7aaf55879bd2892536131165 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 Jun 2024 16:20:34 +0530 Subject: [PATCH 77/81] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Rearranged=20the=20d?= =?UTF-8?q?ependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Docs: Fixed minor code highlighting --- .github/workflows/ci_linux.yml | 4 ++-- appveyor.yml | 4 ++-- azure-pipelines.yml | 4 ++-- docs/gears/writegear/compression/usage.md | 12 ++++++------ docs/gears/writegear/non_compression/usage.md | 15 ++++++++------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 55ee9ce2c..dd5191e38 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -61,9 +61,9 @@ jobs: run: | pip install -U pip wheel pip install "numpy<2.0.0" - pip install -U .[asyncio] + pip install -U .[asyncio] six httpx yt_dlp paramiko pip uninstall opencv-python -y - pip install -U flake8 six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient + pip install -U flake8 codecov pytest pytest-asyncio pytest-cov mpegdash m3u8 async-asgi-testclient pip install -U deffcode if: success() - name: run prepare_dataset_script diff --git a/appveyor.yml b/appveyor.yml index a8edd1388..656054500 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,8 +61,8 @@ install: - "python --version" - "python -m pip install --upgrade pip wheel" - cmd: python -m pip install "numpy<2.0.0" - - "python -m pip install .[asyncio] six codecov httpx yt_dlp aiortc paramiko" - - "python -m pip install pytest pytest-cov pytest-asyncio m3u8 async-asgi-testclient" + - "python -m pip install --upgrade .[asyncio] six httpx yt_dlp aiortc" + - "python -m pip install --upgrade pytest codecov pytest-cov pytest-asyncio m3u8 async-asgi-testclient paramiko" - "python -m pip install --upgrade deffcode" - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev2/mpegdash-0.3.0.dev2-py3-none-any.whl" - cmd: chmod +x scripts/bash/prepare_dataset.sh diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7c1d2013d..93286a688 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,8 +68,8 @@ steps: - script: | python -m pip install -U pip wheel python -m pip install "numpy<2.0.0" - python -m pip install -U .[asyncio] - python -m pip install -U six codecov httpx pytest pytest-asyncio pytest-cov yt_dlp mpegdash paramiko m3u8 async-asgi-testclient + python -m pip install -U .[asyncio] yt_dlp httpx six paramiko + python -m pip install -U codecov pytest pytest-asyncio pytest-cov mpegdash m3u8 async-asgi-testclient python -m pip install -U deffcode displayName: "Install pip dependencies" diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 4f12add16..d1c9256d8 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -29,11 +29,11 @@ limitations under the License. * **DO NOT** feed frames with different dimensions or channels to WriteGear, otherwise WriteGear will exit with `ValueError`. - * While providing additional av-source with `-i` FFmpeg parameter in `output_params` make sure it don't interfere with WriteGear's frame pipeline otherwise it will break things! + * When using the `-i` FFmpeg parameter in `output_params` to provide an additional audio or video source, ensure it **DOES NOT** interfere with WriteGear's internal frame pipeline. Interference can cause the pipeline to break. - * Use [`-disable_force_termination`](../params/#supported-parameters) flag when video duration is too short(<60sec), otherwise WriteGear will not produce any valid output. + * To ensure WriteGear produces valid output when using an additional stream `-i` parameter with videos shorter than `60` seconds, use the [`-disable_force_termination`](../params/#supported-parameters) flag. - * Heavy resolution multimedia files take time to render which can last up to _0.1-1 seconds_. Kindly wait till the WriteGear API terminates itself, and **DO NOT** try to kill the process instead. + * Encoding heavy resolution multimedia files can take up to _~0.2 to 2 seconds_. Please wait for the WriteGear API to terminate itself and **DO NOT** kill the process manually. * Always use `writer.close()` at the very end of the main code. **NEVER USE IT INBETWEEN CODE** to avoid undesired behavior. @@ -47,7 +47,7 @@ limitations under the License. Following is the bare-minimum code you need to get started with WriteGear API in Compression Mode: -```python linenums="1" +```python linenums="1" hl_lines="10 25 42" # import required libraries from vidgear.gears import CamGear from vidgear.gears import WriteGear @@ -168,7 +168,7 @@ WriteGear API provides [`-input_framerate`](../params/#supported-parameters) at In this code we will retrieve framerate from video stream, and set it as `-input_framerate` attribute for `option` parameter in WriteGear API: -```python linenums="1" hl_lines="10" +```python linenums="1" hl_lines="10 13" # import required libraries from vidgear.gears import CamGear from vidgear.gears import WriteGear @@ -584,7 +584,7 @@ In this example code, we will merging the audio from a Audio Device _(for e.g. W !!! warning "You **MUST** use [`-input_framerate`](../params/#supported-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." -```python linenums="1" hl_lines="11-15" +```python linenums="1" hl_lines="11-16" # import required libraries from vidgear.gears import VideoGear from vidgear.gears import WriteGear diff --git a/docs/gears/writegear/non_compression/usage.md b/docs/gears/writegear/non_compression/usage.md index 1ec44f83a..d76c82457 100644 --- a/docs/gears/writegear/non_compression/usage.md +++ b/docs/gears/writegear/non_compression/usage.md @@ -40,7 +40,7 @@ limitations under the License. Following is the bare-minimum code you need to get started with WriteGear API in Non-Compression Mode: -```python linenums="1" +```python linenums="1" hl_lines="10" # import required libraries from vidgear.gears import CamGear from vidgear.gears import WriteGear @@ -97,7 +97,7 @@ In Non-Compression mode, WriteGear API provides flexible control over [**OpenCV' The complete usage example is as follows: -```python linenums="1" +```python linenums="1" hl_lines="7 15" # import required libraries from vidgear.gears import VideoGear from vidgear.gears import WriteGear @@ -109,12 +109,12 @@ output_params = {"-fourcc": "MJPG", "-fps": 30} # open live video stream on webcam at first index(i.e. 0) device stream = VideoGear(source=0, logging=True).start() -# Define writer with defined parameters and suitable output filename for e.g. `Output.mp4` +# Define writer with defined parameters and suitable output filename +# for e.g. `Output.mp4` writer = WriteGear( output="Output.mp4", compression_mode=False, logging=True, **output_params ) - # loop over while True: @@ -159,7 +159,7 @@ writer.close() You can easily use WriterGear API directly with any Video Processing library(_For e.g OpenCV itself_) in Non-Compression Mode. The complete usage example is as follows: -```python linenums="1" +```python linenums="1" hl_lines="9 21 46" # import required libraries from vidgear.gears import WriteGear import cv2 @@ -170,7 +170,8 @@ output_params = {"-fourcc": "MJPG", "-fps": 30} # Open suitable video stream, such as webcam on first index(i.e. 0) stream = cv2.VideoCapture(0) -# Define writer with defined parameters and suitable output filename for e.g. `Output.mp4` +# Define writer with defined parameters and suitable output filename +# for e.g. `Output.mp4` writer = WriteGear( output="Output.mp4", compression_mode=False, logging=True, **output_params ) @@ -227,7 +228,7 @@ writer.close() In this example we will be constructing GStreamer pipeline to write video-frames into a file(`foo.mp4`) at 1M video-bitrate. -```python linenums="1" +```python linenums="1" hl_lines="12-14" # import required libraries from vidgear.gears import WriteGear import cv2 From 0f620e6cbe0da7bff397634c4ee30db542b4dd94 Mon Sep 17 00:00:00 2001 From: Abhishek Thakur Date: Thu, 20 Jun 2024 20:19:17 +0530 Subject: [PATCH 78/81] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Setup.py:=20Fixed?= =?UTF-8?q?=20`simplejpeg`=20requires=20`python>=3D3.9`=20for=20its=20late?= =?UTF-8?q?st=20version.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📌 Pinned `simplejpeg>=1.7.3` to support python-3.8 legacies. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0c0e89feb..fad1aee9c 100644 --- a/setup.py +++ b/setup.py @@ -112,7 +112,7 @@ def latest_version(package_name): "yt_dlp{}".format(latest_version("yt_dlp")), "pyzmq{}".format(latest_version("pyzmq")), "Pillow", - "simplejpeg{}".format(latest_version("simplejpeg")), + "simplejpeg>=1.7.3", # Requires-Python >=3.9 for v1.7.4 "mss{}".format(latest_version("mss")), "pyscreenshot{}".format(latest_version("pyscreenshot")), ] @@ -125,7 +125,7 @@ def latest_version(package_name): "asyncio": [ "yt_dlp{}".format(latest_version("yt_dlp")), "pyzmq{}".format(latest_version("pyzmq")), - "simplejpeg{}".format(latest_version("simplejpeg")), + "simplejpeg>=1.7.3", # Requires-Python >=3.9 for v1.7.4 "mss{}".format(latest_version("mss")), "Pillow", "pyscreenshot{}".format(latest_version("pyscreenshot")), From 7a25a8031a189eb3967be2232e862b56f7981b06 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 Jun 2024 21:08:46 +0530 Subject: [PATCH 79/81] =?UTF-8?q?=E2=9C=A8=20Helper:=20Added=20support=20f?= =?UTF-8?q?or=20SRTP/RTSPS=20in=20`is=5Fvalid=5Furl`=20function=20(Fixes?= =?UTF-8?q?=20#410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Enhanced `is_valid_url` in `helper.py` to recognize and support both `rtsp` and `rtsps` protocols. (Suggested by @jonra1993) - 💬 SRTP/RTSPS extends RTSP/RTP to encrypt video and audio data using the same ciphers as HTTPS, typically AES with a 128-bit key length. --- vidgear/gears/helper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index a7ab5ba85..0535c9391 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -603,8 +603,11 @@ def is_valid_url(path, url=None, logging=False): protocols = check_output([path, "-hide_banner", "-protocols"]) splitted = [x.decode("utf-8").strip() for x in protocols.split(b"\n")] supported_protocols = splitted[splitted.index("Output:") + 1 : len(splitted) - 1] - # rtsp is a demuxer somehow - supported_protocols += ["rtsp"] if "rtsp" in get_supported_demuxers(path) else [] + # RTSP is a demuxer somehow + # support both RTSP and RTSPS(over SSL) + supported_protocols += ( + ["rtsp", "rtsps"] if "rtsp" in get_supported_demuxers(path) else [] + ) # Test and return result whether scheme is supported if extracted_scheme_url and extracted_scheme_url in supported_protocols: logging and logger.debug( From 9e80b7c17afafa3958969ee7afb43fcb4d6227d8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 22 Jun 2024 20:29:46 +0530 Subject: [PATCH 80/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20`changel?= =?UTF-8?q?og.md`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.md | 275 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index 631879f7e..a68d1220f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -20,9 +20,282 @@ limitations under the License. # Release Notes -## v0.3.2 (2023-09-10) +## v0.3.3 (2024-06-22) ???+ tip "New Features" + - [x] **PiGear:** + * ⚡️ Official Support for [**Picamera2**](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf) API backend. (Fixes #342) + + This massive update brings official support for the new Picamera2 API, unlocking powerful features for Raspberry Pi Camera Modules and limited USB camera support. + + **Seamless Python wrapper:** A robust wrapper around Picamera2 API library simplifies integration with minimal code changes for existing PiGear users. + + **Enhanced camera control:** Leverages libcamera API under the hood for Raspberry Pi Camera Modules. + + **Existing compatibility:** Maintains compatibility with PiGear's existing super-charged multi-threaded and colorspace manipulation framework. + + **Proper Resource management:** Ensures proper resource release during PiGear termination. + + **USB camera support (limited):** Provides basic functionality for USB webcams. PiGear could accurately differentiates between USB and Raspberry Pi cameras using metadata. + + **Backward compatibility:** Seamlessly switches to the legacy Picamera library backend if Picamera2 is unavailable. + + **Standalone functionalities:** Standalone functionalities for both legacy `picamera` and newer `picamera2` backends for clarity. + + **Advanced optional parameters handling:** Handles camera configurational parameters and user-defined settings for various camera types. + * **New optional configurational parameters:** Currently Supports `sensor`, `format`, `controls`, `transform`, `stride`, `buffer_count`, and `queue` with sanity checks. + * **New user-defined optional parameters:** Such as `auto_align_output_config`, `enable_verbose_logs`, and more. + - [x] **StreamGear:** + * Introduced new `-enable_force_termination` attribute for immediate FFmpeg process termination. + - [x] **Helper:** + * Added support for SRTP/RTSPS in `is_valid_url` function (Fixes #410) + + Enhanced `is_valid_url` in `helper.py` to recognize and support both `rtsp` and `rtsps` protocols. (Suggested by @jonra1993) + + SRTP/RTSPS extends RTSP/RTP to encrypt video and audio data using the same ciphers as HTTPS, typically AES with a 128-bit key length. + * Added a custom deprecated decorator to mark deprecated functions and parameters to display a warning message when a deprecated one is used. + - [x] **Docs:** + * Overhauled mkdocs material theme: + + Added `unrecognized_links: ignore` to `mkdocs.yml` for validations. + + Added custom admonition icons. + + Added new `git-authors` plugin. + + Added new tables markdown extension. + + Added custom fences to `pymdownx.superfences` markdown extension. + + Added `line_spans: __span` and `pygments_lang_class: true` parameters to `pymdownx.highlight` markdown extension. + + Added `normalize_issue_symbols: true` to `pymdownx.magiclink` markdown extension. + + Added new mkdocs feature dependency `mkdocs-git-authors-plugin`. + * Added the use of new `-enable_force_termination` parameter. + * Added a new FAQ entry about the deprecated `rgb_mode` parameter. + * Added new `screengear_error11.png` asset. + - [x] **CI:** + * Added test cases for `import_dependency_safe` function to validate different scenarios and error handling in `import_dependency_safe`. + +??? success "Updates/Improvements" + - [x] Core: + * Improved exception handling for module imports: + + Updated `import_dependency_safe` in `helper.py`: + * Added specific handling for `ModuleNotFoundError`. + * Included original exception in `ImportError` for better error tracing. + * Enhanced logging to include exception traceback when error is set to "log". + + Enhanced `import_core_dependency` in `__init__.py`: + * Added specific handling for `ModuleNotFoundError`. + * Included original exception in `ImportError` for better error tracing. + * Improved colorspace handling in videocapture gears: + + Logged a warning and discarded invalid colorspace values instead of raising an exception. + + Consolidated colorspace logging into a single line using a ternary operation. + - [x] Asyncio: + * Replaced deprecated Starlette's `on_shutdown` parameter with an async context manager `lifespan` in WebGear and WebGear_RTC APIs. (Fixes #397) + + Moved shutdown logic for VideoGear and peer RTC connections to this new `lifespan` context manager. + + Added new `contextlib` import for using `asynccontextmanager`. + - [x] NetGear_Async API: + * Modified `__init__` method to handle event loop more robustly: + + Try to get the running event loop using `asyncio.get_running_loop()` + + If no running event loop found, create a new one with `asyncio.new_event_loop()` + + Log if creating a new event loop + * Changed launch method to use `self.loop.create_task()` instead of `asyncio.ensure_future()` + + Ensures the task is created using the correct event loop instance. + * Moved the event loop initialization code to an earlier point before setting event loop policy to ensure it is set up correctly before selecting `WindowsSelectorEventLoop` policy. + + On Windows, vidgear requires the `WindowsSelectorEventLoop`, but Python 3.8 and above defaults to the `ProactorEventLoop` which is not compatible. + * Removed redundant python version check to set `WindowsSelectorEventLoop` policy, as minimum supported version is already `3.8`. + * Move event loop setup and policy assignment to the beginning of `__init__` before zmq Context creation. + * Refactored return data handling. + - [x] StreamGear: + * Updated `close()` methods for handling gracefully signal interruptions based on different operating systems with device audio streams. + * Deprecated `terminate()` method, introducing `close()` for safer process termination. + * Enhanced stream copy support in Single Source mode (Fixes #396). + + Moved settings for "-vf" and "-aspect" inside conditional blocks. + + Added warnings and discarded these parameters in stream copy mode. + + Ignored stream copy parameter in Real-time Frames Mode or Custom Streams with appropriate warnings. + + Updated `-acodec` handling: + * Default to `aac` for Custom Streams. + * Use stream copy (`-acodec copy`) for input video’s audio when Custom Streams are disabled. + + Refined `-livestream` parameter usage to Real-time Frames Mode only. + + Adjusted video and audio bitrate assignment to skip when stream copy is enabled. + + Improved log message for `-clear_prev_assets` parameter. + * Restricted `-livestream` parameter to Real-time Frames Mode only. + + Disabled live streaming for video files and updated relevant logging. + * Enhanced warning messages and clarified description. + - [x] PiGear: + * Logging optimization with warning for common `libcamera` messages. + * Lowered `framerate` minimum value to `0.0`. + * Moved `sensor` optional parameter to commonly supported picamera2 configurational parameters. + * Removed unsupported `bit_depth` optional parameters. + * Updated PiGear API tagline and introduction. + - [x] NetGear: + * Enhanced logging and error handling for secure mode. + * Logged Authenticator start/stop events. + * Handled socket session expiration more gracefully in `recv_handler`. + * Ensured proper termination of the ZMQ context and socket when closing the NetGear instance. + - [x] WebGear: + * Enhanced error messages for WebGear auto-generation workflow (Fixes #403) + + Updated `homepage`, `not_found`, and `server_error` methods to include more detailed JSON error messages. + + Added specific error and message prefixes to improve clarity. + - [x] WebGear_RTC: + * Optimized peer connection closure to avoid redundant closures. + * Reduced unnecessary logging by only logging ICE connection state changes when they are not in a "failed" state. + - [x] WriteGear: + * Simplified the logic for formatting output parameters. + * Improved error handling in `execute_ffmpeg_cmd` method: + + Raised `ValueError` with descriptive messages for `BrokenPipeError` or `IOError`. + + Updated error handling per PEP 409 to preserve original exception context or suppress it based on logging settings. + - [x] CamGear: + * Removed GStreamer support check. + * Improved readability of livestream warning logs. + - [x] Setup.py: + * Dropped legacy picamera dependency in `setup.py`. + * Updated setup.py to use the latest `pyzmq` version to address installation issues (Fixes #399). + - [x] Helper: + * Added patch for substring index bug in `get_supported_demuxers` helper method. + * Updated `extract_time` helper function regex to handle milliseconds. + - [x] Docs: + * Update StreamGear documentation: + + Updated documentation to deprecated `terminate()` method, and introducing `close()` for safer process termination. + + Improved the overview section's description and wording. + + Updated usage examples for both Single-Source Mode and Real-time Frames Mode. + + Updated StreamGear usage examples for device audio input. + + Refactored sections for Live Streaming usage. + * Added warning for unsupported `-livestream` parameter in Single-Source Mode. + + Added a tip box on benefits of using stream copy (`-vcodec copy`) for faster HLS/DASH transcoding. + * Highlighted limitations of stream copy, including incompatibility with Real-time Frames Mode and Custom Streams. + * Clarified automatic audio stream copy (`-acodec copy`) usage with input video’s audio stream. + + Updated usage example for device video source. + + Addressed deprecation of the `terminate()` method in favor of the new `close()` method. + + Updated respective notices for the deprecated `terminate()` method and `rgb_mode` parameter. + + Added a deprecation warning admonition for the `rgb_mode` parameter in the `stream()` method. + + Removed the obsolete usage example for deprecation RGB mode with StreamGear. + + Added documentation and usage of the new `-enable_force_termination` parameter. + + Modified the warning message to mention that forced termination can cause corrupted output in certain scenarios. + + Updated the docstring for the `stream()` method and `transcode_source()` method. + + Refactored the StreamGear API Parameters documentation to enhance clarity and readability. + + Refined the description of the `-streams` attribute of the StreamGear API. + * Update PiGear documentation: + + Added a warning advising users to disable common `libcamera` messages when logging is disabled. + + Updated Picamera2 installation instructions _(including `apt`, `pip`, pre-installation on Raspberry Pi images, and compatibility warnings)_ + + Moved legacy Picamera library installation instructions to an admonition. + + Removed Importing section from overview to avoid confusion. + * Update NetGear documentation: + + 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. + + Added warning log for potential issues with `flag=1` (NOBLOCK). (Fixes #390) + * Changed default value of `copy` to `True` in NetGear API documentation. + + Noted that `track` option is ignored when `copy=True`. + * Update WriteGear documentation: + + Updated the documentation for the `-disable_force_termination` parameter. + * Update `README.md`: + + Replaced deprecated options (`frame_jpeg_quality`, `frame_jpeg_optimize`, `frame_jpeg_progressive`) with their newer equivalents (`jpeg_compression_quality,` `jpeg_compression_fastdct`, `jpeg_compression_fastupsample`) in WebGear usage example. + * Update `mkdocs.yml`: + + Set `edit_uri` for GitHub edit links. + + Add new theme features like content actions, tooltips, etc. + + Update palette settings for light/dark mode. + + Enable new markdown extensions. + + Add custom javascript hook support. + + Migrated to new Google Analytics 4. + + Replaced depreciated `materialx `with supported emoji extension. + + Replaced permalink icon with default one. + + Change system mode toggle icon and name in `mkdocs.yml`. + * Improved overall documentation quality by added detailed explanations, practical examples, following best practices, and clearer usage patterns. + * Updated sections, code examples, admonitions, and comments for better readability, consistency, and precision. + * Added missing version contributors to `changelog.md`. + * Added new icons to make headings more readable. + * Replaced unsupported admonitions with supported ones. + * Removed all custom admonition icons and color CSS from `custom.css`. + * Removed Twitter section from help and docs site metadata. + * Updated Zenodo badge and BibTeX entry. + * Added workaround for 'AttributeError: 'DXCamera' object has no attribute 'is_capturing'' error on Windows. + * Remove script tags from `main.html` and use a custom hook for adding javascripts on certain pages. + * Refactored all APIs and bonus examples to use `linenums` and `hl_lines` which makes it easier to highlight specific lines in code blocks. + * Removed Gitter community chat sidecard javascript file. + * Redefined spacing between sections. + * Add failure warning in various docs about `picamera` incompatibility on 64-bit OS. + * Update announcement icon in `main.html`. + * Remove `site.webmanifest` file. + - [x] Maintenance: + * Improved logging, parameter validation, and added descriptive dialogs across various APIs. + * Moved logging enablement before version logging for consistency in vidgear APIs. + * Removed redundant boolean assignment for various APIs internal logging. + * Simplified conditional statements and assignments using short-circuiting, Boolean operations, and ternary operators across various APIs and tests. + * Refactored vidgear code to improve readability, maintainability, and performance. + * Added `.cache` directory to `.gitignore`, + * Updated vidgear library version to `v0.3.3`. + * Improved code efficiency with short-circuiting and formatting. + * Updated logging practices to be more developer-friendly. + * Removed unnecessary parentheses and type checks. + * Removed unused imports. + * Updated code comments. + - [x] CI: + * Temporarily removed PiGear API from code coverage due to hardware limitations. + * Deprecated custom `event_loop` fixture overrides in WebGear_RTC and NetGear_Async tests + + Removed redundant `pytest.mark.asyncio` decorators from several test functions. + * Add a new `event_loop_policy` fixture for pytest to override the event loop policy: + + Added new recommended approach of using `pytest.mark.asyncio(scope="module")` to mark all WebGear_RTC and NetGear_Async tests as asynchronous and utilize the same event loop throughout the module. + + Log the event loop being used for debugging. + * Updated NetGear unit tests to reflect the new default for `copy`. + * Ensured coverage for `raise`, `log`, `silent`, and unknown error types. + * Improved parameterized test cases to support floating point values. + * Updated StreamGear tests to use the new `close()` method instead of the deprecated terminate() method. + * Updated tests of various APIs for better coverage and reliability. + * Enabled `kill=True` in `close()` in NetGear Tests. + * Removed pinned `cryptography==38.0.4` dependency. + * Remove unused imports and code cleanup. + * Rearranged the dependencies. + +??? danger "Breaking Updates/Changes" + - StreamGear: + - [ ] Deprecated `terminate()` method and introduce `close()` method. + + 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. + - [ ] Deprecated `rgb_mode` parameter in `stream()` method. + + This parameter will be removed in a future version, and only BGR format frames will be supported. + - [ ] Restricted `-livestream` parameter to Real-time Frames Mode only. + + 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. + + +??? bug "Bug-fixes" + - [x] PiGear: + * Modify PiGear class behavior when `enforce_legacy_picamera=True` on unsupported system + + Instead of silently disabling `picamera2` API directly, PiGear now raises an error if `picamera` is unavailable or unsupported + + Prevented incorrect initialization of `PiGear` class on unsupported 64-bit OS systems. + * Fixed `UnboundLocalError` bug for 'picamera2' variable assigment. + * Fixed `UnboundLocalError` bug for 'queue' variable assignment. + * Fixed colorspace typo bug. + - [x] StreamGear: + * Fixed incompatibility of stream copy with Real-time Frames Mode. + + Added warnings and discarded `-vcodec copy` when using this mode. + * Removed non-essential aspect ratio parameter to prevent unwanted distortions (Fixes #385). + * Moved handle streaming format to beginning to fix 'StreamGear' object has no attribute '_StreamGear__format' bug. + - [x] NetGear: + * Fixed Secure Mode failing to work on conflicting ZMQ Contexts: + + Handled "Address in use" error more gracefully and disabled secure mode if errors occur. + + Improved handling of ZMQ Authenticator and Certificates. + * Fixed `msg_json` undefined when terminating context in the `recv_handler` method. + - [x] CamGear: + * Fixed logging condition for yt-dlp (Fixes #394) + + Updated `no_warnings` parameter in `CamGear` to be `False` when logging is enabled and `True` otherwise. + - [x] Docs: + * Replaced buggy kofi widget with a button image in `index.md`. + * Fixed Ko-fi sponsorship heart hover effect in footer + + Replaced `twemoji` heart emoji with `heart-pulse` fontawesome SVG + * Fixed titles and indentations in various admonitions. + * Fixed various issues in code comments, and hyperlinks URLs. + * Fixed typos, formatting, code highlighting, and grammar inconsistencies. + * Fixed minor typo in `js_hook.py`. + - [x] CI: + * Fixed simplejpeg and opencv not compatible with `numpy==2.x.x` versions. + + Pinned `numpy<2.0.0` in all CI envs. + * Fixed expected duration value in parameterized test case from `8` to `8.44` since `test_extract_time` function now supports floating point values. + * Fixed `test_secure_mode` NetGear test: + + Added `"127.0.0.1"` address to allow common endpoint for connection. + + Added `"jpeg_compression":False` to disable frame compression, allowing frame to be the same while assertion. + * Fixed `pip install` hash bug in Azure Pipelines CI. + * Fixed various typos and code issues in tests. + * Fixed invalid escape sequence in test case string. + * Fixed python environment bugs in `appveyor.yml`. + +??? question "Pull Requests" + * PR #411 + * PR #409 + * PR #406 + * PR #401 + * PR #398 + * PR #392 + +??? new "New Contributors" + * @jonra1993 + +  + +  + +## v0.3.2 (2023-09-10) + +??? tip "New Features" - [x] **NetGear:** * Added new `kill` parameter to `close()` method to forcefully kill ZMQ context instead of graceful exit only in the `receive` mode. * Added new `subscriber_timeout` integer optional parameter to support timeout with `pattern=2` _(or Publisher-Subscriber)_ pattern. From 0677976d9293327b5b366109832d957c4a3f266d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 22 Jun 2024 20:43:34 +0530 Subject: [PATCH 81/81] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20`README.?= =?UTF-8?q?md`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a0aa327d2..051993e66 100644 --- a/README.md +++ b/README.md @@ -439,9 +439,9 @@ Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** -- **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]_** +- **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]_** -- **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]_** +- **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]_** ### StreamGear API Guide: