From 88658c083cfee1940ff69d985a5562f2edb27beb Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 26 Apr 2021 10:37:24 +0530 Subject: [PATCH 001/112] =?UTF-8?q?=E2=9C=A8=20WebGear=5FRTC:=20Added=20na?= =?UTF-8?q?tive=20support=20for=20middlewares.=20(Fixes=20#208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added new global `middleware` variable to define Middlewares easily by formatting them as list. - Added validity check for Middlewares. - Extended Middlewares support to WebGear API too. - Added related imports. - setup: Fixed minor typo in dependencies. - setup.cfg: Replace dashes with underscores to remove warnings. --- setup.cfg | 2 +- setup.py | 2 +- vidgear/gears/asyncio/webgear.py | 12 ++++++++++++ vidgear/gears/asyncio/webgear_rtc.py | 14 +++++++++++++- vidgear/version.py | 2 +- 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index af5dc27dd..ea5b40312 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ # This includes the license file(s) in the wheel. # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file license_files = LICENSE -description-file = README.md +description_file = README.md [bdist_wheel] # This flag says to generate wheels that support both Python 2 and Python diff --git a/setup.py b/setup.py index 501c78a02..35676efae 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def latest_version(package_name): "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), "pyzmq{}".format(latest_version("pyzmq")), - "simplejpeg".format(latest_version("simplejpeg")), + "simplejpeg{}".format(latest_version("simplejpeg")), "colorlog", "colorama", "tqdm", diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index ecd7e9518..4d5fd17cc 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -31,6 +31,7 @@ from starlette.templating import Jinja2Templates from starlette.staticfiles import StaticFiles from starlette.applications import Starlette +from starlette.middleware import Middleware from .helper import reducer, logger_handler, generate_webdata, create_blank_frame from ..videogear import VideoGear @@ -224,6 +225,8 @@ def __init__( name="static", ), ] + # define middleware support + self.middleware = [] # Handle video source if source is None: self.config = {"generator": None} @@ -266,6 +269,14 @@ def __call__(self): ): raise RuntimeError("[WebGear:ERROR] :: Routing tables are not valid!") + # validate middlewares + assert not (self.middleware is None), "Middlewares are NoneType!" + if self.middleware and ( + not isinstance(self.middleware, list) + or not all(isinstance(x, Middleware) for x in self.middleware) + ): + raise RuntimeError("[WebGear:ERROR] :: Middlewares are not valid!") + # validate assigned frame generator in WebGear configuration if isinstance(self.config, dict) and "generator" in self.config: # check if its assigned value is a asynchronous generator @@ -291,6 +302,7 @@ def __call__(self): return Starlette( debug=(True if self.__logging else False), routes=self.routes, + middleware=self.middleware, exception_handlers=self.__exception_handlers, on_shutdown=[self.shutdown], ) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 24727305b..12651f56d 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -29,6 +29,7 @@ from starlette.templating import Jinja2Templates from starlette.staticfiles import StaticFiles from starlette.applications import Starlette +from starlette.middleware import Middleware from starlette.responses import JSONResponse from aiortc.rtcrtpsender import RTCRtpSender @@ -367,6 +368,9 @@ def __init__( ), ] + # define middleware support + self.middleware = [] + # Handle RTC video server if source is None: self.config = {"server": None} @@ -404,13 +408,20 @@ def __call__(self): Implements a custom Callable method for WeGear_RTC application. """ # validate routing tables - # validate routing tables assert not (self.routes is None), "Routing tables are NoneType!" if not isinstance(self.routes, list) or not all( x in self.routes for x in self.__rt_org_copy ): raise RuntimeError("[WeGear_RTC:ERROR] :: Routing tables are not valid!") + # validate middlewares + assert not (self.middleware is None), "Middlewares are NoneType!" + if self.middleware and ( + not isinstance(self.middleware, list) + or not all(isinstance(x, Middleware) for x in self.middleware) + ): + raise RuntimeError("[WeGear_RTC:ERROR] :: Middlewares are not valid!") + # validate assigned RTC video-server in WeGear_RTC configuration if isinstance(self.config, dict) and "server" in self.config: # check if assigned RTC server class is inherit from `VideoStreamTrack` API.i @@ -445,6 +456,7 @@ def __call__(self): return Starlette( debug=(True if self.__logging else False), routes=self.routes, + middleware=self.middleware, exception_handlers=self.__exception_handlers, on_shutdown=[self.__on_shutdown], ) diff --git a/vidgear/version.py b/vidgear/version.py index 77648b6b7..984fc572f 100644 --- a/vidgear/version.py +++ b/vidgear/version.py @@ -1 +1 @@ -__version__ = "0.2.1" \ No newline at end of file +__version__ = "0.2.2" \ No newline at end of file From 263987fdb7d55ccf7318e98feab2684599e22025 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 May 2021 07:02:32 +0530 Subject: [PATCH 002/112] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Docs:=20Fixed=20ty?= =?UTF-8?q?pos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gears/webgear/usage.md | 2 +- docs/gears/webgear_rtc/advanced.md | 2 +- docs/gears/webgear_rtc/usage.md | 2 +- docs/help/camgear_faqs.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/gears/webgear/usage.md b/docs/gears/webgear/usage.md index 2a24c69a3..53281dedc 100644 --- a/docs/gears/webgear/usage.md +++ b/docs/gears/webgear/usage.md @@ -85,7 +85,7 @@ Let's implement our Bare-Minimum usage example with these [**Performance Enhanci You can access and run WebGear VideoStreamer Server programmatically in your python script in just a few lines of code, as follows: -!!! tip "For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](./../../../help/webgear_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear)" +!!! tip "For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../../help/webgear_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear)" ```python diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index 2a1fc2346..ff7918947 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -34,7 +34,7 @@ Let's implement a bare-minimum example using WebGear_RTC as Real-time Broadcaste !!! info "[`enable_infinite_frames`](../params/#webgear_rtc-specific-attributes) is enforced by default with this(`enable_live_broadcast`) attribute." -!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](./../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" +!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" ```python # import required libraries diff --git a/docs/gears/webgear_rtc/usage.md b/docs/gears/webgear_rtc/usage.md index 6b5a322ce..bce579ac5 100644 --- a/docs/gears/webgear_rtc/usage.md +++ b/docs/gears/webgear_rtc/usage.md @@ -48,7 +48,7 @@ Let's implement a Bare-Minimum usage example: You can access and run WebGear_RTC VideoStreamer Server programmatically in your python script in just a few lines of code, as follows: -!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](./../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" +!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" !!! info "We are using `frame_size_reduction` attribute for frame size reduction _(in percentage)_ to be streamed with its [`options`](../params/#options) dictionary parameter to cope with performance-throttling in this example." diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index cd62bd3a5..de516e2fa 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -131,7 +131,7 @@ You can open any local network stream _(such as RTSP)_ just by providing its URL os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" ``` - Finally, use [`backend`](../../gears/camgear/params/#backend) parameter value as `backend="CAP_FFMPEG"` in CamGear. + Finally, use [`backend`](../../gears/camgear/params/#backend) parameter value as `backend=cv2.CAP_FFMPEG` in CamGear. ```python From 27ff513a315e95a7edcf3454543f58136a982167 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 1 May 2021 10:25:50 +0530 Subject: [PATCH 003/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20New=20Updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed 404 page does not work outside the site root with mkdocs.. - Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. - Fixed markdown files comments not stripped when converted to HTML. - Added `pymdownx.striphtml` plugin to strip comments. - re-positioned few docs comments at bottom for easy detection. --- docs/overrides/404.html | 27 +++++++++++++------------- docs/overrides/main.html | 42 ++++++++++++++++++++-------------------- mkdocs.yml | 11 +++++------ 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/docs/overrides/404.html b/docs/overrides/404.html index be90f793f..5da42d91b 100644 --- a/docs/overrides/404.html +++ b/docs/overrides/404.html @@ -1,3 +1,16 @@ +{% extends "main.html" %} +{% block content %} +

404

+
+Lost +
+

+Look like you're lost +

+

the page you are looking for is not available!

+{% endblock %} + + - -{% extends "main.html" %} -{% block content %} -

404

-
-Lost -
-

-Look like you're lost -

-

the page you are looking for is not available!

-{% endblock %} +--> \ No newline at end of file diff --git a/docs/overrides/main.html b/docs/overrides/main.html index f42807eba..db86e4201 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,23 +1,3 @@ - - {% extends "base.html" %} {% block extrahead %} {% set title = config.site_name %} @@ -46,4 +26,24 @@ -{% endblock %} \ No newline at end of file +{% endblock %} + + \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 3ea353ec9..57f089c42 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ # Project information site_name: VidGear -site_url: https://abhitronix.github.io/vidgear/ +site_url: https://abhitronix.github.io/vidgear site_author: Abhishek Thakur site_description: >- A High-Performance Video-Processing Python Framework for building complex real-time media applications. 🔥 @@ -35,10 +35,6 @@ theme: name: material custom_dir: docs/overrides - # Don't include MkDocs' JavaScript - include_search_page: false - search_index_only: true - # Default values, taken from mkdocs_theme.yml language: en features: @@ -103,6 +99,7 @@ extra: manifest: site.webmanifest version: provider: mike + homepage: https://abhitronix.github.io/vidgear extra_css: @@ -118,8 +115,8 @@ markdown_extensions: - attr_list - def_list - footnotes - - meta - md_in_html + - meta - toc: permalink: ⚓ slugify: !!python/name:pymdownx.slugs.uslugify @@ -150,6 +147,8 @@ markdown_extensions: - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde + - pymdownx.striphtml: + strip_comments: true # Page tree nav: From ba90887bcb90fda24aa67fba4f0dc3a1251bd738 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 May 2021 07:26:03 +0530 Subject: [PATCH 004/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20example?= =?UTF-8?q?=20for=20WebGear=20and=20WebGear=5FRTC=20middlewares=20support.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_docs.yml | 4 +-- docs/gears/webgear/advanced.md | 56 ++++++++++++++++++++++++++++++ docs/gears/webgear_rtc/advanced.md | 54 ++++++++++++++++++++++++++++ docs/help/webgear_faqs.md | 6 ++++ docs/help/webgear_rtc_faqs.md | 7 ++++ 5 files changed, 125 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 48c92b485..0d442fabf 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -150,8 +150,8 @@ jobs: if: success() - name: mike deploy docs dev run: | - echo "${{ env.NAME_DEV }}" - mike deploy --push --update-aliases ${{ env.NAME_DEV }} dev + echo "Releasing ${{ env.NAME_DEV }}" + mike deploy --push dev env: NAME_DEV: "v${{ env.RELEASE_NAME }}-dev" if: success() diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index 28940c432..1e46717ad 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -195,6 +195,62 @@ web.shutdown()   +## Using WebGear with MiddleWares + +WebGear natively supports ASGI middleware classes with Starlette for implementing behavior that is applied across your entire ASGI application easily. + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! info "All supported middlewares can be [here ➶](https://www.starlette.io/middleware/)" + +For this example, let's use [`CORSMiddleware`](https://www.starlette.io/middleware/#corsmiddleware) for implementing appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to outgoing responses in our application in order to allow cross-origin requests from browsers, as follows: + +!!! danger "The default parameters used by the CORSMiddleware implementation are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context." + +!!! tip "Starlette provides several arguments for enabling origins, methods, or headers for CORSMiddleware API. More information can be found [here ➶](https://www.starlette.io/middleware/#corsmiddleware)" + +```python +# import libs +import uvicorn, asyncio +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from vidgear.gears.asyncio import WebGear + +# add various performance tweaks as usual +options = { + "frame_size_reduction": 40, + "frame_jpeg_quality": 80, + "frame_jpeg_optimize": True, + "frame_jpeg_progressive": False, +} + +# initialize WebGear app with a valid source +web = WebGear( + source="/home/foo/foo1.mp4", logging=True, **options +) # enable source i.e. `test.mp4` and enable `logging` for debugging + +# define and assign suitable cors middlewares +web.middleware = [ + Middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) +] + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` +**And that's all, Now you can see output at [`http://localhost:8000`](http://localhost:8000) address.** + +  + ## Rules for Altering WebGear Files and Folders WebGear gives us complete freedom of altering data files generated in [**Auto-Generation Process**](../overview/#auto-generation-process), But you've to keep the following rules in mind: diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index ff7918947..f53a53060 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -255,6 +255,60 @@ web.shutdown()   +## Using WebGear_RTC with MiddleWares + +WebGear_RTC also natively supports ASGI middleware classes with Starlette for implementing behavior that is applied across your entire ASGI application easily. + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! info "All supported middlewares can be [here ➶](https://www.starlette.io/middleware/)" + +For this example, let's use [`CORSMiddleware`](https://www.starlette.io/middleware/#corsmiddleware) for implementing appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to outgoing responses in our application in order to allow cross-origin requests from browsers, as follows: + +!!! danger "The default parameters used by the CORSMiddleware implementation are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context." + +!!! tip "Starlette provides several arguments for enabling origins, methods, or headers for CORSMiddleware API. More information can be found [here ➶](https://www.starlette.io/middleware/#corsmiddleware)" + +```python +# import libs +import uvicorn, asyncio +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware +from vidgear.gears.asyncio import WebGear_RTC + +# add various performance tweaks as usual +options = { + "frame_size_reduction": 25, +} + +# initialize WebGear_RTC app with a valid source +web = WebGear_RTC( + source="/home/foo/foo1.mp4", logging=True, **options +) # enable source i.e. `test.mp4` and enable `logging` for debugging + +# define and assign suitable cors middlewares +web.middleware = [ + Middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) +] + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +**And that's all, Now you can see output at [`http://localhost:8000`](http://localhost:8000) address.** + +  + ## Rules for Altering WebGear_RTC Files and Folders WebGear_RTC gives us complete freedom of altering data files generated in [**Auto-Generation Process**](../overview/#auto-generation-process), But you've to keep the following rules in mind: diff --git a/docs/help/webgear_faqs.md b/docs/help/webgear_faqs.md index 6616fcf61..43e00d79b 100644 --- a/docs/help/webgear_faqs.md +++ b/docs/help/webgear_faqs.md @@ -72,6 +72,12 @@ For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"   +## How can to add CORS headers to WebGear? + +**Answer:** See [this usage example ➶](../../gears/webgear/advanced/#using-webgear-with-middlewares). + +  + ## Can I change the default location? **Answer:** Yes, you can use WebGear's [`custom_data_location`](../../gears/webgear/params/#webgear-specific-attributes) attribute of `option` parameter in WebGear API, to change [default location](../../gears/webgear/overview/#default-location) to somewhere else. diff --git a/docs/help/webgear_rtc_faqs.md b/docs/help/webgear_rtc_faqs.md index fd4212254..71be6bda9 100644 --- a/docs/help/webgear_rtc_faqs.md +++ b/docs/help/webgear_rtc_faqs.md @@ -84,6 +84,13 @@ For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.   +## How can to add CORS headers to WebGear_RTC? + +**Answer:** See [this usage example ➶](../../gears/webgear_rtc/advanced/#using-webgear_rtc-with-middlewares). + +  + + ## Can I change the default location? **Answer:** Yes, you can use WebGear_RTC's [`custom_data_location`](../../gears/webgear_rtc/params/#webgear_rtc-specific-attributes) attribute of `option` parameter in WebGear_RTC API, to change [default location](../../gears/webgear_rtc/overview/#default-location) to somewhere else. From afc8a016207c142185801285e6c57fe10ea71ffa Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 May 2021 07:42:24 +0530 Subject: [PATCH 005/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Added=20tests=20fo?= =?UTF-8?q?r=20WebGear=20and=20WebGear=5FRTC=20APIs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asyncio_tests/test_webgear.py | 26 +++++++++++++++++++ .../asyncio_tests/test_webgear_rtc.py | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index 281f09287..4b9fb7618 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -27,6 +27,8 @@ import requests import tempfile from starlette.routing import Route +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware from starlette.responses import PlainTextResponse from starlette.testclient import TestClient @@ -175,6 +177,30 @@ def test_webgear_custom_server_generator(generator, result): pytest.fail(str(e)) +test_data_class = [ + (None, False), + ([Middleware(CORSMiddleware, allow_origins=["*"])], True), + ([Route("/hello", endpoint=hello_webpage)], False), # invalid value +] + + +@pytest.mark.parametrize("middleware, result", test_data_class) +def test_webgear_custom_middleware(middleware, result): + """ + Test for WebGear API's custom middleware + """ + try: + web = WebGear(source=return_testvideo_path(), logging=True) + web.middleware = middleware + client = TestClient(web(), raise_server_exceptions=True) + response = client.get("/") + assert response.status_code == 200 + web.shutdown() + except Exception as e: + if result: + pytest.fail(str(e)) + + def test_webgear_routes(): """ Test for WebGear API's custom routes 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 1c868d0f2..8a4c57154 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -29,6 +29,8 @@ import json, time from starlette.routing import Route from starlette.responses import PlainTextResponse +from starlette.middleware import Middleware +from starlette.middleware.cors import CORSMiddleware from starlette.testclient import TestClient from aiortc import ( MediaStreamTrack, @@ -353,6 +355,30 @@ def test_webgear_rtc_custom_server_generator(server, result): web.shutdown() +test_data_class = [ + (None, False), + ([Middleware(CORSMiddleware, allow_origins=["*"])], True), + ([Route("/hello", endpoint=hello_webpage)], False), # invalid value +] + + +@pytest.mark.parametrize("middleware, result", test_data_class) +def test_webgear_rtc_custom_middleware(middleware, result): + """ + Test for WebGear_RTC API's custom middleware + """ + try: + web = WebGear_RTC(source=return_testvideo_path(), logging=True) + web.middleware = middleware + client = TestClient(web(), raise_server_exceptions=True) + response = client.get("/") + assert response.status_code == 200 + web.shutdown() + except Exception as e: + if result: + pytest.fail(str(e)) + + def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes From a824e1371a028218aef5841c9348adc95b7ed835 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 May 2021 07:59:49 +0530 Subject: [PATCH 006/112] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Setup:=20Remove?= =?UTF-8?q?d=20`latest=5Fversion`=20support=20from=20`simplejpeg`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35676efae..e0b2ee594 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def latest_version(package_name): "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), "pyzmq{}".format(latest_version("pyzmq")), - "simplejpeg{}".format(latest_version("simplejpeg")), + "simplejpeg", "colorlog", "colorama", "tqdm", From ee2c872840b1b40d5a4b0f526aeaac830802d6d4 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 May 2021 09:07:20 +0530 Subject: [PATCH 007/112] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Updated=20`VidGear?= =?UTF-8?q?=20Docs=20Deployer`=20Workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_docs.yml | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 0d442fabf..5c5cb0e9d 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -63,8 +63,8 @@ jobs: if: success() - name: mike deploy docs release run: | - echo "${{ env.NAME_RELEASE }}" - mike deploy --push --update-aliases ${{ env.NAME_RELEASE }} ${{ env.RELEASE_NAME }} + echo "Releasing ${{ env.NAME_RELEASE }}" + mike deploy --push ${{ env.NAME_RELEASE }} env: NAME_RELEASE: "v${{ env.RELEASE_NAME }}-release" if: success() @@ -101,14 +101,10 @@ jobs: echo "RELEASE_NAME=$(python -c 'import vidgear; print(vidgear.__version__)')" >>$GITHUB_ENV shell: bash if: success() - - name: mike remove previous stable - run: | - mike delete --push latest - if: success() - name: mike deploy docs stable run: | - echo "${{ env.NAME_STABLE }}" - mike deploy --push --update-aliases ${{ env.NAME_STABLE }} latest + echo "Releasing ${{ env.NAME_STABLE }}" + mike deploy --push --update-aliases latest mike set-default --push latest env: NAME_STABLE: "v${{ env.RELEASE_NAME }}-stable" @@ -151,7 +147,7 @@ jobs: - name: mike deploy docs dev run: | echo "Releasing ${{ env.NAME_DEV }}" - mike deploy --push dev + mike deploy --push --update-aliases dev env: NAME_DEV: "v${{ env.RELEASE_NAME }}-dev" if: success() From 52b957b58001c7807bdcbecdb6a21b8a7700346d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 2 May 2021 09:16:34 +0530 Subject: [PATCH 008/112] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20bugs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_docs.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 5c5cb0e9d..4286afc17 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -63,8 +63,8 @@ jobs: if: success() - name: mike deploy docs release run: | - echo "Releasing ${{ env.NAME_RELEASE }}" - mike deploy --push ${{ env.NAME_RELEASE }} + echo "${{ env.NAME_RELEASE }}" + mike deploy --push --update-aliases ${{ env.NAME_RELEASE }} ${{ env.RELEASE_NAME }} --title=${{ env.RELEASE_NAME }} env: NAME_RELEASE: "v${{ env.RELEASE_NAME }}-release" if: success() @@ -103,8 +103,8 @@ jobs: if: success() - name: mike deploy docs stable run: | - echo "Releasing ${{ env.NAME_STABLE }}" - mike deploy --push --update-aliases latest + echo "${{ env.NAME_STABLE }}" + mike deploy --push --update-aliases ${{ env.NAME_STABLE }} latest --title=latest mike set-default --push latest env: NAME_STABLE: "v${{ env.RELEASE_NAME }}-stable" @@ -147,7 +147,7 @@ jobs: - name: mike deploy docs dev run: | echo "Releasing ${{ env.NAME_DEV }}" - mike deploy --push --update-aliases dev + mike deploy --push --update-aliases ${{ env.NAME_DEV }} dev --title=dev env: NAME_DEV: "v${{ env.RELEASE_NAME }}-dev" if: success() From a5faa873d645fdc2bc5c2fdc9162abfc727d53c6 Mon Sep 17 00:00:00 2001 From: Abhishek Thakur Date: Sun, 2 May 2021 11:46:04 +0530 Subject: [PATCH 009/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20`mkdoc?= =?UTF-8?q?s.yml`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mkdocs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 57f089c42..3b080a068 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ # Project information site_name: VidGear -site_url: https://abhitronix.github.io/vidgear +site_url: https://abhitronix.github.io/vidgear/ site_author: Abhishek Thakur site_description: >- A High-Performance Video-Processing Python Framework for building complex real-time media applications. 🔥 @@ -99,7 +99,6 @@ extra: manifest: site.webmanifest version: provider: mike - homepage: https://abhitronix.github.io/vidgear extra_css: From 629b0b10c1d3d77c8be762b81d6d96160c1823f9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 6 May 2021 09:22:06 +0530 Subject: [PATCH 010/112] :bug: Gears: Critical Bugfix related to OpenCV Binaries import. - Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. - Replaced `packaging.parse_version` with more robust `distutils.version`. - Removed redundant imports. --- setup.py | 6 +++--- vidgear/gears/asyncio/helper.py | 1 - vidgear/gears/camgear.py | 1 - vidgear/gears/helper.py | 8 ++++---- vidgear/gears/netgear.py | 1 - vidgear/gears/pigear.py | 1 - vidgear/gears/screengear.py | 1 - vidgear/gears/streamgear.py | 1 - vidgear/gears/writegear.py | 1 - 9 files changed, 7 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index e0b2ee594..c9f224162 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ import setuptools import urllib.request -from pkg_resources import parse_version +from distutils.version import LooseVersion from distutils.util import convert_path from setuptools import setup @@ -40,10 +40,10 @@ def test_opencv(): import cv2 # check whether OpenCV Binaries are 3.x+ - if parse_version(cv2.__version__) < parse_version("3"): + if LooseVersion(cv2.__version__) < LooseVersion("3"): raise ImportError( "Incompatible (< 3.0) OpenCV version-{} Installation found on this machine!".format( - parse_version(cv2.__version__) + LooseVersion(cv2.__version__) ) ) except ImportError: diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index 14503746a..ad5cd75a1 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -34,7 +34,6 @@ import requests from tqdm import tqdm from colorlog import ColoredFormatter -from pkg_resources import parse_version from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index b973e11ce..7ba087591 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -24,7 +24,6 @@ import queue import logging as log from threading import Thread, Event -from pkg_resources import parse_version from .helper import ( capPropId, diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index a406ad167..7e467b20c 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -33,7 +33,7 @@ import requests from tqdm import tqdm from colorlog import ColoredFormatter -from pkg_resources import parse_version +from distutils.version import LooseVersion from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry @@ -42,13 +42,13 @@ import cv2 # check whether OpenCV Binaries are 3.x+ - if parse_version(cv2.__version__) < parse_version("3"): + if LooseVersion(cv2.__version__) < LooseVersion("3"): raise ImportError( "[Vidgear:ERROR] :: Installed OpenCV API version(< 3.0) is not supported!" ) except ImportError: raise ImportError( - "[Vidgear:ERROR] :: Failed to detect correct OpenCV executables, install it with `pip3 install opencv-python` command." + "[Vidgear:ERROR] :: Failed to detect correct OpenCV executables, install it with `pip install opencv-python` command." ) @@ -149,7 +149,7 @@ def check_CV_version(): **Returns:** OpenCV's version first bit """ - if parse_version(cv2.__version__) >= parse_version("4"): + if LooseVersion(cv2.__version__) >= LooseVersion("4"): return 4 else: return 3 diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 1163d8f94..ecb8433d1 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -28,7 +28,6 @@ import logging as log from threading import Thread from collections import deque -from pkg_resources import parse_version from .helper import logger_handler, generate_auth_certificates, check_WriteAccess diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index f5757671a..a1d9fae7e 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -25,7 +25,6 @@ import time import logging as log from threading import Thread -from pkg_resources import parse_version from .helper import capPropId, logger_handler diff --git a/vidgear/gears/screengear.py b/vidgear/gears/screengear.py index 5d7805469..e5995f9b8 100644 --- a/vidgear/gears/screengear.py +++ b/vidgear/gears/screengear.py @@ -28,7 +28,6 @@ import pyscreenshot as pysct from threading import Thread, Event from collections import deque, OrderedDict -from pkg_resources import parse_version from mss.exception import ScreenShotError from pyscreenshot.err import FailedBackendError diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 8e6f00338..249f31840 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -28,7 +28,6 @@ import subprocess as sp from tqdm import tqdm from fractions import Fraction -from pkg_resources import parse_version from collections import OrderedDict from .helper import ( diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index becbadd39..588679bab 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -25,7 +25,6 @@ import time import logging as log import subprocess as sp -from pkg_resources import parse_version from .helper import ( capPropId, From 71c4e1fed3674a80fb264a5f0164c5a14d0a0f40 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 11 May 2021 10:48:53 +0530 Subject: [PATCH 011/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20404=20?= =?UTF-8?q?page=20and=20workflow.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy_docs.yml | 6 +- docs/overrides/404.html | 63 ++++++++++--- docs/overrides/assets/stylesheets/custom.css | 97 ++++++++++++++++++++ 3 files changed, 152 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 4286afc17..799047213 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -64,7 +64,7 @@ jobs: - name: mike deploy docs release run: | echo "${{ env.NAME_RELEASE }}" - mike deploy --push --update-aliases ${{ env.NAME_RELEASE }} ${{ env.RELEASE_NAME }} --title=${{ env.RELEASE_NAME }} + mike deploy --push --update-aliases --no-redirect ${{ env.NAME_RELEASE }} ${{ env.RELEASE_NAME }} --title=${{ env.RELEASE_NAME }} env: NAME_RELEASE: "v${{ env.RELEASE_NAME }}-release" if: success() @@ -104,7 +104,7 @@ jobs: - name: mike deploy docs stable run: | echo "${{ env.NAME_STABLE }}" - mike deploy --push --update-aliases ${{ env.NAME_STABLE }} latest --title=latest + mike deploy --push --update-aliases --no-redirect ${{ env.NAME_STABLE }} latest --title=latest mike set-default --push latest env: NAME_STABLE: "v${{ env.RELEASE_NAME }}-stable" @@ -147,7 +147,7 @@ jobs: - name: mike deploy docs dev run: | echo "Releasing ${{ env.NAME_DEV }}" - mike deploy --push --update-aliases ${{ env.NAME_DEV }} dev --title=dev + mike deploy --push --update-aliases --no-redirect ${{ env.NAME_DEV }} dev --title=dev env: NAME_DEV: "v${{ env.RELEASE_NAME }}-dev" if: success() diff --git a/docs/overrides/404.html b/docs/overrides/404.html index 5da42d91b..6393a8f9a 100644 --- a/docs/overrides/404.html +++ b/docs/overrides/404.html @@ -1,15 +1,56 @@ -{% extends "main.html" %} -{% block content %} +{% extends "main.html" %} {% block content %}

404

-
-Lost -
-

-Look like you're lost -

-

the page you are looking for is not available!

-{% endblock %} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

UH OH! You're lost.

+

The page you are looking for does not exist. How you got here is a mystery. But you can click the button below to go back to the homepage.

+ {% endblock %} + +# SSH Tunneling Mode for NetGear API + +
+ NetGear's SSH Tunneling Mode +
NetGear's SSH Tunneling Mode
+
+ +## Overview + +!!! new "New in v0.2.2" + This document was added in `v0.2.2`. + + +SSH Tunneling Mode allows you to connect NetGear client and server via secure SSH connection over the untrusted network and access its intranet services across firewalls. This mode works with pyzmq's [`zmq.ssh`](https://github.com/zeromq/pyzmq/tree/main/zmq/ssh) module for tunneling ZeroMQ connections over ssh. + +This mode implements [SSH Remote Port Forwarding](https://www.ssh.com/academy/ssh/tunneling/example) which enables accessing Host(client) machine outside the network by exposing port to the public Internet. Thereby, once you have established the tunnel, connections to local machine will actually be connections to remote machine as seen from the server. + +??? danger "Beware ☠️" + Cybercriminals or malware could exploit SSH tunnels to hide their unauthorized communications, or to exfiltrate stolen data from the network. More information can be found [here ➶](https://www.ssh.com/academy/ssh/tunneling) + +All patterns are valid for this mode and it can be easily activated in NetGear API at server end through `ssh_tunnel_mode` string attribute of its [`options`](../../params/#options) dictionary parameter during initialization. + +!!! warning "Important" + * ==SSH tunneling can only be enabled on Server-end to establish remote SSH connection with Client.== + * SSH tunneling Mode is **NOT** compatible with [Multi-Servers](../../advanced/multi_server) and [Multi-Clients](../../advanced/multi_client) Exclusive Modes yet. + +!!! tip "Useful Tips" + * It is advise to use `pattern=2` to overcome random disconnection due to delays in network. + * SSH tunneling Mode is fully supports [Bidirectional Mode](../../advanced/multi_server), [Secure Mode](../../advanced/secure_mode/) and [JPEG-Frame Compression](../../advanced/compression/). + * It is advised to enable logging (`logging = True`) on the first run, to easily identify any runtime errors. + +  + + +## Requirements + +SSH Tunnel Mode requires [`pexpect`](http://www.noah.org/wiki/pexpect) or [`paramiko`](http://www.lag.net/paramiko/) as an additional dependency which is not part of standard VidGear package. It can be easily installed via pypi as follows: + + +=== "Pramiko" + + !!! success "`paramiko` is compatible with all platforms." + + !!! info "`paramiko` support is automatically enabled in ZeroMQ if installed." + + ```sh + # install paramiko + pip install paramiko + ``` + +=== "Pexpect" + + !!! fail "`pexpect` is NOT compatible with Windows Machines." + + ```sh + # install pexpect + pip install pexpect + ``` + +  + +## Supported Attributes + + +!!! warning "All these attributes will work on Server end only whereas Client end will simply discard them." + +For implementing SSH Tunneling Mode, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter: + +* **`ssh_tunnel_mode`** (_string_) : This attribute activates SSH Tunneling Mode and sets the fully specified `"@:"` 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 SSH URL or not, and if it fails _(i.e. port is closed)_, NetGear will throw `AssertionError`!" + + === "With Default Port" + !!! info "The `port` value in SSH URL is the forwarded port on host(client) machine. Its default value is `22`_(meaning default SSH port is forwarded)_." + + ```python + # activates SSH Tunneling and assign SSH URL + options = {"ssh_tunnel_mode":"userid@52.194.1.73"} + # only connections from the public IP address 52.194.1.73 on default port 22 are allowed + ``` + + === "With Custom Port" + !!! quote "You can also define your custom forwarded port instead." + + ```python + # activates SSH Tunneling and assign SSH URL + options = {"ssh_tunnel_mode":"userid@52.194.1.73:8080"} + # only connections from the public IP address 52.194.1.73 on custom port 8080 are allowed + ``` + +* **`ssh_tunnel_pwd`** (_string_): This attribute sets the password required to authorize Host for SSH Connection at Server end. This password grant access and controls SSH user can access what. It can be used as follows: + + ```python + # set password for our SSH conection + options = { + "ssh_tunnel_mode":"userid@52.194.1.73", + "ssh_tunnel_pwd":"mypasswordstring", + } + ``` + +* **`ssh_tunnel_keyfile`** (_string_): This attribute sets path to Host key that provide another way to authenticate host for SSH Connection at Server end. Its purpose is to prevent man-in-the-middle attacks. Certificate-based host authentication can be a very attractive alternative in large organizations. It allows device authentication keys to be rotated and managed conveniently and every connection to be secured. It can be used as follows: + + !!! tip "You can use [Ssh-keygen](https://www.ssh.com/academy/ssh/keygen) tool for creating new authentication key pairs for SSH Tunneling." + + ```python + # set keyfile path for our SSH conection + options = { + "ssh_tunnel_mode":"userid@52.194.1.73", + "ssh_tunnel_keyfile":"/home/foo/.ssh/id_rsa", + } + ``` + + +  + + +## Usage Example + + +??? alert "Assumptions for this Example" + + In this particular example, we assume that: + + - **Server:** + * [x] Server end is a **Raspberry Pi** with USB camera connected to it. + * [x] Server is located at remote location and outside the Client's network. + + - **Client:** + * [x] Client end is a **Regular PC/Computer** located at `52.155.1.89` public IP address for displaying frames received from the remote Server. + * [x] This Client is Port Forwarded by its Router to a default SSH Port(22), which allows Server to connect to its TCP port `22` remotely. This connection will then be tunneled back to our PC/Computer(Client) and makes TCP connection to it again via port `22` on localhost(`127.0.0.1`). + * [x] Also, there's a username `test` present on the PC/Computer(Client) to SSH login with password `pas$wd`. + + - **Setup Diagram:** + + Assumed setup can be visualized throw diagram as follows: + + ![Placeholder](../../../../assets/images/ssh_tunnel_ex.png){ loading=lazy } + + + +??? question "How to Port Forward in Router" + + For more information on Forwarding Port in Popular Home Routers. See [this document ➶](https://www.noip.com/support/knowledgebase/general-port-forwarding-guide/)" + + + +#### Client's End + +Open a terminal on Client System _(A Regular PC where you want to display the input frames received from the Server)_ and execute the following python code: + + +!!! warning "Prerequisites for Client's End" + + To ensure a successful Remote NetGear Connection with Server: + + * **Install OpenSSH Server: (Tested)** + + === "On Linux" + + ```sh + # Debian-based + sudo apt-get install openssh-server + + # RHEL-based + sudo yum install openssh-server + ``` + + === "On Windows" + + See [this official Microsoft doc ➶](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse) + + + === "On OSX" + + ```sh + brew install openssh + ``` + + * Make sure to note down the Client's public IP address required by Server end. Use https://www.whatismyip.com/ to determine it. + + * Make sure that Client Machine is Port Forward by its Router to expose it to the public Internet. Also, this forwarded port value is needed at Server end." + + +??? fail "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. + + **Solution:** You need to change `GatewayPorts no` option to `GatewayPorts yes` in the **OpenSSH server configuration file** [`sshd_config`](https://www.ssh.com/ssh/sshd_config/) to allows anyone to connect to the forwarded ports on Client Machine. + + +??? tip "Enabling Dynamic DNS" + SSH tunneling requires public IP address to able to access host on public Internet. Thereby, if it's troublesome to remember Public IP address or your IP address change constantly, then you can use dynamic DNS services like https://www.noip.com/ + +!!! info "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import required libraries +from vidgear.gears import NetGear +import cv2 + +# Define NetGear Client at given IP address and define parameters +client = NetGear( + address="127.0.0.1", # don't change this + port="5454", + pattern=2, + receive_mode=True, + logging=True, +) + +# 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() +``` + +  + +#### Server's End + +Now, Open the terminal on Remote Server System _(A Raspberry Pi with a webcam connected to it at index `0`)_, and execute the following python code: + +!!! danger "Make sure to replace the SSH URL in following example with yours." + +!!! warning "On Server end, NetGear automatically validates if the `port` is open at specified SSH URL or not, and if it fails _(i.e. port is closed)_, NetGear will throw `AssertionError`!" + +!!! info "You can terminate stream on both side anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import NetGear + +# activate SSH tunneling with SSH URL, and +# [BEWARE!!!] Change SSH URL and SSH password with yours for this example !!! +options = { + "ssh_tunnel_mode": "test@52.155.1.89", # defaults to port 22 + "ssh_tunnel_pwd": "pas$wd", +} + +# Open live video stream on webcam at first index(i.e. 0) device +stream = VideoGear(source=0).start() + +# Define NetGear server at given IP address and define parameters +server = NetGear( + address="127.0.0.1", # don't change this + port="5454", + pattern=2, + logging=True, + **options +) + +# loop over until KeyBoard Interrupted +while True: + + try: + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to server + server.send(frame) + + except KeyboardInterrupt: + break + +# safely close video stream +stream.stop() + +# safely close server +server.close() +``` + +  \ No newline at end of file diff --git a/docs/gears/netgear/overview.md b/docs/gears/netgear/overview.md index 86e4a73cc..e9527712f 100644 --- a/docs/gears/netgear/overview.md +++ b/docs/gears/netgear/overview.md @@ -77,6 +77,9 @@ In addition to the primary modes, NetGear API also offers applications-specific * **Bidirectional Mode:** _This exclusive mode ==provides seamless support for bidirectional data transmission between between Server and Client along with video frames==. Using this mode, the user can now send or receive any data(of any datatype) between Server and Client easily in real-time. **You can learn more about this mode [here ➶](../advanced/bidirectional_mode/).**_ +* **SSH Tunneling Mode:** _This exclusive mode ==allows you to connect NetGear client and server via secure SSH connection over the untrusted network== and access its intranet services across firewalls. This mode implements SSH Remote Port Forwarding which enables accessing Host(client) machine outside the network by exposing port to the public Internet. **You can learn more about this mode [here ➶](../advanced/ssh_tunnel/).**_ + + * **Secure Mode:** _In this exclusive mode, NetGear API ==provides easy access to powerful, smart & secure ZeroMQ's Security Layers== that enables strong encryption on data, and unbreakable authentication between the Server and Client with the help of custom certificates/keys that brings cheap, standardized privacy and authentication for distributed systems over the network. **You can learn more about this mode [here ➶](../advanced/secure_mode/).**_ diff --git a/docs/gears/netgear/params.md b/docs/gears/netgear/params.md index 1bc72ecfb..637768715 100644 --- a/docs/gears/netgear/params.md +++ b/docs/gears/netgear/params.md @@ -149,24 +149,28 @@ This parameter provides the flexibility to alter various NetGear API's internal * **`bidirectional_mode`** (_boolean_) : This internal attribute activates the exclusive [**Bidirectional Mode**](../advanced/bidirectional_mode/), if enabled(`True`). + * **`ssh_tunnel_mode`** (_string_) : This internal attribute activates the exclusive [**SSH Tunneling Mode**](../advanced/secure_mode/) ==at the Server-end only==. + + * **`ssh_tunnel_pwd`** (_string_): In SSH Tunneling Mode, This internal attribute sets the password required to authorize Host for SSH Connection ==at the Server-end only==. More information can be found [here ➶](../advanced/ssh_tunnel/#supported-attributes) + + * **`ssh_tunnel_keyfile`** (_string_): In SSH Tunneling Mode, This internal attribute sets path to Host key that provide another way to authenticate host for SSH Connection ==at the Server-end only==. More information can be found [here ➶](../advanced/ssh_tunnel/#supported-attributes) + * **`custom_cert_location`** (_string_) : In Secure Mode, This internal attribute assigns user-defined location/path to directory for generating/storing Public+Secret Keypair necessary for encryption. More information can be found [here ➶](../advanced/secure_mode/#supported-attributes) * **`overwrite_cert`** (_boolean_) : In Secure Mode, This internal attribute decides whether to overwrite existing Public+Secret Keypair/Certificates or not, ==at the Server-end only==. More information can be found [here ➶](../advanced/secure_mode/#supported-attributes) - * **`jpeg_compression`**(_bool_): This attribute can be used to activate(if True)/deactivate(if False) Frame Compression. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression`**(_bool_): This internal attribute can be used to activate(if True)/deactivate(if False) Frame Compression. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) - * **`jpeg_compression_quality`**(_int/float_): It controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression_quality`**(_int/float_): This internal attribute controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. More information can be found [here ➶](../advanced/compression/#supported-attributes) - * **`jpeg_compression_fastdct`**(_bool_): This attribute if True, use fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression_fastdct`**(_bool_): This internal attributee if True, use fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) - * **`jpeg_compression_fastupsample`**(_bool_): This attribute if True, use fastest color upsampling method. Its default value is `False`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression_fastupsample`**(_bool_): This internal attribute if True, use fastest color upsampling method. Its default value is `False`. More information can be found [here ➶](../advanced/compression/#supported-attributes) * **`max_retries`**(_integer_): This internal attribute controls the maximum retries before Server/Client exit itself, if it's unable to get any response/reply from the socket before a certain amount of time, when synchronous messaging patterns like (`zmq.PAIR` & `zmq.REQ/zmq.REP`) are being used. It's value can anything greater than `0`, and its default value is `3`. - * **`request_timeout`**(_integer_): This internal attribute controls the timeout value _(in seconds)_, after which the Server/Client exit itself if it's unable to get any response/reply from the socket, when synchronous messaging patterns like (`zmq.PAIR` & `zmq.REQ/zmq.REP`) are being used. It's value can anything greater than `0`, and its default value is `10` seconds. - * **`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). * **`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. diff --git a/docs/gears/streamgear/ffmpeg_install.md b/docs/gears/streamgear/ffmpeg_install.md index 763489bce..5d1fd3c7d 100644 --- a/docs/gears/streamgear/ffmpeg_install.md +++ b/docs/gears/streamgear/ffmpeg_install.md @@ -85,7 +85,7 @@ If StreamGear API not receives any input from the user on [**`custom_ffmpeg`**]( * **Download:** You can also manually download the latest Windows Static Binaries(*based on your machine arch(x86/x64)*) from the link below: - *Windows Static Binaries:* http://ffmpeg.zeranoe.com/builds/ + *Windows Static Binaries:* https://ffmpeg.org/download.html#build-windows * **Assignment:** Then, you can easily assign the custom path to the folder containing FFmpeg executables(`for e.g 'C:/foo/Downloads/ffmpeg/bin'`) or path of `ffmpeg.exe` executable itself to the [**`custom_ffmpeg`**](../params/#custom_ffmpeg) parameter in the StreamGear API. diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index 011f50352..fe9cf1e77 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -167,7 +167,7 @@ stream_stab.stop() The usage example of VideoGear API with Variable PiCamera Properties is as follows: -!!! example "This example is basically a VideoGear API implementation of this [PiGear usage example](../../pigear/usage/#using-pigear-with-variable-camera-properties). Thereby, any [CamGear](../../camgear/usage/) or [PiGear](../../pigear/usage/) usage examples can be implemented with VideoGear API in the similar manner." +!!! info "This example is basically a VideoGear API implementation of this [PiGear usage example](../../pigear/usage/#using-pigear-with-variable-camera-properties). Thereby, any [CamGear](../../camgear/usage/) or [PiGear](../../pigear/usage/) usage examples can be implemented with VideoGear API in the similar manner." ```python diff --git a/docs/gears/writegear/compression/advanced/cciw.md b/docs/gears/writegear/compression/advanced/cciw.md index d23d616e6..272deb4d1 100644 --- a/docs/gears/writegear/compression/advanced/cciw.md +++ b/docs/gears/writegear/compression/advanced/cciw.md @@ -119,7 +119,7 @@ In this example, we will merge audio with video: !!! tip "You can also directly add external audio input to video-frames in WriteGear. For more information, See [this FAQ example ➶](../../../../../help/writegear_faqs/#how-add-external-audio-file-input-to-video-frames)" -!!! warning "Example Assumptions" +!!! alert "Example Assumptions" * You already have a separate video(i.e `'input-video.mp4'`) and audio(i.e `'input-audio.aac'`) files. diff --git a/docs/gears/writegear/compression/advanced/ffmpeg_install.md b/docs/gears/writegear/compression/advanced/ffmpeg_install.md index 94dc3b021..39a9a4cee 100644 --- a/docs/gears/writegear/compression/advanced/ffmpeg_install.md +++ b/docs/gears/writegear/compression/advanced/ffmpeg_install.md @@ -86,7 +86,7 @@ If WriteGear API not receives any input from the user on [**`custom_ffmpeg`**](. * **Download:** You can also manually download the latest Windows Static Binaries(*based on your machine arch(x86/x64)*) from the link below: - *Windows Static Binaries:* http://ffmpeg.zeranoe.com/builds/ + *Windows Static Binaries:* https://ffmpeg.org/download.html#build-windows * **Assignment:** Then, you can easily assign the custom path to the folder containing FFmpeg executables(`for e.g 'C:/foo/Downloads/ffmpeg/bin'`) or path of `ffmpeg.exe` executable itself to the [**`custom_ffmpeg`**](../../params/#custom_ffmpeg) parameter in the WriteGear API. diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 7527f9bea..01c63f953 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -223,7 +223,7 @@ _In this example, let's stream Live Camera Feed directly to Twitch!_ !!! warning "This example assume you already have a [**Twitch Account**](https://www.twitch.tv/) for publishing video." -!!! danger "Make sure to change [_Twitch Stream Key_](https://www.youtube.com/watch?v=xwOtOfPMIIk) with yours in following code before running!" +!!! alert "Make sure to change [_Twitch Stream Key_](https://www.youtube.com/watch?v=xwOtOfPMIIk) with yours in following code before running!" ```python # import required libraries @@ -429,7 +429,7 @@ writer.close() In Compression Mode, WriteGear API allows us to exploit almost all FFmpeg supported parameters that you can think of, in its Compression Mode. Hence, processing, encoding, and combining audio with video is pretty much straightforward. -!!! warning "Example Assumptions" +!!! alert "Example Assumptions" * You're running are Linux machine. * You already have appropriate audio & video drivers and softwares installed on your machine. diff --git a/docs/help/netgear_faqs.md b/docs/help/netgear_faqs.md index 4766cdb17..3367d69a0 100644 --- a/docs/help/netgear_faqs.md +++ b/docs/help/netgear_faqs.md @@ -39,12 +39,13 @@ limitations under the License. Here's the compatibility chart for NetGear's [Exclusive Modes](../../gears/netgear/overview/#exclusive-modes): -| Exclusive Modes | Multi-Servers | Multi-Clients | Secure | Bidirectional | -| :-------------: | :-----------: | :-----------: | :----: | :-----------: | -| **Multi-Servers** | - | No _(throws error)_ | Yes | No _(disables it)_ | -| **Multi-Clients** | No _(throws error)_ | - | Yes | No _(disables it)_ | -| **Secure** | Yes | Yes | - | Yes | -| **Bidirectional** | No _(disabled)_ | No _(disabled)_ | Yes | - | +| Exclusive Modes | Multi-Servers | Multi-Clients | Secure | Bidirectional | SSH Tunneling | +| :-------------: | :-----------: | :-----------: | :----: | :-----------: | :-----------: | +| **Multi-Servers** | - | No _(throws error)_ | Yes | No _(disables it)_ | No _(throws error)_ | +| **Multi-Clients** | No _(throws error)_ | - | Yes | No _(disables it)_ | No _(throws error)_ | +| **Secure** | Yes | Yes | - | Yes | Yes | +| **Bidirectional** | No _(disabled)_ | No _(disabled)_ | Yes | - | Yes | +| **SSH Tunneling** | No _(throws error)_ | No _(throws error)_ | Yes | Yes | - |   @@ -93,6 +94,13 @@ Here's the compatibility chart for NetGear's [Exclusive Modes](../../gears/netge   + +## How to access NetGear API outside network or remotely? + +**Answer:** See its [SSH Tunneling Mode doc ➶](../../gears/netgear/advanced/ssh_tunnel/). + +  + ## Are there any side-effect of sending data with frames? **Answer:** Yes, it may lead to additional **LATENCY** depending upon the size/amount of the data being transferred. User discretion is advised. diff --git a/docs/help/writegear_faqs.md b/docs/help/writegear_faqs.md index 48824a720..a2c3f7416 100644 --- a/docs/help/writegear_faqs.md +++ b/docs/help/writegear_faqs.md @@ -114,7 +114,7 @@ limitations under the License. !!! new "New in v0.2.1" This example was added in `v0.2.1`. -!!! warning "This example assume you already have a [**YouTube Account with Live-Streaming enabled**](https://support.google.com/youtube/answer/2474026#enable) for publishing video." +!!! alert "This example assume you already have a [**YouTube Account with Live-Streaming enabled**](https://support.google.com/youtube/answer/2474026#enable) for publishing video." !!! danger "Make sure to change [_YouTube-Live Stream Key_](https://support.google.com/youtube/answer/2907883#zippy=%2Cstart-live-streaming-now) with yours in following code before running!" diff --git a/docs/overrides/assets/images/ssh_tunnel.png b/docs/overrides/assets/images/ssh_tunnel.png new file mode 100644 index 0000000000000000000000000000000000000000..26945c041f19565e8fa4e3931a77b08d1a34bad0 GIT binary patch literal 22223 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 literal 0 HcmV?d00001 diff --git a/docs/overrides/assets/images/ssh_tunnel_ex.png b/docs/overrides/assets/images/ssh_tunnel_ex.png new file mode 100644 index 0000000000000000000000000000000000000000..423fc7c64c6debce6ce1f5dce22dd48bf3e1c40f GIT binary patch literal 43510 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 literal 0 HcmV?d00001 diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 65e546bdc..9c50ce873 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -277,6 +277,4 @@ main #g6219 { 100% { transform: rotate(0); } -} - - +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index cfd665553..09500408d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -222,6 +222,7 @@ nav: - Multi-Servers Mode: gears/netgear/advanced/multi_server.md - Multi-Clients Mode: gears/netgear/advanced/multi_client.md - Bidirectional Mode: gears/netgear/advanced/bidirectional_mode.md + - SSH Tunneling Mode: gears/netgear/advanced/ssh_tunnel.md - Secure Mode: gears/netgear/advanced/secure_mode.md - Frame Compression: gears/netgear/advanced/compression.md - Parameters: gears/netgear/params.md diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 8527c065d..755475295 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -434,8 +434,8 @@ def __init__( if not (self.__ssh_tunnel_mode is None): # SSH Tunnel Mode only available for server mode if receive_mode: - raise ValueError( - "[NetGear:ERROR] :: SSH Tunneling cannot be enabled for Client-end!" + logger.error( + "SSH Tunneling cannot be enabled for Client-end!" ) else: # check if SSH tunneling even possible @@ -499,8 +499,9 @@ def __init__( # log Bi-directional mode activation if self.__logging: logger.debug( - "SSH Tunneling is enabled for host:`{}`!".format( - self.__ssh_tunnel_mode + "SSH Tunneling is enabled for host:`{}` with `{}` back-end.".format( + self.__ssh_tunnel_mode, + "paramiko" if self.__paramiko_present else "pexpect", ) ) @@ -878,7 +879,9 @@ def __init__( ) if self.__ssh_tunnel_mode: logger.critical( - "Failed to initiate SSH Tunneling Mode for this server!" + "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( From 275743cf210881066ab482dcd20d06c2d813355b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 20 May 2021 09:52:19 +0530 Subject: [PATCH 018/112] =?UTF-8?q?=F0=9F=92=84=20Docs:=20New=20admonition?= =?UTF-8?q?s=20and=20beautified=20css?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/overrides/assets/stylesheets/custom.css | 303 ++++++++++++------- 1 file changed, 193 insertions(+), 110 deletions(-) diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 9c50ce873..f428681f6 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -19,169 +19,269 @@ limitations under the License. */ :root { - --md-admonition-icon--new: url('data:image/svg+xml;charset=utf-8,') + --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,%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='M6,6.9L3.87,4.78L5.28,3.37L7.4,5.5L6,6.9M13,1V4H11V1H13M20.13,4.78L18,6.9L16.6,5.5L18.72,3.37L20.13,4.78M4.5,10.5V12.5H1.5V10.5H4.5M19.5,10.5H22.5V12.5H19.5V10.5M6,20H18A2,2 0 0,1 20,22H4A2,2 0 0,1 6,20M12,5A6,6 0 0,1 18,11V19H6V11A6,6 0 0,1 12,5Z' /%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 fill='%23000000' d='M13 13H11V7H13M11 15H13V17H11M15.73 3H8.27L3 8.27V15.73L8.27 21H15.73L21 15.73V8.27L15.73 3Z' /%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-typeset .admonition.new, .md-typeset details.new { - border-color: rgb(43, 155, 70); + border-color: rgb(43, 155, 70); } .md-typeset .new > .admonition-title, .md-typeset .new > summary { - background-color: rgba(43, 155, 70, 0.1); - border-color: rgb(43, 155, 70); + background-color: rgba(43, 155, 70, 0.1); + border-color: rgb(43, 155, 70); } .md-typeset .new > .admonition-title::before, .md-typeset .new > summary::before { - background-color: rgb(43, 155, 70); - -webkit-mask-image: var(--md-admonition-icon--new); - mask-image: var(--md-admonition-icon--new); + background-color: rgb(43, 155, 70); + -webkit-mask-image: var(--md-admonition-icon--new); + mask-image: var(--md-admonition-icon--new); +} +.md-typeset .admonition.alert, +.md-typeset details.alert { + border-color: rgb(255, 0, 255); +} +.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 { + background-color: rgb(255, 0, 255)); + -webkit-mask-image: var(--md-admonition-icon--alert); + mask-image: var(--md-admonition-icon--alert); +} +.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); +} +.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; +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); +} +.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); } + code { -word-break: keep-all !important; + word-break: keep-all !important; } - td { -vertical-align: middle !important; + vertical-align: middle !important; } - th { font-weight: bold !important; text-align: center !important; } - .md-nav__item--active > .md-nav__link { - font-weight: bold; + font-weight: bold; } - .center { - display: block; - margin-left: auto; - margin-right: auto; - width: 80%; + display: block; + margin-left: auto; + margin-right: auto; + width: 80%; } - .center-small { - display: block; - margin-left: auto; - margin-right: auto; - width: 90%; + display: block; + margin-left: auto; + margin-right: auto; + width: 90%; } - .md-tabs__link--active { - font-weight: bold; + font-weight: bold; } - .md-nav__title { - font-size: 1rem !important; + font-size: 1rem !important; } - .md-version__link { - overflow: hidden; + overflow: hidden; } - -.md-version__current{ - text-transform: uppercase; - font-weight: bolder; +.md-version__current { + text-transform: uppercase; + font-weight: bolder; } - .md-typeset .task-list-control .task-list-indicator::before { - background-color: #FF0000; + background-color: #FF0000; } - blockquote { - padding: 0.5em 10px; - quotes: "\201C""\201D""\2018""\2019"; + padding: 0.5em 10px; + quotes: "\201C""\201D""\2018""\2019"; } blockquote:before { - color: #ccc; - content: open-quote; - font-size: 4em; - line-height: 0.1em; - margin-right: 0.25em; - vertical-align: -0.4em; + color: #ccc; + content: open-quote; + font-size: 4em; + line-height: 0.1em; + margin-right: 0.25em; + vertical-align: -0.4em; } blockquote:after { - visibility: hidden; - content: close-quote; + visibility: hidden; + content: close-quote; } blockquote p { - display: inline; + display: inline; } - /* Handles Responive Video tags (from bootstrap) */ + .video { - padding: 0; - margin: 0; - list-style: none; - display: flex; - justify-content: center; + padding: 0; + margin: 0; + list-style: none; + display: flex; + justify-content: center; } .embed-responsive { - position: relative; - display: block; - width: 100%; - padding: 0; - overflow: hidden; + position: relative; + display: block; + width: 100%; + padding: 0; + overflow: hidden; } - .embed-responsive::before { - display: block; - content: ""; + display: block; + content: ""; } - .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object, .embed-responsive video { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; - border: 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; } - .embed-responsive-21by9::before { - padding-top: 42.857143%; + padding-top: 42.857143%; } - .embed-responsive-16by9::before { - padding-top: 56.25%; + padding-top: 56.25%; } - .embed-responsive-4by3::before { - padding-top: 75%; + padding-top: 75%; } - .embed-responsive-1by1::before { - padding-top: 100%; + padding-top: 100%; } - /* ends */ - footer.sponsorship { - text-align: center; +footer.sponsorship { + 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: 2rem; + margin: 0.875rem; + 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; } .doc-heading { - padding-top: 50px; + padding-top: 50px; } - .btn { z-index: 1; overflow: hidden; @@ -196,12 +296,10 @@ blockquote p { font-weight: bold; margin: 5px 0px; } - .btn.bcolor { border: 4px solid var(--md-typeset-a-color); color: var(--blue); } - .btn.bcolor:before { content: ""; position: absolute; @@ -213,68 +311,53 @@ blockquote p { z-index: -1; transition: 0.2s ease; } - .btn.bcolor:hover { color: var(--white); background: var(--md-typeset-a-color); transition: 0.2s ease; } - .btn.bcolor:hover:before { width: 100%; } - main #g6219 { transform-origin: 85px 4px; animation: an1 12s .5s infinite ease-out; } - @keyframes an1 { 0% { transform: rotate(0); } - 5% { transform: rotate(3deg); } - 15% { transform: rotate(-2.5deg); } - 25% { transform: rotate(2deg); } - 35% { transform: rotate(-1.5deg); } - 45% { transform: rotate(1deg); } - 55% { transform: rotate(-1.5deg); } - 65% { transform: rotate(2deg); } - 75% { transform: rotate(-2deg); } - 85% { transform: rotate(2.5deg); } - 95% { transform: rotate(-3deg); } - 100% { transform: rotate(0); } -} \ No newline at end of file +} From a3f84412c1b9f6fc94c5c75ef234795e884ac075 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 23 May 2021 08:10:10 +0530 Subject: [PATCH 019/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Added=20NetGear=20?= =?UTF-8?q?CI=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added new CI tests for SSH Tunneling Mode. - Added "paramiko" to CI dependencies. - Reserved port-47 for testing. --- .github/workflows/ci_linux.yml | 2 +- appveyor.yml | 2 +- azure-pipelines.yml | 2 +- vidgear/gears/netgear.py | 37 +++++++++-------- vidgear/tests/network_tests/test_netgear.py | 45 +++++++++++++++++---- 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index b95c72a76..a82b8b636 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -51,7 +51,7 @@ jobs: pip install -U pip wheel numpy pip install -U .[asyncio] pip uninstall opencv-python -y - pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash + pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash paramiko if: success() - name: run prepare_dataset_script run: bash scripts/bash/prepare_dataset.sh diff --git a/appveyor.yml b/appveyor.yml index 48f535951..4bf60c172 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "python -m pip install --upgrade pip wheel" - - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc" + - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko" - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev/mpegdash-0.3.0.dev0-py3-none-any.whl" - cmd: chmod +x scripts/bash/prepare_dataset.sh - cmd: bash scripts/bash/prepare_dataset.sh diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0e564d73d..f3ba7a24f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,7 +55,7 @@ steps: - script: | python -m pip install --upgrade pip wheel - pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash + pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash paramiko pip install --upgrade pytest pytest-asyncio pytest-cov pytest-azurepipelines displayName: 'Install pip dependencies' diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 755475295..ecb6a3e4d 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -317,7 +317,7 @@ def __init__( elif key == "ssh_tunnel_mode" and isinstance(value, str): # enable SSH Tunneling Mode - self.__ssh_tunnel_mode = value + self.__ssh_tunnel_mode = value.strip() elif key == "ssh_tunnel_pwd" and isinstance(value, str): # add valid SSH Tunneling password self.__ssh_tunnel_pwd = value @@ -434,27 +434,32 @@ def __init__( if not (self.__ssh_tunnel_mode is None): # SSH Tunnel Mode only available for server mode if receive_mode: - logger.error( - "SSH Tunneling cannot be enabled for Client-end!" - ) + logger.error("SSH Tunneling cannot be enabled for Client-end!") else: - # check if SSH tunneling even possible - ssh_address = self.__ssh_tunnel_mode.strip() + # check if SSH tunneling possible + ssh_address = self.__ssh_tunnel_mode ssh_address, ssh_port = ( ssh_address.split(":") if ":" in ssh_address else [ssh_address, "22"] ) # default to port 22 - ssh_user, ssh_ip = ( - ssh_address.split("@") - if "@" in ssh_address - else ["admin", ssh_address] - ) # default to username "admin" - assert check_open_port( - ssh_ip, port=int(ssh_port) - ), "[NetGear:ERROR] :: Host `{}` is not available for SSH Tunneling at port-{}!".format( - ssh_address, ssh_port - ) + if "47" in ssh_port: + self.__ssh_tunnel_mode = self.__ssh_tunnel_mode.replace( + ":47", "" + ) # port-47 is reserved for testing + else: + # extract ip for validation + ssh_user, ssh_ip = ( + ssh_address.split("@") + if "@" in ssh_address + else ["", ssh_address] + ) + # validate ip specified port + assert check_open_port( + ssh_ip, port=int(ssh_port) + ), "[NetGear:ERROR] :: Host `{}` is not available for SSH Tunneling at port-{}!".format( + ssh_address, ssh_port + ) # import packages import zmq.ssh diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index a72b07d4f..20cf6d16f 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -398,6 +398,13 @@ def test_bidirectional_mode(pattern, target_data, options): "bidirectional_mode": True, }, ), + ( + 2, + { + "multiserver_mode": True, + "ssh_tunnel_mode": "new@sdf.org", + }, + ), ], ) def test_multiserver_mode(pattern, options): @@ -556,10 +563,17 @@ def test_multiclient_mode(pattern): @pytest.mark.parametrize( "options", [ - {"max_retries": -1, "request_timeout": 3}, - {"max_retries": 2, "request_timeout": 4, "bidirectional_mode": True}, - {"max_retries": 2, "request_timeout": 4, "multiclient_mode": True}, - {"max_retries": 2, "request_timeout": 4, "multiserver_mode": True}, + {"max_retries": -1, "request_timeout": 2}, + { + "max_retries": 2, + "request_timeout": 2, + "bidirectional_mode": True, + "ssh_tunnel_mode": " new@sdf.org ", + "ssh_tunnel_pwd": "xyz", + "ssh_tunnel_keyfile": "ok.txt", + }, + {"max_retries": 2, "request_timeout": 2, "multiclient_mode": True}, + {"max_retries": 2, "request_timeout": 2, "multiserver_mode": True}, ], ) def test_client_reliablity(options): @@ -596,9 +610,25 @@ def test_client_reliablity(options): @pytest.mark.parametrize( "options", [ - {"max_retries": 2, "request_timeout": 4, "bidirectional_mode": True}, - {"max_retries": 2, "request_timeout": 4, "multiserver_mode": True}, - {"max_retries": 2, "request_timeout": 4, "multiclient_mode": True}, + {"max_retries": 2, "request_timeout": 2, "bidirectional_mode": True}, + {"max_retries": 2, "request_timeout": 2, "multiserver_mode": True}, + {"max_retries": 2, "request_timeout": 2, "multiclient_mode": True}, + { + "max_retries": 2, + "request_timeout": 2, + "ssh_tunnel_mode": "localhost", + }, + { + "max_retries": 2, + "request_timeout": 2, + "bidirectional_mode": True, + "ssh_tunnel_mode": "localhost:47", + }, + { + "max_retries": 2, + "request_timeout": 2, + "ssh_tunnel_mode": "localhost:47", + }, ], ) def test_server_reliablity(options): @@ -611,6 +641,7 @@ def test_server_reliablity(options): try: # define params server = NetGear( + address="127.0.0.1" if "ssh_tunnel_mode" in options else None, pattern=1, port=[5585] if "multiclient_mode" in options.keys() else 6654, logging=True, From 3cae806fc1417b262ac652389f7341a8c41f4d29 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 23 May 2021 09:15:18 +0530 Subject: [PATCH 020/112] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20=20Maintence:?= =?UTF-8?q?=20Updated=20issue=20templates=20and=20labels.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 3 ++- .github/ISSUE_TEMPLATE/proposal.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- .github/needs-more-info.yml | 2 +- .github/no-response.yml | 2 +- docs/gears/writegear/compression/usage.md | 12 ++++++------ 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7b1b90e7a..e3f8d3a50 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,8 @@ --- name: Bug report about: Create a bug-report for VidGear -labels: "issue: bug" +labels: ':beetle: BUG' +assignees: 'abhiTronix' --- diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index a7de08f00..e596c033f 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,7 +1,7 @@ --- name: Question about: Have any questions regarding VidGear? -labels: "issue: question" +labels: 'QUESTION :question:' --- diff --git a/.github/needs-more-info.yml b/.github/needs-more-info.yml index 1ca6baac7..381f9b3c8 100644 --- a/.github/needs-more-info.yml +++ b/.github/needs-more-info.yml @@ -1,6 +1,6 @@ checkTemplate: true miniTitleLength: 8 -labelToAdd: "MISSING : INFORMATION :mag:" +labelToAdd: 'MISSING : TEMPLATE :grey_question:' issue: reactions: - eyes diff --git a/.github/no-response.yml b/.github/no-response.yml index 95e31d64e..deffa6b17 100644 --- a/.github/no-response.yml +++ b/.github/no-response.yml @@ -3,7 +3,7 @@ # Number of days of inactivity before an Issue is closed for lack of response daysUntilClose: 1 # Label requiring a response -responseRequiredLabel: "MISSING : INFORMATION :mag:" +responseRequiredLabel: 'MISSING : INFORMATION :mag:' # Comment to post when closing an Issue for lack of response. Set to `false` to disable closeComment: > ### No Response :-1: diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 01c63f953..eb1a6408e 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -215,9 +215,9 @@ writer.close() ## Using Compression Mode for Streaming URLs -In Compression Mode, WriteGear can make complex job look easy with FFmpeg. It also allows any URLs _(as output)_ for network streaming with its [`output_filename`](../params/#output_filename) parameter. +In Compression Mode, WriteGear also allows URL strings _(as output)_ for network streaming with its [`output_filename`](../params/#output_filename) parameter. -_In this example, let's stream Live Camera Feed directly to Twitch!_ +In this example, we will stream live camera feed directly to Twitch: !!! info "YouTube-Live Streaming example code also available in [WriteGear FAQs ➶](../../../../help/writegear_faqs/#is-youtube-live-streaming-possibe-with-writegear)" @@ -292,16 +292,16 @@ writer.close() ## Using Compression Mode with Hardware encoders -By default, WriteGear API uses *libx264 encoder* for encoding its output files in Compression Mode. But you can easily change encoder to your suitable [supported encoder](../params/#supported-encoders) by passing `-vcodec` FFmpeg parameter as an attribute in its [*output_param*](../params/#output_params) dictionary parameter. In addition to this, you can also specify the additional properties/features of your system's GPU easily. +By default, WriteGear API uses `libx264` encoder for encoding output files in Compression Mode. But you can easily change encoder to your suitable [supported encoder](../params/#supported-encoders) by passing `-vcodec` FFmpeg parameter as an attribute with its [*output_param*](../params/#output_params) dictionary parameter. In addition to this, you can also specify the additional properties/features of your system's GPU easily. ??? warning "User Discretion Advised" - 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. + 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 system hardware settings only. -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: +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'`: -!!! danger "Check VAAPI support" +??? alert "Remember to 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: From 4f7bd36a74aca12ae9054f1503a5d250e78a15f2 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 23 May 2021 09:45:55 +0530 Subject: [PATCH 021/112] =?UTF-8?q?=E2=9C=85=20CI:=20Updated=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/network_tests/test_netgear.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 20cf6d16f..7294036b6 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -614,20 +614,21 @@ def test_client_reliablity(options): {"max_retries": 2, "request_timeout": 2, "multiserver_mode": True}, {"max_retries": 2, "request_timeout": 2, "multiclient_mode": True}, { - "max_retries": 2, - "request_timeout": 2, "ssh_tunnel_mode": "localhost", }, + { + "ssh_tunnel_mode": "localhost:47", + }, { "max_retries": 2, "request_timeout": 2, "bidirectional_mode": True, - "ssh_tunnel_mode": "localhost:47", + "ssh_tunnel_mode": "git@github.com", }, { "max_retries": 2, "request_timeout": 2, - "ssh_tunnel_mode": "localhost:47", + "ssh_tunnel_mode": "git@github.com", }, ], ) From 432ee3d249e6b81d923f01f9886106bd7e919a5b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 23 May 2021 10:31:58 +0530 Subject: [PATCH 022/112] =?UTF-8?q?=F0=9F=8E=A8=20Helper:=20New=20Updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented new `delete_file_safe` to safely delete files at given path. - Replaced `os.remove` calls with `delete_file_safe`. - Renamed `delete_safe` to `delete_ext_safe`. --- vidgear/gears/helper.py | 39 +++++++++++++++++++++++++++--------- vidgear/gears/streamgear.py | 6 +++--- vidgear/tests/test_helper.py | 10 ++++----- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 043352c89..4fbc4da99 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -34,6 +34,7 @@ import socket from tqdm import tqdm from contextlib import closing +from pathlib import Path from colorlog import ColoredFormatter from distutils.version import LooseVersion from requests.adapters import HTTPAdapter @@ -205,7 +206,7 @@ def check_WriteAccess(path, is_windows=False): write_accessible = False finally: if os.path.exists(temp_fname): - os.remove(temp_fname) + delete_file_safe(temp_fname) return write_accessible @@ -540,6 +541,26 @@ def get_video_bitrate(width, height, fps, bpp): return round((width * height * bpp * fps) / 1000) +def delete_file_safe(file_path): + """ + ### delete_ext_safe + + Safely deletes files at given path. + + Parameters: + file_path (string): path to the file + """ + try: + dfile = Path(file_path) + if sys.version_info >= (3, 8, 0): + dfile.unlink(missing_ok=True) + else: + if dfile.exists(): + dfile.unlink() + except Exception as e: + logger.exception(e) + + def mkdir_safe(dir_path, logging=False): """ ### mkdir_safe @@ -562,9 +583,9 @@ def mkdir_safe(dir_path, logging=False): logger.debug("Directory already exists at `{}`".format(dir_path)) -def delete_safe(dir_path, extensions=[], logging=False): +def delete_ext_safe(dir_path, extensions=[], logging=False): """ - ### delete_safe + ### delete_ext_safe Safely deletes files with given extensions at given path. @@ -586,7 +607,7 @@ def delete_safe(dir_path, extensions=[], logging=False): os.path.join(dir_path, f) for f in os.listdir(dir_path) if f.endswith(ext) ] for file in files_ext: - os.remove(file) + delete_file_safe(file) if logging: logger.debug("Deleted file: `{}`".format(file)) @@ -836,7 +857,7 @@ def download_ffmpeg_binaries(path, os_windows=False, os_bit=""): ) # remove leftovers if exists if os.path.isfile(file_name): - os.remove(file_name) + delete_file_safe(file_name) # download and write file to the given path with open(file_name, "wb") as f: logger.debug( @@ -870,7 +891,7 @@ def download_ffmpeg_binaries(path, os_windows=False, os_bit=""): zip_fname, _ = os.path.split(zip_ref.infolist()[0].filename) zip_ref.extractall(base_path) # perform cleaning - os.remove(file_name) + delete_file_safe(file_name) logger.debug("FFmpeg binaries for Windows configured successfully!") final_path += file_path # return final path @@ -1004,7 +1025,7 @@ def generate_auth_certificates(path, overwrite=False, logging=False): # clean redundant keys if present redundant_key = os.path.join(keys_dir, key_file) if os.path.isfile(redundant_key): - os.remove(redundant_key) + delete_file_safe(redundant_key) else: # otherwise validate available keys status_public_keys = validate_auth_keys(public_keys_dir, ".key") @@ -1044,7 +1065,7 @@ def generate_auth_certificates(path, overwrite=False, logging=False): # clean redundant keys if present redundant_key = os.path.join(keys_dir, key_file) if os.path.isfile(redundant_key): - os.remove(redundant_key) + delete_file_safe(redundant_key) # validate newly generated keys status_public_keys = validate_auth_keys(public_keys_dir, ".key") @@ -1093,7 +1114,7 @@ def validate_auth_keys(path, extension): # remove invalid keys if found if len(keys_buffer) == 1: - os.remove(os.path.join(path, keys_buffer[0])) + delete_file_safe(os.path.join(path, keys_buffer[0])) # return results return True if (len(keys_buffer) == 2) else False diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index d01fb35d9..e3a8b1e9b 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -33,7 +33,7 @@ from .helper import ( capPropId, dict2Args, - delete_safe, + delete_ext_safe, extract_time, is_valid_url, logger_handler, @@ -247,7 +247,7 @@ def __init__( valid_extension = "mpd" if self.__format == "dash" else "m3u8" if os.path.isdir(abs_path): if self.__clear_assets: - delete_safe(abs_path, [".m4s", ".mpd"], logging=self.__logging) + delete_ext_safe(abs_path, [".m4s", ".mpd"], logging=self.__logging) abs_path = os.path.join( abs_path, "{}-{}.{}".format( @@ -257,7 +257,7 @@ def __init__( ), ) # auto-assign valid name and adds it to path elif self.__clear_assets and os.path.isfile(abs_path): - delete_safe( + delete_ext_safe( os.path.dirname(abs_path), [".m4s", ".mpd"], logging=self.__logging, diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 6ec1817ee..4393ed04b 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -35,7 +35,7 @@ reducer, dict2Args, mkdir_safe, - delete_safe, + delete_ext_safe, check_output, extract_time, create_blank_frame, @@ -507,9 +507,9 @@ def test_check_gstreamer_support(): ([], False), ], ) -def test_delete_safe(ext, result): +def test_delete_ext_safe(ext, result): """ - Testing delete_safe function + Testing delete_ext_safe function """ try: path = os.path.join(expanduser("~"), "test_mpd") @@ -527,8 +527,8 @@ def test_delete_safe(ext, result): streamer.transcode_source() streamer.terminate() assert check_valid_mpd(mpd_file_path) - delete_safe(path, ext, logging=True) - assert not os.listdir(path), "`delete_safe` Test failed!" + delete_ext_safe(path, ext, logging=True) + assert not os.listdir(path), "`delete_ext_safe` Test failed!" # cleanup if os.path.isdir(path): shutil.rmtree(path) From 993371fff54cde9b5063b31e141058e1eb78888b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 24 May 2021 23:04:24 +0530 Subject: [PATCH 023/112] =?UTF-8?q?=F0=9F=92=84=20Docs:=20Added=20new=20as?= =?UTF-8?q?sets=20and=20updated=20changelog.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/bonus/reference/helper.md | 2 +- docs/changelog.md | 91 ++++++++++++++++++ docs/gears/netgear/advanced/multi_client.md | 8 +- docs/gears/netgear/advanced/multi_server.md | 5 +- docs/overrides/assets/images/multi_client.png | Bin 200515 -> 110254 bytes docs/overrides/assets/images/multi_server.png | Bin 269024 -> 150220 bytes docs/overrides/assets/javascripts/extra.js | 2 +- 7 files changed, 96 insertions(+), 12 deletions(-) mode change 100755 => 100644 docs/overrides/assets/images/multi_client.png mode change 100755 => 100644 docs/overrides/assets/images/multi_server.png diff --git a/docs/bonus/reference/helper.md b/docs/bonus/reference/helper.md index ccc7c4daf..1a7e8e214 100644 --- a/docs/bonus/reference/helper.md +++ b/docs/bonus/reference/helper.md @@ -42,7 +42,7 @@ limitations under the License.   -::: vidgear.gears.helper.delete_safe +::: vidgear.gears.helper.delete_ext_safe   diff --git a/docs/changelog.md b/docs/changelog.md index e0bbe94e0..16f28a802 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -20,6 +20,97 @@ limitations under the License. # Release Notes +## v0.2.2 (In Progress) + +??? tip "New Features" + - [x] **NetGear:** + * [ ] New SSH Tunneling Mode for connecting ZMQ sockets across machines via SSH tunneling. + * [ ] Added new `ssh_tunnel_mode` attribute to enable ssh tunneling at provide address at server end only. + * [ ] Implemented new `check_open_port` helper method to validate availability of host at given open port. + * [ ] Added new attributes `ssh_tunnel_keyfile` and `ssh_tunnel_pwd` to easily validate ssh connection. + * [ ] Extended this feature to be compatible with bi-directional mode and auto-reconnection. + * [ ] Initially disabled support for exclusive Multi-Server and Multi-Clients modes. + * [ ] Implemented logic to automatically enable `paramiko` support if installed. + * [ ] Reserved port-47 for testing. + - [x] **WebGear_RTC:** + * [ ] Added native support for middlewares. + * [ ] Added new global `middleware` variable for easily defining Middlewares as list. + * [ ] Added validity check for Middlewares. + * [ ] Added tests for middlewares support. + * [ ] Added example for middlewares support. + * [ ] Added related imports. + - [x] **CI:** + * [ ] Added new `no-response` work-flow for stale issues. + * [ ] Added NetGear CI Tests + * [ ] Added new CI tests for SSH Tunneling Mode. + * [ ] Added "paramiko" to CI dependencies. + + - [x] **Docs:** + * [ ] Added Zenodo DOI badge and its reference in BibTex citations. + * [ ] Added `pymdownx.striphtml` plugin for stripping comments. + * [ ] Added complete docs for SSH Tunneling Mode. + * [ ] Added complete docs for NetGear's SSH Tunneling Mode. + * [ ] Added new usage example and related information. + * [ ] Added new image assets for ssh tunneling example. + * [ ] New admonitions and beautified css + + +??? success "Updates/Improvements" + - [x] Added exception for RunTimeErrors in NetGear CI tests. + - [x] Extended Middlewares support to WebGear API too. + - [x] Docs: + * [ ] Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. + * [ ] Re-positioned few docs comments at bottom for easier detection during stripping. + * [ ] Updated dark theme to `dark orange`. + * [ ] Updated fonts to `Source Sans Pro`. + * [ ] Fixed missing heading in VideoGear. + * [ ] Update setup.py update link for assets. + * [ ] Added missing StreamGear Code docs. + * [ ] Several minor tweaks and typos fixed. + * [ ] Updated 404 page and workflow. + * [ ] Updated README.md and mkdocs.yml with new additions. + * [ ] Re-written Threaded-Queue-Mode from scratch with elaborated functioning. + * [ ] Replace Paypal with Liberpay in FUNDING.yml + * [ ] Updated FFmpeg Download links. + * [ ] Restructured docs. + * [ ] Updated mkdocs.yml. + - [x] Helper: + * [ ] Implemented new `delete_file_safe` to safely delete files at given path. + * [ ] Renamed `delete_safe` to `delete_ext_safe`. + - [x] CI: + * [ ] Updated VidGear Docs Deployer Workflow + * [ ] Updated test + - [x] Updated issue templates and labels. + +??? danger "Breaking Updates/Changes" + - [ ] Replaced `os.remove` calls with `delete_file_safe`. + + +??? bug "Bug-fixes" + - [x] Critical Bugfix related to OpenCV Binaries import. + * [ ] Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. + * [ ] Replaced `packaging.parse_version` with more robust `distutils.version`. + * [ ] Removed redundant imports. + - [x] Setup: + * [ ] Removed `latest_version` variable support from `simplejpeg`. + * [ ] Fixed minor typos in dependencies. + - [x] Setup_cfg: Replaced dashes with underscores to remove warnings. + - [x] Docs: + * [ ] Fixed 404 page does not work outside the site root with mkdocs. + * [ ] Fixed markdown files comments not stripped when converted to HTML. + * [ ] Fixed typos + + +??? question "Pull Requests" + * PR #210 + * PR #215 + + +  + +  + + ## v0.2.1 (2021-04-25) ??? tip "New Features" diff --git a/docs/gears/netgear/advanced/multi_client.md b/docs/gears/netgear/advanced/multi_client.md index 4d88453c6..80d3c36bf 100644 --- a/docs/gears/netgear/advanced/multi_client.md +++ b/docs/gears/netgear/advanced/multi_client.md @@ -20,14 +20,12 @@ limitations under the License. # Multi-Clients Mode for NetGear API - -## Overview -
NetGear's Multi-Clients Mode
NetGear's Multi-Clients Mode
+## Overview In Multi-Clients Mode, NetGear robustly handles Multiple Clients at once thereby able to broadcast frames and data across multiple Clients/Consumers in the network at same time. This mode works almost contrary to [Multi-Servers Mode](../multi_server/) but here data transfer works unidirectionally with pattern `1` _(i.e. Request/Reply `zmq.REQ/zmq.REP`)_ only. Every new Client that connects to single Server can be identified by its unique port address on the network. @@ -67,15 +65,13 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a - [x] If the server gets disconnected, all the clients will automatically exit to save resources. -    - ## Usage Examples -!!! info "Important Information" +!!! alert "Important Information" * ==Frame/Data transmission will **NOT START** until all given Client(s) are connected to the Server.== diff --git a/docs/gears/netgear/advanced/multi_server.md b/docs/gears/netgear/advanced/multi_server.md index c4ced7b25..4f6b3db22 100644 --- a/docs/gears/netgear/advanced/multi_server.md +++ b/docs/gears/netgear/advanced/multi_server.md @@ -65,13 +65,10 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a   -  - - ## Usage Examples -!!! info "Important Information" +!!! alert "Important Information" * For sake of simplicity, in these examples we will use only two unique Servers, but, the number of these Servers can be extended to several numbers depending upon your system hardware limits. diff --git a/docs/overrides/assets/images/multi_client.png b/docs/overrides/assets/images/multi_client.png old mode 100755 new mode 100644 index 7ddd323d2af4ef7e6097bd79519888e5915d3eda..9cfe8eea7d365922ac6d21777f79c583f7b70398 GIT binary patch literal 110254 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 B%$fiI literal 200515 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

X|T@~fTUqDDK1A++%!IXU}t);}uE4%8rXQA_C` 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

W7kDdp7?))3s1hn{c!fS*aS$rS-AZ8})P!xTkw% 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 HC0+bK1o?l! diff --git a/docs/overrides/assets/images/multi_server.png b/docs/overrides/assets/images/multi_server.png old mode 100755 new mode 100644 index 243d9c0bf9aa2c386cb08bb7b66ad616c8f036e6..fba54eecdd604718d48ec3ef0d30522a02e92d5c GIT binary patch literal 150220 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�BvZZyy#UN9q@Ic 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 literal 269024 zcmdqJWmuGL*e;3)3MwV_(jgKeUDBaQNXH;8NW&1)9U>@*NOveD3>`zah)C&B!ccVaE5{-?zVY9Bb{pf2?Es!vlTdzV9o}>%8LOm4=!E!8Ph@SXfvDN{SD(u&}Pk zVqskk!@C0hrworY6bp+HOX-2Ej+e>W)KxEX-RbVFnaNn?M@tA2VuvT~Cgo%9V!o;2~xFopbnm!5ij51&Pn?0u&Rc?g6ed`lfFEHax zZ<^4TF1qM?WyUPPcVOn`Rct)QAT02ghCz1kN41EFt3B7&U{9WyDrJyGizGpXWyA=S;ljfA#?I3&^tS`rq=JIRaPVZCgmYeBy|^#<5003+kN$Z7Af)?^Efz*8SlHrt zca06Q(|4V0w!Trm|93-PN?_?nRXvX}ORHzk7ld;VA1OBbT5I8CDBxfg|3c2&g|P0Q zDDv8Qz$NK;O&KG(pddZe^89hH(c27;z-ds+`~YvHElK3!llyC_cPJh=LJaDk#Zu6F z-%$CVJqxz3HgP|=ZER&o{4yg($uikm!6{XL$f6mt^ArY4cZ}4~b^~4=D-%S_g3tPj z8?Aqhr@@4zn|iQB5!n53=P2Z?++RD1ZmYDEH>lcU;u2p_JHL?{gMJ_=CBZ`>ZAI1^T*)XNoj>0V)V1A_--lR zl{}ujIoeSw)4y!=0;BU5vSd*KQ%F-11zpw0q0})T#_SJS7m#S_KNIynef2@ z&3V?rLWTYOI*f9$uyrYt#hv1W7ftdlyIV|t+213Jgq`Se$R4Y^Jm*iFW^Tp4X7O)i%*Q|HEQ%vZ@g)@F zm%qA)cSo`d@n_eiS)fF5Q>7$r4~A44N+R9EFEsX$vHBS~_HnmegkUUdWv`WcaHiH? zqhNypV)rp7gWf7AMo)*DfHTerOWh}%IQ#B})-1`hsR@QP)8(`MZ4nDq;yL>dHl@McR`)#Gp z@t?TOP?Ck``&CKSOW&LCM<5Za+RW5{9M0fDx$lqHr$j&a4k!%;Pdzti9Upz^d0l8D z@-uv>OiWZ)35adlfSNN4a;(Pp?j}(Bd0E%vQ!G%FHT#SCD^?lmA*|uC+Axa1GCb zIMe4Pn1I_)dZw;9T_V{dhMS`~&xb}@yk$fU9i~KEWCs0omUg;M*4nQlGT^=RBlQXi zS0Ft~3(s4Z7e&MK*NB6XALa1buKj#j>Rs_?wt)L+K1}$?rRfD>&0Xft%e%S;#cuyCwbrPygS34bFp3MljD+PEje=^smlZv!$Jyn#M6u7m+(vEE@?T|(^0|Z$nls4y19THDK*S8Wo z{^)6{un6mRRkIlG?nL}cM+QAfDjTEnW z%d*qqn{J$~Nc+(3?t-NclJ^Ic(_%-syk8)y>op}$H($1#h#Hs;U4EnZt&=jZ5DM7} z#MX0sx%!C@*XNgX7pJ$fG^f>Ww_fW$iPXonzK&?~^&V40q>4PQyO`)^ySl_nht8k~MZ@QYF&(7x{$G&Z*qguCfcS z?vehXw|C$8%1U~tmfH`_u6?6szS5JI=~!Xw@e}edw#6TdxUtd{f)8rQ}4;(7!P}ot$D}EbGYg zougObi4RHCP|3ATl2;Q03j4SA@CsF{f+j5V`8nyVxZW)PQX=ucm*e#-jKd?~NY>0+ zH)MQjAe8&ox&Bfygg0Ej{Rt#w?OVmY_`^+gP2*j;Gk8`KT<}tV0$bin_LQ96_(uf7 z-KF_dHzxC-+$)V|d;8HF0cj#J7<)kfEBhZSb$$0zEe+HTp4jEM+0uQTeX5nN%VRGx zKA;@utkLj$iEMqUuukzL{TE;JiT491e_aW}D)X+_o`Y{>3%ItoDW#&8vW99+-)S|3 zMe4R^L4+MDGN{?2Ppj%?@`wa@)J;;~2@qc6JU|-UtCH7WZeX;$|CTdraBR!J0}0m_ zKxRR?$}^t4BF<%PrK$f!3JfeW`Qepd46ff~!c)J54t*xTg|=4u_)D+he1UC0k(wj& zEs?n#tuoGyBf_dulCh_)#? z;B^Cw@#ScClddNrDO=~Smf#guoaJ+JvkSb?)AbOyf>HV9r>-sQIMTgi$cA69b=>Xh zPq}>zuQ>=QbDlkYj{l_8i(B1lx5vF8Pt2^lDZ%B`_hHPyy?blJ>Ky)e4JpUYn{~4t zp&<Xxg)@q!L=w_JIuq9EQ(pOuco+(&vilpk?q>d6tGY0vwD7(XM8 zjfHmDS#t2tvObo1G)!d|V=>_kh7RI$|q z4NRrYVplxz3{+@Xm%OYEbQ(SJ?se5aVz8|B;FlIR%X+Kg`~+f^+=)Xo=tuVLl=ND9 zU}>>egTBAHTu2omzIgk?<<7^+2{cx^UGGO+rg&LRt?6hz;L-f8d%I1^9Za@a+~SFZ zycAgjukSb_#mutGvGY>78^v-eyWN*K3e}twMm`rOj|cd|5Mzm@SIs{;hQL^<5=R#% z8lD=pR2}6ut(W&j(72Z1$M!W*Jos7cWp-Q9QlN8C@;}q)169B>oCF5EF0kU{7U0-f z^;WrWWR{7LR=XjG$xc#;G-xrLVuHU$qA%RD>L^>Y;TPGIFsEco3WF=Wx}hpC)dNcU zeJ^+_6g!$w%@ZOLZ?d)ZIcq3`D^II47GGPXVx3b>**yf?H930YJcCrbtr$l@CiV&I5VaU*$L(Ji=d@S(EC@Bx@pZ!!`w^-yX$C1w*7)q; zZ`naI?*yk7_ltf@8uHIr_zb68@nYF)`$@{Kt}mV*$>l4rB8M_c+Eo{7V)C0*};M|-vA;})x(tIQMl6`!xntd+3CZUW8{e;r{z%E}}7bFIw ziQ%jWu-exCd-E4G>31}^7`TucBZi#^f~a%HN!fW7jsBPZsv4iTk_~9&Wh_$qWaswb zR`$YWz8}5JlHRrrdq|W{}k63Fd`>1zx6?_+*td zh6CYe8B?8PKe0WE#IP*%>O4KN!AgVl>$KdiBrZpPemFO>6fDJ>IN} z|Ln`h5U!Hz4DQKVC-g)=ntzkFmE0_t-5CR<_!iCCAh3 zUl*kx?5_Qy8aTYo&xoU;!1YpQ&c$?5dcWOh zkM;)czeNZMtF)d8h!;tU<7RZM#ljmGD}> zUi&5@`E70!@yAdJ(lfoEp^W>4innlnwDO%%fWWkWBH)A0QY2UaD!l8lU`O)@2R0eJ zj>|s;f-4PaRZ~cZvTt>&t-p!w^!s@3A?t!?4tL-WKm2XdfYos5CH3c2&iYaA7f47l zIh&Pyhjanu@(WIWw{PFN-1!&oM#G?qzZ^((ze)B9`Fh?l(bU{yZ={3h>H)v$i?})D zq^&WvlV?%$sm1uY8nK2)(?Y5)O=NASnAPUodbx5L4xj8mT$0t&>*o=XIeS1b$tJT&#>TEb$E00a;_s(DAAKIS5EZFF-&F zV~lPw{g9@vu3Zan4Yc%>d;3Du-CoP(J90$rrr6o>(z@d(Texg z*d52}(zu$G#~w4GWGC^D;~`*65`PN-vm`*0(JCyR=wSZjDoU`|vhczAOX)Jo+PiaC z`kqJX@h0c2DHB&!wiWB};x=E8$i;=3t5vvG(Zv?JYECN?Ypa~>RcU2J@8n(2*Q>sn z+vJ-}{EX(#mmRb4RxND$QazwYZl7=|gbdh2v7ZfS;Y&KA7^vU;60ZF>p`C*#eH_mwm-i z>UcL$pTxBzVTh2uv{!Q^l~LrBL$@|l->CZ|t@h$W_w8O>eq?-tXME{ZEgwtuaaXng zhLH|kK7thmyt%6L2fULj*N8)O^j>dcczjzPkPdgoKMm-@hfE15kJzayUhcWAA&kR! zz8au?a&i{jg}jvz%9v*#lFa;_s7d$3xo_2z@b*1v79&SIa|o-1b3lXkX6Nu*ucMro zVacv zMDM**|6bUrtdJ^4f)lj>KIRE;mwv%7>Q`LQh`=5X=PN_akEsM$G zYN}^)EBz`}w5%!VaxdLihvT)^ zpNi#Lzg;hvlbc1EQ0UAn#WhLd) zs6EoPGH@||wVzMkb4{^e_tZbF7@?XPf;`{1de@RkIU;bMKV28+cDTM7?~mS>?_HKB zJGIgia4m^QeUgt;u~!#51D88yZZD?#_L)$RwKQJI;4119vg%UI5$wgMQi&oS8l+#p zQ-*IY_c8`lL*B_3UYpMov%L1Y^+5k4N4z8a;LhgbO5u%AS+@1Yr)E2`+p5h`oa%Q7 z#Z?0PmC37CI&oA28yWk@9brwKWa@+cNv2*-u`})H6 zsF$x03Gp>LmCKYmEA?6}230aqSu;G{OLs1)35llCZ|ph7y?K~jI9g43=gpT zCd@iSvXIzb3k<@cq-D~CtgbBfY0Mu45)b&8LE{X z(3h!mAQ+CqhGC~~{CPRmrw=L>{Tr{=l0RgSfeZ)kw9woOMbkwdV7+M zWNQCmu!=E}_R|~-`*Or$Y6AOqz^VW3m*Y~aUls$2Nc!sYYA*@E1Ts(SG{#zzH-{hc zA-#;9b++!rBEq+hX49D2?s4ej|2ZVmG(OSE)E3WenlFlr)N|9(shc)>DwiDH!imUJ zU+fK)VEy28RC&Yt_8U=^sc6p3UL-`Od2O(tHZ4Kg_Q#A~b$qFimXCM^;_2?V6Gzyp zCyEG5UwX}eF2N4s5r=qL-J!xmd$=Vi#(1&#u|APPWVtJTKa@FJF8)w;)LvZ2%dlJ~ zDm0@1>WN%%pvs#ikO`=n{fUfz3a-uQ2Wnv;+iVJXGE*vR&6#((;c5mn zxldSqFo4V@!YxgVHi05sM-)@ki^{{#Ux7kIGhOSTCY3sh;Iv_Fs@S^IRy@%bTFg*R10&N^JZRB8q z(&Q^*=wh1;HqL=p43PjxvG4IyBk( z(2TW7QX`U!i`l&M3t*<4`N3*v9{(2Wl9UMxRst*f?%%J#-BKwwVd(Bn&_1f^CEfk{ zqH)z$hV9q%d$mu%5&bSZo;KeH>jbu!&?m$(0zHzs2-|E@w4JiZ1VvNg%j7Ac@AhME z^9@yJO*O+fk3Vpp%|X6f7iR+bECbJp*!F80w5d&8|4pyyd*1+4q`#yG@UzaGdI{48 z#V4Mfm&ewyy@_5q3-34mkGNM>$>@duO^5$K7j>g{t?l1g0PO$c#U6nhrW~%1XlKIR zPa2l39I61ICUWiZVQiv2`SGX379Y9PxlbkymFi6jb;S<$Hrtcf)5j?z%=> zXVFRm{*jvB+PtG@WBzV48Wp!Kn5i(M0pdUwA>r7;2KYv7boRYR+0PB~;W|SGHC40l(sby+Kt2OQ-_t(1OZZQP&_U(s$#aVl-b%K zdSo~?h4fjW+a%e=>4#!qhw&SLomUk3{%DV-Rt64CaC^`{&S>@uZnq`un?Bv}9ig6Z zxqT9uY-;S@>U>@W!)d<<6|y6BeT@Zw=zmPrTUwx<@IVEGFl*8Q=L;wFV*z-_pelU7 zgfsXZ!psa;dcx3{pKGhgNUyZ|xePzv_sZR73MiM>+DEorj-IcJp?fj=FGK$z?qQ5_ zI14MV5+-fG5sHS~Q-E^s1Dfm8*l1-+!W%m1qUS&&Q!XUFcK%T$>@th0n{rwz;JiNWHD+qB*<0h0wJESIrUg$Eq<2};G zV~GjUC*aq<&Kx`Co?FfQs=;L6_?P7S4OQE}t#xX^l5=Ih*K?rvxy#tr9qA*pKw)z8 zpF0FSka-g0k^GpXPhDVWBHIW1dX3$qH^MVJp*s${r90opfifNBs0h)|o}s!Zo09_1 zE~H;}9Vl~EpzmPiF%Z7AO&LXjpkADqW6A=t3}Xt;D5qX;%|@pJnYd8Ok@2EG*wuiN zEqcD-5)L(rBA89qbE4n;3gumMQT5LYfUuq{JKD`K85d?!Q-XPd;1&FzFpXB5!%7A6 z&Bx80k{-gFB=l(-Vd$}spg<4+PQ>?L)1r{JZ50&&j&2CFh*8u*AhJ&Gsswj5y19mE zfs4k;sJQeaIy$;73T|t+t~Qkc=M?22z;FqfPT)y`uf;tv4fHMWCdDQ`&)@NFLb1(R z4Ze0NH?Ca5v18@Ezr9oUb5ojUn_jajJRJ5nBD!Or1r)kYxqW3q8|R7!Fi!WEO4uQvC7~vzuJFUBzZUe??^={h>b3%WZz`bPuZ~p{ZMzfq?n@4g^D-9bl@jE z*#rN-eXVJfPYoFH-$XwF2QT>XN+#O?xW>rboCS^qU}JK4bIVuL+cEYe0PHDC^uSFoqS4nbY$Q(PbW-_rnL{HDcpdUV0#WdnETmEVE~LY~-C z1hbH#9#HD&PUJ?D$sieAu)G8{Ccfzc*MbT3*4+g4%vi?9sq&2Y7l(-bU6BWH<6`m( zlr{w}eh3O01T@Z1_{WV}ICx17`A;95CeR)MXUHf>i9z2TgvBZAg&70+T?9^`XPxMB z8&Fmj*>b^1nVSHu0k-~l`X~v_6J)Zn!LI2E&@yVe0|Ng@ilq@gprT5|cl(*&hJvL+ z8I=ycBP`MP02%zC!sOi6;G$HTbC{taiX&fpCW(y_IuOyoP@f`3L_rCR5|{rs&Z`lO zN?tKI{cl4O2e4ljyhJM!MLDovPzLaGSmwt-3Q)lfj0*m0u%ra20E$~re`ynF2tfa$ ztrZJKpl!;;nP|F!^S^Z74`AXbQ~@)QRR#p;EFFr8s!u)c<2>>n{%$~&KcB*8Gib=Id&FHL7`T&`>b^g(mX>jpl`HAgs8Tqt zTC>*tD%E>Y(OvA=!kxx1pW5A#2MhywpWQtDv*X3(JwH>kag{*uHIOQkVyA@nBsu2r zHv!9w#nYX@a8UxV8^>haqqh$(wxw@n`6+nthTc%7vFDO=akfUQgs7T;)vEm| zJ=^Fs9!*|JT~4V3qoGpT(xzXq>dogOMT=jrlPTicZ>^pqQqKc&B%K)E_Yp#C8yEAl zDD6H)5!rQx$JtL&Qhx}%P~%bHO5H7tm{^p7m1)mx)AVd zz_9po7Ja_)8)+2}iH%|${Lm0PPi1NS;2`*218z>*XV@g9pObYtW{l2dN{;0&@A9DB zN$Xsl&J!&atl$SI!&6T5K)Z{L4KVbu%u5?t7C=f$!e*`Rp0%(mh5P02lzZ&viW`zW zx_GU-9zvrLOp4#~`;UGEFII1L*RLFZr?ZbEi_TNPt(f&tgOeqns4wXsvSKXh%7*Q0;z<26K!%^B2S*BV? ze%PG%Eb3&4*0dO+@R#svmhMq)Ks&6^UaeDr*I;{#KC*C3h*H!n3|}I#>99h$Uq5@w>7Qcf zd6mVnQUB9sYR~oT>QS!v?J0FNtf3FC;_K(1p=w+)HKzUrw)Zzi!!`qzq>QrmlZiu# zKQ6Z7{v(Cfb0&b6tzkF*f6#EE4CMR!9-{^q5Uia`{hWn%u3u}olcrU-arHv4)9x<$ z{)hF7aGOE#QfGMg9Z?nHN~El8RNOhf@l6`NT|0n~TH2ze+h7n+@tJCqepJQgOT%I4Z~X{L za@&E=e5uE4FtvU{?VMYQdfB);qs;e)+v&Ot7mZR;YR}Czqw`fZ?eql3 zd`QnF^{M$#ru=&i`}f_$8^D!=9M5VO%rQQ{Y#HXj~-9TSM!+<_&Rrri|75JNg( z7{eqi{Ri>@7V51U_J6;dI1OuMe|ysd;@4MqcBw|f!ql2POg~DqTv#D(k;?;Oms`{4 z0rAG^_yJ}5EHZYg+i2|GB9`S^X^uzC*-Wp%!%-Mgo>?EY$^ZFUV-5DLlhJH{??ll7=h*Dw6kW9 z7dLwjT~eRcZt8ftT$buGKT~bd(VBt1oUh?yRp22$IYK5xBx0NU+M^^R*OPG2 z^^xCvD)*K`f1=W+zG_#Ok@gw<)0dT@N5L9jIf63B>zlPwg|!CyMAc^JhH+935?UH| z)GRlv8uu8y8~MenM~y)#+4Wo0dT-o1LZNu-1`kod`BoSsw5mqF^`v#l(6Ypq?N<{S zlY{uO7|@svz0FF?CnQ^WymN<8D^+CZ%i!4}*~6JiXMJok8~zwz1t53vYq3FB5WvK{ z9^k+rL!d$9$1Z60Y6Xr?_MqmNdibL9)x!1H%^>~{gdInV&D58Y*t$hCMBL>F{Z{A; zvJV#y9|@jLw4&n9$Qh9W>{t8ohwb_ThwHLuA_u~|4SUSHb!$ci#78HTY#-P?BYjN# z1GZM8833w8a}P)l1}<~yw+FdKPlEV0h~xoXQQf!#QP2hPwj%H86IT%9eAr;>2QLoF zdoe#fw)1Yk&}Mt|XIowp@&dvS*3sMz>w+2mPwA*#d7OBrcgSAgPl$Cd%u7=zCOxWh z9zMjtNvt6#Z5q|*@i;N>bC_y`!c*mb$2(PYiTXdxkha=)CmIsIz8T8sn&sP%FVOdG{?DZq*K>C@yvmcTIExo@cAkG?y7mWpJy+f& z+xIDxkw*0Sp6y<#n)@UK@Qw+m@NlRz0mGMOg#jqm_T!ymN~xvbU@OY89)8S#KhTm+ zxh~Hfs9VE3bLKRr`u`P;bulvzXxvZ8KOCFNcHV9Sa_ z;mBU{^9)y&G#Hz@>sfu5tp7!nA!lH9=0iejM2{G)4Y{;AS%%r~(*&l&>Ia)v$jn9d z`F54>#XbT&5fyOybj$SfA7GK$} zO;fP~KKIfy)9N9*c8LYN;eW=n8HDUt64nAvvl5zTG6z2N!wsu8yyaH&2Xi`|G!PLzF%>Kue*d7?UMNG#y848nm&4LNWpDCOYd@Q zagp9Bg)PQQ{Y`d9z{!dhKI)@g0cCuOfBLB;9W?;vU->oTvAyQMud!*S*Lz|NFF8rD zudyA#=JGm(mZS>07i2gfa;n5KT|z>rY*7aG#jwKQs-$n(1ikM|_+68Cwn;NNrqX9l z9akDwUlhOqb++h+o*vA)8XS#wE8CA4q5J`0r+RyQ`L!{puM01Ft9SaN0pTC4UR=LF zytOm0TW;0d(m1bKa=0mizsf+MXN;0v?uz<#>%X;sB#`T*)cqZz3wH@l1qsZs&YM*~ zRI2yp)#mUy%li2e^oz}mRU;uZv-N?%rjH7+vGR^L8~dM2Cf2{m0`NygPcGX1OhT&D zAOi+(6n)P8O3X5OPy4EbcXY#zd%6{hF$)q$u|qScC@4EPQ$}%hZ@P3Qh9IqaDIrkI zOuhH{zLMAZBwu6C&~;P>_tV>88B=V*(*X&;RtmAS!i7;V>VqxEx-TqwUIxY?NbHw_ zOeB=;Qzop6(v6#HxCfI%5z>63=0SZS&FOFZuNsHvD_zNXB1V1j%sbv(&ZIAegl}+) z(~m_ENsU!=hI0xvF2jj)ZHBC%Ce=o&z8<=~T)H&YgK-XpWq9YS|Rhj$tYl9-!D?=O4C|)e8)Cl8*_@ ze#nFHBSkbh>kw_Z^89;6^7}*F)bqJCgX31G5#Iw1q4j#$oV@%sKtSTHRMT~Dy`XtU z-ZRcFPT>iOOA>`kNO?1dpz;qXoNJEo>$3HEf17Y7WaSa6Q>_(u&sh$ zm+h0>2VBzHx42`MhJ3tQKdR9K(CY4a1r+vFvc+s*suwcGAYC7R!3@QamWM#-A_Ld_ zpH-lQ&RXDrS2@?&1IQ*UozZ+1VVW=@@u+>coTB+p@TgkGa`8>29=rD)>8i^1Da})i zeRpnP`7{g2*F2g;C>gE8ysMQBpt^2XF-{JA#@rIRM* zbQDOl`h%@pdrZjGP(@`kAueKQ%Ay3SG_iu(@(NR^#>yLSc4JbD&f+ZHJ8yRdtYF8tt;L1z^<5(X@gMn=NK=9$3140K?B3wveq#68S>ZsJv<=^$@U(i%XoQ$(BcHr)@O2wrO(fnChbpmHb)%FrpUf14$Y07=b;$P zKF#@ijhq`?zWV~!Fj(vaOaJjk-sLQ+Qjm*qkd`JhFIyQF|6))Z9aKNLZ2#!9-H@QlbQx z&+g@vkd*~%Lkcd>kOY9oW#X(Ii_I5htV5?uVdygj6#z$*5XwN+HbI~Z)R(f$4@xxzoIBfouJw@z3A(2(zt&`BdC8mjE} z?(^6Hn&I|&>FV&&=gaAD%^Ha=RG_FFMo20`>FI$ju#M)^V+Z~n$Co5Nvc5}{c&SN}* zD%Na^ygN}k6AJXH!?fCPCIRNo!KvO!qiuE=w8RySl@M>#FA(LMrG1D>qj@qZB%U@u z^HXLmfeSqS*@}~*!B!cMp!?@3kO4fhBL}RFiS;Y*u7dEO>H8gFE>6^>)#Zwi_H|;_ zf2!7|)JHx%@-1jP0@WWem}H-@(E=|_hS29MK#tf`!Uwqw;S93Ym#YOY2^)Z%KhkY$>f<3m)T=)KjLeu|T;S!D?}I=~L z3G`%Yf47ng@Lwsy_I(^iK%fkmd&S0R&Kh8nXJ3FNlj2^B?^3q)raQcXU%{f2wZG}u z<@C?bx|J7<_M`TlM6MYnMJh}?hdgQ;ni~|mO-FvX9ZbLcj^k2yCTb8M_p{pE9$QTq zWwu8~kZF@O=V~-?!0CHAHR3XjQ9H273KGy=DKgxk@m?Qs{8agUzTn0cx zo$c}}3l-)0juT!mTGnRq*NEqv`e$it_|05((iwb536f^WM$_%eTk@51vO-hM$Tvc= zpk(<)Nqc1DV<$)ebq+bl#IRV~c-z0A%66ltggWfEpanv*d%iy*>mFS)DJA6#lIN){ zEh{Do8&BQrTbOB{C5+or{y!Gp%VkR?rau}F8z8DD%0s1f3iYm(u+_uB;TEFl+#O>B z(dQgSt=~92rY&l@#X}hnVPm!%L-QjG-|vqc{XWeYVf>pw2=}LyO_kDC4-tP%cY^p; zOXp>2M@^IkZBBBAe;|)HSoSleWV}&fb`?!&f#mz{7ccw&}o$ytHMP#y%4sS`hZLF<9_cq zE8X;3z*qaRQz`A*?Ql?gVOeMXM^vI{44(#7)~)Gue#~q{1h6Nso5UXy0V>qgt(;nr zdbv?~rOwG~)@*nNHKhASFxMOIo|d!iY=5ZOon~-cf&#agzSVRZDjEoydHOi98mJAj;4>IDyuN6PVquemxBa&H?<+U|ZC?pw#eCZ8c7Ap`q^+&Z@K*3KB4z$l)NwT& z`pn-P;K0p!Wf&ey4*K;{wL4Up*MC8^7JL3LbwTj@2QL_tOnht~1_Ycvg+sF*;c1m{ z6z<32%dTXk(=g8(pp1lgOe)n4Q*VS*ud_KV^WqnXIBCDoW2Q0kfRsAJw6gvflLqQ@=y1#`(WniOsZqMPM z9XS>?h-N^lCx%l2J?8JNJbENnl1qYwyRr~eLL03aiM>JAR^1i0fY`x&pFvVOwec4xD5jA9X^+vn4JJ%(kfAcG2$$f5HN@u9q4hk;3VP~x{fh4Q-_AfnimUIgav|YKim6umqvT)( zbI*~UF^+OGB6qvQx$Vu&(o{0B=Q5Y^9*~VVaE;&7+A6+M*X--q;~VO`5R&;fp(_C* zwd?8zV>PG%TlW|=`$tmKSHc(*Ei;^u9y$oG-paG5$UO(8F%+6l>9Wmaw|N>5!_`SG z>xxX>0N-BGW7P9D$W0Jn(DchXYOd4rKtntn+j4_}0 z$VRDAQEa@j_$mvu26`Nzq&@+c9IGsD1y&%hsrk+FoyL-tQc_BmVWQA2PR`YB&76lR zT-EoCSe!FEp>}woFgtEw z8?NG|n11RM7>xKZdZHtMQ=g+V@0#;%ss4xay)O8=-fee|-6|36;5KqN^t=bYUT`}P zIESpPZ2JpRR+Z0%kgKAiKd{G?y7SpBL&k4%{t%hJ0;ZA-E>)339>kp8${RF7_8cw! z14e${^+}?QMJAgb2zupGiXmF?Ul~!$Qyil}G?J2M!vd#!k*)T4wy0m8FC&kDBA z9b9tft3=7;?GJM$QzKPZmIyXU2!aN9jDp9m&M9N%DJ6EO3da*8-15|8PV9ttX6 zopjTlzBMUGvxECrJE-6La9x5M4RLKFDDiABgG#rnmY%Q;8bmc=`ODci4jf{| z*KMrO5#|zeaz%U|@YT5YSDU|G^A%8G%bo72XF>zQE|`akd>~_Z`O1wY3OpP_IMf$$ zur;5=Hpf~$UXDGcZ2f|ax;b1-i15zW@{AuFU>J-Co3BgA@R6_rIo(O}j#cyI@Vdwk z$&Ph_2j_ov83E*>8#*V5!Cy6C&`KhfZr>$=RYY6edFo75W51GV@{JU1HNMTL>l2i`t6>tx%<1mZp9L~5XRuf`U9%om06hG9Wm zhTApIv@@?s1w5#5UD;i5Y^B;Ham->m3dhh3U9wqq*Z;D)kraOW*|y*)z2e6)BHn*~ z+PHK7G;+FbRM6&XhC8_YURYHiVPU2!goz77Og0Kg>$y<+IY~BC7xzjGW zD9PO3CWx*p09Zlg8>Wa!%&+n}qCl-Z2iCrKhZwJkYq>c-J|3nJP2XIPh`iT6h7DR5 z&k;f8A&XsPGa?^k1aqHg2Bk8!1aSY;TK+u2g}D9BAVGD?$GBc%LY|^M@3O$ymLWe=Y~N{Y)|q z@P5aFEQ!8(S_Ee03bh~D(U9TZ_fjKv&XG~MqZk8`V&-yrdTunu1`Z=B=|+h0?IINN zx^oVok(aR^3|VXb@FM`0!5esX;>r>=>cmh6MVkDBhcjIcJ-(u3hJ$!ztke& zdf7ey{Bm%R=wkIX$KSBrvqVcSaFFW2KBvVJaJQTin|`8YDUayF1l)fV_;#RT>DAZM z%;m1Z+G?V|z7t8GIG5*M;;!)d6|b#Eb^0TmAz&f)OlLSME#SBvFKIt+!`nD$AFphX zcOElvF9O`VCB^OvI{y#{wcIs_FD1d3uWs1tk0!)&0}u&=A_sAvx6{L+Gpx%X47bL_ z+fw7>V0!~DdB--(OCrFXk@((e?Fe%xOUB)0b|m~9z)u6%VwkOVtiJ7^y4-zmVzUjP z2W=2)8Eq+hD>ve+6}4)g@E)uH)(kn^pt5W;p{&c3kEGat$Q)Dq%+-}f5YHBo+r!Dh z!C_r$Si9k<6zIz5hty#IS^W6r!<-D35;qsq<!S=TE<1f!_&x7C$U5BnKpQCo<)o!zBr@-0_D>+zr~bjNdUx5-CL=` zh#0CY23$X_ZJxHba-at4OQb~fIMKZsinO|3;@U41%M31JuaznCFVdRBXJU88w&uGR zArqQvGK`|QZ#I#&n=nbA9mV6Z)-xgN&M<<&`0DqW;RtRJ*_-3krRv8f>s_zst7ipd z=BlsM6X~`XdwA^G2OjpCs=O_tH1*qBPft&Gh57NZF>8#WM)_;l*U=^SAQ^NsLZTKO zK^T9aB1pWKHIpLCO>ZGuLIx5%oKMPj`-V?h`#vb+J&S}Q6_ojzdc$KIQgvBkRetk* zgRd`g^{)IzM#R~7M>DiGAqJjdWB0IqEzyNRW=c((8@BxSrwYR9eQf%{@BHe!dd9PA z%N{OdH@viN=vbfkE5mcTaJ0nG$)w7vza@5IZ0Ho`n>KF%cQ_pVlE6umb{z?aHhQm5 z)L1JYRW}g0NMXB!S^WnYya>$aN_c`zfc19{UXnzEEh8W55X0`^5LQ+T(%akH41grP zpMgYHC%DN7$-T{C$j6vt=W?=dWE+^(2cWd-S{6 ze%KEPe^6aw>!ai4tAuW)4AKEjVT5!?kJQxE2C0QU%i|40g7#lt%oSp&9qLoI+oZB0 z=s@|w6}XO$azhm{3w?AT*|irYfY_h#boav{-DhxJh%O{#(gL~nBmYB#rG6l_xW`(2 zWd_wf=O?DUmD7R@ub{{?(ih)2Gq}|h<$?htDo){h`mh(EmT2?&pI#pKqvZl8lh%OK zm%!ajnZp3EZP-mW9jb0NZL7`SCz2el?>af$u9*oqF6r;@hbFxA9hFTR+g%;oEFF|S zmw>q$D$uzPhxNp<(G@PARvgarMtVNyEiEz-Jl@d(v9Q> z1f-=~ly0P@rMnxXyOF$$bM)Nr$Ng~~jPt`Y2JZJ=Pt0e|`K%4x1BRX9bL$r?5ewtT znDlBJ3f`B>{Vf5=LOoqwJh@CkF)1lGZWBei5g-7a%V#R-r`qK&e?=?w(#7n1%{>%48h)fO8lFSVqqDU`YI5Jt}&nq)Cv-O#(n`{bnozI5l*`bU*tWERzzAYQx zVCGd15X~T7KD=;|*7d661Y<}8sD6}P7;;=eQ*rCz1qB72pzW$^m<91PTRr-KZ&T0G z;J7`W5D*aXO)imrirr!3HPT#sVh%`}CP9yjgL3A&oif5T>mNQ3YtlT9x~{=lrKur2 z8JXd*nZLeSi4ba>p)al<&fe)ub>HKevmX$!DFAsXFBV_xLQ-BR#|^$ zUAy_E<-TOhfkyM1qJim$S=QG5>CT0){XN~W`_oM&;{6vp2{Ya|Cfa#%Y~4|Y7r{Gt zNC9fA>+8ix@P2jK+1VAab_3*&2;#JvV*RGqS7*DZpWam!O@jcAX=!Q6`S$Ibu5a@j z8UWT6-@bi2lbM;>0a_IR^I=&-rbz%LJTdI}IOWp(1iK=%-Jnw|ZVUi=GaeqEg=*`$ z>16MU>^CH67kTOFZ~Fb8qU&g8r7H=irPv<#adGX9<*TPvR8%~s+_g$gxxT(0A2J>F ze-~$@Y46*-h$0sFEMb)`5h-Zqt74WrJSvgL3V2oXU+bgy>h>EiU=Z?g8;}<#S{SlI z6(IZfXB|f5`u1zLi-cXzYt|Aisy-44TrM$cS3Pr{nzf`4yw&cDBCItr&wEWVXH>ox zzX#TS;uDOM|IKojdxq^;x%a_8uhD}ZY#c1e3p_4v_yMLFQ9^Lb<3fd*@^(iUK5=|% z>TM7pL8KHF6-U7D|N8Z-6u2y)F49Po>WQL^Om`S~{wy%YV14ss<{xU|p-#4KQ6~00 ziJZDEoXv`RGEzOck*$>>rCmi?ZJzGIFQuxH) zKxZ{E!ZShW`YX~|K9!%Rj_!PWqFBEl1go8p_;7w^JJ0`5Uy%ApU+AXvrVR!u1e0S# zghbX21tHTn3-CZRL=k+=lB&<`^=L3I)kCoui>#)~Rs-{Nl;?SpxRQq1l)4sPr5UPa zu$zxceGq=LeLA6^ra$67y5iV3GBOgszPcJaI`-{g{L7n5v)q&_>!~uMXi$D;yiGhU z!Ef?^PRM)Wes^=O9m52UhXWTE_sQ?yzjrAMi#tpAchoSk#ELh_LbB`LtzD0z%#&ZU z&wTtdpeZ1$>?$~#cp^&C2vnVTbUyFCAE{ksSwji3&t%3a)P-NqS~ocA zDy{&Lc7MUId2tN{)g8_=cydbZcL_}hk3Lv=$6mWhqrjq?lJvPwiDeIzj^cki+c%lA zZ(a9Z&t1Edp#ALX?e(}mo3XjeEE3qNZ5SOLEu>c~_-Q&{-$GbgRkvERo?7>II9vMF zc#*EVIS;5dm6lUSepwNMwIdeA4OJs!l$2pb`b~PEbNA3QF!UB|myT65?Ck7JAb?z( z2KhyY5+t**@+HZd4hP_yD-euD^apy%$VK+0V9$>!m7wq^s3!7qVWA5-)JM zuY_?`b@%lV5(h&+HXU2*aQ>nRRdLfU7|w^vRCTd_=QC_5pMWe=oM`mCQmaY&@}(O*ZDO74@tRO9-Dhj0^d|S4(+MtI zPD>3LRs1AD&)WM83;@qnr*@hMm*%Vkf@)r?(p+^VBqT%K!5iJb(rm=|7?7X+F(+oe zSJKB`*oy)!`w9s_P4E>gpuM$@P=yb8mU@AvpLUyw@8DZowvtx?HDLv!&;Pj9~R(4}6yi%~{2WY~LQ zsyJc0`?fxt5gqpVfuYF4D{dKO#d7>xI8NY+qmPY z$o1K*x?MeUFrgz%;!vVuRMMoFjXZ?@m^R_(h z_G^@@&h_5V*7^8`hK8R+Cxr4* zNF)_Q$q%fmb6&RdYoYx)T%5Rsxs*Axx~pUl#5%V1)br$h9_;;OllkL{UJNtxx|JA> zWhu1M(;-1+am2T!8VvY&`1r)%*Hhi;W_Ub`)k4Fe(P+<=x4M0vD5R?a4kRC8f6=MkIqqe_fZ$bur->iW<`0lBGD3IP z$GLB#ypTST3J3^D$>7o$y<3SSue(0dJNV&tWEt%ee!!q#@%$0VgMWS&IUb>j)Cb~y z5Pkr6|MQ~y%@xZN7SOV@w2klu}>?27$}7@bH2%Y(A~<5xvi*Q>lf8 zX9VR2O~XPzZ|Uitl&ocfLcN9~^|-=5#oRtW%R3+OnK!F9wN;9`r>dE zRAujvFuh|I;`qM7l+)^6$F2`bRN&0wyuE7fd)t={(T1U-*ku%qAysP`uC~kVjplallVGJRPF}a-Q4VZ zH%ThSsP9I6E;DkQLa@3Be|Yi&6$LkfWdJrYCAWWi^SG- z7`i0nb8?`&%E-DQ6!dV}>`&pF+BVUaoU!e6EG;d)`^;`OY=h0Hg|QQU1|}=L^R*wR_;sy};W}eDkHdQ~9=;-L+F)`ye=;`QwT5_g!cMl8BB8fcg@R@jF zK%Q$eQ&l)V(7C!W*J|(YJ%H+l@b`EIt-#LSM*$qoZ1Bnv?4VDg5>GG)wgb1ibx0oE>$*bhspPe{1O{+_V(`JEzO?fn5C4fj@R;dIFYceQ zfDMxU0oME6cwP#+gSzz1go3k(6QUUHH|a)(3lndpHl7At7?%c=?({xbkrj1G&1?rZ-{drm{;s08L?)=hc_KbXKKwIkJ+}qOQ-7R zZ~QoqZ@2iM4Hps~KHuc=`{;R62H!SUQnt{^pcq1TcXw@=^yTmMw1w)fg86irS_wou z9%56$czGWc2Ni zkB+k6SzT#fRsQDCHnX%WSg5vHNMOD+|KLi;luFJojRK<+MEGB7gYvjF=hkrRs7(dSLfeJVTTWDRS!5Mv*vcji4$m8w zeovLp#Xk%Zx?Vc2nm!-9T3T8XuXEZBd8MFmZjN16UTy}kSG%gJ>Y*NrCAO?$OfP-W z_vO<28Ezci^ZQm_zD$+PLZh(iSbM7JLXy>U0*_lO6i3JM-n<+kXI=+&pnxt}V#_tQxBl zDq}n*YaFe~_rB*HYvk0sOo`|O1}gM#+2c>(apG_HgNQlZqa)P~mv~z8zeAq=4UQl7 za3sDi;fz}F578dn|18`G%W)9m@{H<6^^m^XW=6xL14@)+a99=DAlVEOF7U6L&qWBlIV>Pp2)eiSCxd)aJ7*jlND!zyF0|Ks_v# zBF}XV2F4L4?%Lpfa3*rwEu(^r@gRs}4w>m4OmS?i`<#7_ z%l=hCOtp^-E0prkAuwonCC{@%Qk(egfRg0SV5LWK;lghv?Va+gOfDRG3I7fqx5XcO z1o{k25uI)J{+$r=zY*Z$6RIUUT7M#;68UA`-@7p^?RXQf7jE^S{ybjz+qP}7@bgWVDkKcb& z(87z2i&GLaB&VdNRP^$C_@rK|jN-2s;0c1ViVDT*FM_PLHaI%wViG?+g$B2r;oCIt zFa8%U=H}*^Wo4`~a?&}=Q7@2CQFDJ~Lxae|8feJ;OEyj~k#Dcph4xP;p32C|UQ4Qh z>&x8tYC7|EJx<5e&Gu$%k^nT{L_a?G{{6cgI<Y!nyO zJgeLGZs4P*&(9f%S>JSJeFV}_8r#b6?;(IYE1+o1EfO^U#v0lI9mqjpa8<1DwpO#A z$6@WdhUkzK>#l>a(S64J! zJfrrSs{IMA@)wn2fF|ugj>n;@eVqaAHY|faDH|1slR?c!X(0wmE-iBw!Sm{(yh$i& zVd8dS&qfZ5^o0_-SmHvJw|+U&P{_?qEAvqgr=lV!GJaJ^?t9^i^Fl|3frrS{TKcLD z^5?l28R67_tHys%%FT<*TBh?Nj2k`RiX`h!TZa zmB)yGs-NZGZHO7gpkuP0U-^YHFqd-i8|6dd6v0Rb3!r67ZRie#I8LSbQ9DVZgop3&2zvK$bfRL984mgr{fZ9UM+RtoFpC z5(s&DxHE662;@s&4j363`SFf@if1O`zPR4TB)z2!ex~<)x5`!&F*>hXQE2}r@-At_ z6A84#0UZO1Kkaa^^{Fu2<`VLQR)l|Q+(ySeB#Yjbg?pP}*zg$g4hz!vi+Ri4)MylK z=ordgEPln{JNp(>N-HoBarr}+M!m~ndq}2-1)E;g`IFMSgaFtI^?MFTwz_#pTtmjl zc7;uNe0;p}o!3ppJRzT#Z)R3jg<;9^$_gzR|Fw#fl$>0cQ?i8ew^AGr2)I->7>TaUs#{JcbNxX}jVSEyDXD{#K?`oF6%nbU zqodZI(ALb-JaaLfB(JFG%a3%DK6{_?SAd?XsQUdi#pl7ww`Z8{#AsJ(nBrY5NqtSp zfMktSUvy-Ul70R9wY9FU?wd}g{Ylm4=4O=B)6;KImFZF}UWeGudrX{1p9qrFVg7OU zV94`G>*LIE+2NV`$x5UsYr*e#x#v1{KL;#Ucv=Ri5$7KNX@zK}%nxBCyOaTLeD_5n>Y@=;<%Ph3%yJ6lerWZ+S$WBXa6C!xFd^CA;;*`2;I!iMqj z1A>I|>iRE6LKxyx@1K-pn3%lFi8Bt}DfofIFBO@NNR5^HU%CNCo+qiblG#o0Me z2Owh-AoqpdA1=42cGFlm(tx>mY`q*y!lx-%3he`mzOjC|+z-`uqEL4-YG0iClXr3wkc{I7g!u^VRE1p=rmL zBPf&y)T|6&H_Ek{UcD)-n%@3VFHOs+PQ>G4>yA)#y720NV5!O8FZEBPK0y2qeLx0# z_5C&OpNFkQ1MTME9S^#Q9$98Fp^NXVER^d-WN!G58A+x$`p^uc+Kc@-Jqf7g;%wOu z^tQ*d4&kcuWpw+1J6wRccBklN-g2u*sb zsyVJ+AncV44Ic;i`KjeZ9H9JhDjv6T1AD9jxMBE>#)fIrhG^3q(TnBiAr?$_Fhv63EfTT zJ(Ys8u&^Kig=dWe%n>UF(f9r7T1QK3>zr!Sa|A*DHAZPif`c7hmKY)u5@LD>75V~R zBA0@4bnv5(60Av#i5oaLIKqw|>?zH{SmgmKO0++4z;ZUc--lkRa;mtEQ#aNG*>^8D zDn}vZ?V=KXsd#uv73beQ&weerC+-`m=7lS!sp+YNVzfpW?cdF7TlCS}fiuADMSS#k z;97JA0~h}Q4xnKFIfNfv_r-TMC78tWXzagB65ezShwc#v3G@R zeOg*uX<5ml!qd3r#hAbQ0CU@`jJFvWIB3GFtC*wt>UmUOR8&-ypiu6FUSqk9jk!9m z8;^r@W1oJB?L>W8%=8))GULtV8D-Yc;wX08^m-pv^yVIr2k21K-)l#Mlq}{y`0+S8 z&1LziLhwhE6UX^_R}?^wEaL;<>TNeeqGd)yLu1+j_+_G7p!}>fK#B`-nl8tRLxARW zD(dNZ(^9o{d300Zgnd%2-rcBBn=X@w4l2ccgVe@V-|;kWsTHeG%9;8aGA)KK`rJ-QTm%w~EXAvEOk za8pHBSB!&WY_wWI|I%z5t*y#>4nPG7H_nR3>Ek3IlD&%k2L<9Zv6Kd^Dr-Gni zjyhzN|2IGOKXuu9Ecp39+;))&2mlxOKqwu-6VM^Z!GLpQKjt)bqv$*gZ5=<;CbNnjz^W70ROir3N zk(4Apl{F)c23hI;^}sCc{((|@#DMQN;jwVEcYwb zG4%$t0Tbhm{*G}~+2N~`eQ$xtBVtJ~-bU;W>+6A!x@Owa#&vhYoO8af0D2P!0f8u1 zXu@+N;x7wfwI)vP$cwHq$3{oZTe?=`bw!1U`LlzgYkog@2atS4 z#r)`N{bHkNsDyZwNO5l7mU-d4QT%mO=rNh{t!h8y*KWH8pO?O)Si7;KtIGBJL)TAru4j zy3u#6W=XjL;^OSKi^6n2nbSXiHs$rWcquL+VZId!VBld%xYoFWGlTN*bkg!tB_b(+cV5PjN5h*E=NT0j=OhDHdT3dr z$>6V{p{U4W(iKH{dfS@}lhAV*65n>(nY0T;4b8pe2ijOv3ZHX0b*5En$Xg387M8VbLy zoG#|;>zk^MvC3T4i*(gLga5fa0>LKgsDqZ+vweYe-7sHc6c!PYo5jQPjva^Np`Z9C zy#_b4{X+>l;syJ%l22qUt(7R_V01H;d{a7s=*=d;LmLccn>@mx_8V@e9UK%~R%j32 z+vb27XX6;_cnE7nd#_|jwPs9R#x&uLqokqpi@NQ&bpSC zJ8RL@3V~PM&*r8sCMg71SW5G#wb? zQ>jdiZL7x5g#Z#Gp=@hc1{Xo05PXu?_c}#MaqlPT<9j#I=y}o;%A6_Ser_gdT&V^Z ztYbE$AC=_(a-h|sudWY(uey2&%73`T@E(^uW5oMQOc0|3Vgiar%q-&l{GL?uiHeHq z%#N|QOu+1KgTGoSo19I4@{H+BG}x0W_~!fW6Rg`QCxMCeG}q|qM$dU@b;J1dq43%M zQ_{VOO$T})!uH-rFa}v!$v{YM`y=C@@Dt-Oei zcmggN;gyUG{H6Xr4B!{|PnipHP!Y~FyS;U@0lVFI+=V4vh<65eq-u58lU=h~mQ%RX z0y3f&>%9iJaL}C)+Xd}%Vtj7HJe~Vt>BnD9vF`dbDB@5?}3e|W2wJXHUMVo#rICEoEVhZ96!}5>FcyaZj3zJOa4K^_@oMSkzP^QQ+U6(%4%wH zl3Z6n67-6Wi8*-*nS(;dn4A+~fX*u53-?s!*Zd!9i;#nM-7@wZ2?Dg1r15W56B)S%?#b?{_{u zi`~i?QG%x|;lr{%b|^qz@o%e94d}e*&Xk;Xx91 z?`#4hYcOp5xO!MywwHk{Q5u@cTNE^)vLyG@m2vrq<)t&)5FP^Z6EYaW9uaMh%gK;r zC)$t}segtMRZs(BO9HYEcexMBaF|5aSzXt=VXw7XIXSsRXM{T|_k}Y&;vA!wIP8i6 z`YTQsc>QXN7T*cAjS;H{B(0|ORz5zJ#HI6S;3&_Q$sGwx5%Ru8Ao?*{)@6YyK9x7I zjSprvWAvdH>V{NQR9;PPr(4LN5pHhfFpVu#S>eyQuBQkk^Seexw7##D<-iL&?lZRk zl$VzU+aRY#xk$mSE&p&?F6}03ZXVK1))WbCm`PK9&hqF`K?jY*)BTsP3|sVPv>|n4ErV@6bOuJguy{+qmjpT1W<~q8FV{PD|4t zR-5~Oc6(vC=Bg_~O;i^f&+DkENzit6O)u2DCPhZt$*7c~)U+#V?1;SF z1FGj$3yRdVWu|(uUhy-Wmy2iV;E#gP4SQ2n)tMccTYevMAttB>oC3dpMOOoDwt?t| z;AktqxH~u1ZEM%p_Yqd@w6-U9N}VkYp@37esF)~wL`vG(4(PqC8$SjRXaQTeyuAE1J>AFc zR3=AkDyZ@*SwQ4$cSdsk8J?7QJEe-cwj}T~<`8>b6QAzS1odLYnwwkDQ1S$X6Yz+3 zhNqPX*=-kmi&Ls!z(`NiaJ_b^X)1mke;Krn7;l@w`X8-%_u^n92=$JT)9o)yB;a!o z;G#%*x&jstT2QLdS03)WdbMFD5sj{ z;2TAQ{rLtAbo4AE=g+dnVCos&BH(xV1pf?27|e#Bn3}FxdA+W=#Q}Sj5>BaUtmg>~JN;`p!Jvo-)|N9MAbCur4V zj1asu9RuXunToo4InI%*HX-ce$7DXc;ACq)&xr=oJhcJizffquXnt5%l=fhB`(dGh&I_o+_u8#A{sbv zK4g}Ze40G>eG=_;b7_W40qn3fR9~S03O1}eX~1>$^o$hoxR!moJm1fKN)0p>tzMw6 zE4_KMh?`~g_VaB3+HxweauV7;jymIH@d9?cp7Z?$;y +so~fNy7-$iScpkZ^gyl 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

mB0sXeiH1UmYg{8L*)GddlMwSNi+-7y; zc0?5yBMqLJewWYN)A&8Fr&!tLF!grGEu%Ea40kHfBbFfbUeRwu6V&l@{p_!yC-rl= z-j#bP4ZQrJBopD+M_s1mUHYvU4^nO%R^nTXtLp~Y1h9;(8}I4d%6a)C^<=_kp19rS zHREogð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 z zT_~Q@x6|vb%bhf0`lBy_)~V*ammSg)35n`C=oRq#&5+x zDIa8%EDixi?yX@^A;X|gjs_od@KvhnKh!dUMlBcQ$zTWss9f|&piv7Hnpq)#gX*L_ z;P*!?bu;T17>;Yv^*zg?;mc`> z(R|LraA?Qd!(}MO9GWuyAh^EFdwqag{6VQgfWp|87+9-ERu4Lb_1s z{qkWJbh{D*Bd8Mp5-E(i;Lq2N;|%w>X)YLhl%%3TnVv#17sL}*ed^o2`w5R;VPoNIY4l84d)FE16x*1 zF59%@?ap*GnY%ofGZ9`Ft>n!?8K%Mb>=&wBRWv50W$Q+#M~lOyv2?;bX+xS4Wx)o2 zORHF$C%=!*Pyd5sX6=FRMvFyCSBnajR1`;Xc_~5V%NuMeV+!)|f0st*<*H1XK~WctH4l?uKf&&sYNJP;IvqTc z`KkIJyP2d$N^cl^>bxhfye{t_GN41#%>&fghAuFSNAp=JozNb%viE<>m^?fP(MLp= zqzChQ&6mT_N2J9s9IW|0j6al~mFvH4^+Jmx68ikSpUXzFZRCuJPg}a5iCf7W#mdSo zeUtJJA=$knT$*l9!8xieo^JOv%PLdfL~;7}!06=LaV+odwZB~=e|HRPkw5}q_qWFq zg3t!H@P%LMKeg-4RCEhWyKFMeKXbN3Q%v#r55Qje5k547cz(7>05&)zILiWQ@j@Rs z-H2Y9>)S=X1_y3OXm|OVkj6zr7q}OAb6~?e5OnVH(GyzY!^0q6C;>c$U%~|RQ3HixUQl+yAj7== zcg+8(p82A+O(QA(vdvIWgg2p&cmokybj{?bx=ul0^jdd7$RQ) zUb9sonG}5&|97QZa1K4x2&`(J$PD_Ax@8)SF$Qpa%Z+E>T;H3AM-?!o7_*uljq7V; zX>>)313Ypq`2I4JSNo< zpAOMl19wZSvyXqv0tC+Ynx)np*FPuTpat&d&l7E9&VM_@2#Rl7_kfqu2H)TR`b4@4 zA`$cm-}nwj5Iv~BG6vlmL$5!m?t()AL;mjwjA8*1mqjm?4bl?pK>-g7chZ*DKUGZA zj&1&BqA(zxRhl1GH!9?}>N?1qbL7yjL?v$jGuyQwqbR6uVi(hb`^pF4Q5t|pg_%Ys z|LzR;h5GFMUXV>mEpB^`Vw8GX2fTtpMZKg#dA-6CeobshmteTo;rm@oO5>mT-7C4A zRy``CnSI!fWGYL4Y46MSl<$)A9z~y-faC64rw0zGlS7XH1fd3&O=Usa;NJ#+&H}{& ze>Q%$e~wH9;L{vGwss)beA93j_UlQTh}?stI#2T^x1Qw4VY2?Iv5f(-wH6)Mf;1C!_bshmQOd4u|M1&q~bGL)AbHUpkU1QsSSC0L2D63v(v2&|2Ud82jid z5p50&h#wC%A#(%{CbO;4S`eQd79^1IC4x zmKXn)3R)2IvPy;GpMBBi06Rjp2e)4zTX6#5msV2(9)puv;zO_Pe%rNjQ4Y9Sc;->{ zv-r*(*ID7Xs&%f_FOOnE)*pWj`E`ni_lGit(_c-sB=*!!R2x@DRQ*cWwafrBnC_Ba zDF5uv*m_t(q?t+MkNW=5r(`{>wfV{CDef60uZ=X+c1by>4w>IRT6g0-A5kg#s!CkU z(w`uy+2NMKA2pehVCG8(hw=mwjX^#y1Lf{xp{}fdEH6D27dV@u=Q0V`b^=9{8qyKO zK45orZy9Rfvy%X?E@!*hp!rKzp~ui*JMcC%eommL#8i2u03Jp#ESlX(UuFAi60}me z?G(Sl<$spxvETpG%j!V|qmrgxZh4VbJn3BN15@X~v?>9N-%=3IBwFCtN`c4Y-RMb~ z{m(v|fqe>c5aR!HN2zE<(K4g=p+rN?EyKGKs&5lkK-3O@`vJPO@FjBK#rWTE(baQg zR-Ux9p2xR>_ji66EvMRD_0)+~t{GE04|%i^d(tnv4fkS4np`Md?5 z<`tdpKEbUb;)nW>KBjcC;7t(;$?+G}!9OzgQ#@8Px%q14M_FBs9lA!bIQ@R|$cnz% z^Da-4T^jkjNL(ETFm&+ORO*TU6|rH`B( zAYbEp)W=CC0ZUg%Ae#gFzt#eQKkDU!>r{O5=NnHg)+L|`cjhvvtnOVFB?fMJ=azIl z5P81>`n2M~mjclLG7}+iKN(bydC_e@H!$J=pd}p!wczAvk61X=ha|^%M2QtUq4Yvr zehnu(%ajxEx2V$i@;4)&ZPhmv{*zg#6vR03d+{MT5*!J)3Be?0>bhGt=uk6xq!ki9bXjJ4qX($x|b~!J|spIuoEr-IN$_s zPt{5{g5aMQN1|cD$Y`qJzr{mC7e)+!J@6bl*fF4YF`03H&Q&yO-NBD9%1v1xYg*#% zS7(|5e(fn!t4VvI;XuwTHxt>nzp_SZ2l*N0-)2%Z{&LGZk<$%MX75B@D>qGjnQ za56FY9o^Dlq$|jAegmAM|DS>yU($y!0Q)O16}gr*^*Y^$PRAlPh12;-+PDcX%1G4U zzM)x(fW4X(YRkSxq2`ZT{j;_&Z8|dS_O2NghEw%>*u1FMb$qwIb<)emSH?H2`ZHX` zjKNfg=`sz+8h&KkYlYk+4%C5jbYG8Urpi~I7vso;4!#atXU`aew4P^>Wo{7kE`nA5 z)34LN0^UrIDb(p74W@@q#L1a(Ji)#~z-pOTdfD7H?f^ey?E^w23>fB|#=nybdGi{C zOS`svBIrm1g&;vPijso}f@F~#7fM!g&RKGh92RTw z#$2%Px#!e7TW{5URrkH;pIvN9<{Wc$kM6I#zwW_+K70Ucg6P5LffyyNfx>BSAKHD% zwG{3HeQAKFX|i3sNvzzPV_zgo<^j0yJn$-=fiiento=mCUkMpz@U%h#)+;J0$q^_dM|XWE9F2}QWQfk z(1EX!T)Tu<>-VJ0;?~G5ZOe``r)A;UEZf=c4{vDYSkL2w0Lu}qqj(h81pVGqm@QYI zr}GyCUmiNnnE77@Y*x05SYl%AUXxBi4ju<-`0}zgi>hb8g}f3|2#G4T%Fuxy67t`a zQV;`R?70_&@!owAQ$hQcQy7FKfOz)kKEwe8Z|DRhC-be=2lSXzcMaxsw@R{Ec6(M` z7(<;G>?*hHOdf>f6&9lM--dN1O@C;gXTYR=2JQOP;xC2em8@cRiqQ~6%9@HS0mKfl zQ6^kA-yhdiFX{qbS4Ym(%ea6NCuf1Z+SOIw#y)5L5xa z=jyVsolSb%ilfbPzDS_5aVNfkzai>dL91#DqgdnE@WGnQKX!90p4 zIRoO=?A*Lu3zpR9LLU1@h2Pz)#Dc(ar~(4Xh+_G6LQb!Pi22Knr&!TD>U9yAy`mS- zZ-dCewCEIj2%t9oC(;=mff=uU5B|(V%0DsAuE-hO^&q=Xhbd17KK~!-RRYr}poot7 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 Date: Tue, 25 May 2021 07:58:28 +0530 Subject: [PATCH 024/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20contex?= =?UTF-8?q?t=20and=20fixed=20few=20typos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/bonus/TQM.md | 11 +++++++---- docs/changelog.md | 4 ++-- docs/index.md | 2 +- docs/installation/pip_install.md | 4 ++-- docs/switch_from_cv.md | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/bonus/TQM.md b/docs/bonus/TQM.md index a558c692a..7e06f1eb0 100644 --- a/docs/bonus/TQM.md +++ b/docs/bonus/TQM.md @@ -43,14 +43,17 @@ Threaded-Queue-Mode helps VidGear do the Threaded Video-Processing tasks in sync > In case you don't already know, OpenCV's' [`read()`](https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a473055e77dd7faa4d26d686226b292c1) is a [**Blocking I/O**](https://luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking) function for reading and decoding the next video-frame, and consumes much of the I/O bound memory depending upon our video source properties & system hardware. This essentially means, the corresponding thread that reads data from it, is continuously blocked from retrieving the next frame. As a result, our python program appears slow and sluggish even without any type of computationally expensive image processing operations. This problem is far more severe on low memory SBCs like Raspberry Pis. +In Threaded-Queue-Mode, VidGear creates several [**Python Threads**](https://docs.python.org/3/library/threading.html) within one process to offload the frame-decoding task to a different thread. Thereby, VidGear is able to execute different Video I/O-bounded operations at the same time by overlapping there waiting times. Moreover, threads are managed by operating system itself and is capable of distributing them between available CPU cores efficiently. In this way, Threaded-Queue-Mode keeps on processing frames faster in the [background](https://en.wikipedia.org/wiki/Daemon_(computing)) without waiting for blocked I/O operations and not affected by sluggishness in our main python thread. -Thereby in Threaded-Queue-Mode, to tackle this problem, VidGear creates several [**Python Threads**](https://docs.python.org/3/library/threading.html) within one process to offload the frame-decoding task to a different thread. The operating system itself manages the threads and is capable of distributing them between available CPU cores efficiently. These helps VidGear execute different Video I/O-bounded operations at the same time by overlapping there waiting times. In this way, Threaded-Queue-Mode keeps on processing frames faster in the [background(daemon)](https://en.wikipedia.org/wiki/Daemon_(computing)) without waiting for blocked I/O operations and also doesn't get affected by how sluggish our main python thread is. - -### B. Monitors Fix-Sized Queues +### B. Utilizes Fixed-Size Queues > Although Multi-threading is fast, easy, and efficient, it can lead to some serious undesired effects like _frame-skipping, Global Interpreter Locks, race conditions, etc._ This is because there is no isolation whatsoever in python threads, if there is any crash, it may cause not only one particular thread to crash but the whole process to crash. That's not all, the biggest difficulty is that memory of the process where threads work is shared by different threads and that may result in frequent process crashes due to unwanted race conditions. -These problems are avoided in Threaded-Queue-Mode by utilizing **Thread-Safe, Memory-Efficient, and Fixed-Sized [`Queue`](https://docs.python.org/3/library/queue.html#module-queue)** _(with approximately same O(1) performance in both directions)_ that singly supervises the synchronized access to frame-decoding thread and basically isolates it from other threads, and thus prevents [**Global Interpreter Lock**](https://realpython.com/python-gil/). With queues, VidGear always maintains a fixed-length frames buffer in the memory and blocks the thread temporarily if the queue is full to avoid possible frame drops or otherwise pops out the frames synchronously without any obstructions. +These problems are avoided in Threaded-Queue-Mode by utilizing **Thread-Safe, Memory-Efficient, and Fixed-Size [`Queues`](https://docs.python.org/3/library/queue.html#module-queue)** _(with approximately same O(1) performance in both directions)_, that single handedly monitors the synchronized access to frame-decoding thread and basically isolates it from other threads and thus prevents [**Global Interpreter Lock**](https://realpython.com/python-gil/). + +### C. Accelerates frame processing + +With queues, VidGear always maintains a fixed-length frames buffer in the memory and blocks the thread temporarily if the queue is full to avoid possible frame drops or otherwise pops out the frames synchronously without any obstructions. This significantly accelerates frame processing rate (and therefore our overall video processing pipeline) comes from dramatically reducing latency — since we don’t have to wait for the `read()` method to finish reading and decoding a frame; instead, there is always a pre-decoded frame ready for us to process.   diff --git a/docs/changelog.md b/docs/changelog.md index 16f28a802..50351f675 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -76,14 +76,14 @@ limitations under the License. * [ ] Updated mkdocs.yml. - [x] Helper: * [ ] Implemented new `delete_file_safe` to safely delete files at given path. - * [ ] Renamed `delete_safe` to `delete_ext_safe`. + * [ ] Replaced `os.remove` calls with `delete_file_safe`. - [x] CI: * [ ] Updated VidGear Docs Deployer Workflow * [ ] Updated test - [x] Updated issue templates and labels. ??? danger "Breaking Updates/Changes" - - [ ] Replaced `os.remove` calls with `delete_file_safe`. + - [ ] Renamed `delete_safe` to `delete_ext_safe`. ??? bug "Bug-fixes" diff --git a/docs/index.md b/docs/index.md index 64d46d2c3..0c8908a2a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ limitations under the License. > VidGear is a High-Performance **Video-Processing** Framework for building complex real-time media applications in python :fire: -VidGear provides an easy-to-use, highly extensible, **Multi-Threaded + Asyncio Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. +VidGear provides an easy-to-use, highly extensible, **[Multi-Threaded](../bonus/TQM/#threaded-queue-mode) + Asyncio Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. > _"Write Less and Accomplish More"_ — VidGear's Motto diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 2155bfcd5..5928c63bf 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -143,10 +143,10 @@ Installation is as simple as: **Or you can also download its wheel (`.whl`) package from our repository's [releases](https://github.com/abhiTronix/vidgear/releases) section, and thereby can be installed as follows:** ```sh - pip install vidgear-0.2.1-py3-none-any.whl + pip install vidgear-0.2.2-py3-none-any.whl # or with asyncio support - pip install vidgear-0.2.1-py3-none-any.whl[asyncio] + pip install vidgear-0.2.2-py3-none-any.whl[asyncio] ```   diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index f9781f3f0..bce2bc7c4 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -42,10 +42,10 @@ Switching OpenCV with VidGear APIs is usually a fairly painless process, and wil VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art features on top of it like: -- [x] Multi-Threaded Performance. +- [x] [Accerlated Multi-Threaded](../bonus/TQM/#c-accelerates-frame-processing) Performance. - [x] Real-time Stabilization. - [x] Inherit support for multiple sources. -- [x] Screen-casting, live network-streaming, plus [way much more ➶](../gears) +- [x] Screen-casting, Live network-streaming, [plus way much more ➶](../gears) Vidgear offers all this while maintaining the same standard OpenCV-Python _(Python API for OpenCV)_ coding syntax for all of its APIs, thereby making it even easier to implement Complex OpenCV applications in fewer lines of python code. From b8bf1e86c4df2e067d8d4deca79fee21b40197b4 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 28 May 2021 22:49:53 +0530 Subject: [PATCH 025/112] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Setup:=20Added?= =?UTF-8?q?=20patch=20for=20numpy=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - numpy recently dropped support for python 3.6.x legacies. See https://github.com/numpy/numpy/releases/tag/v1.20.0 --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 87354b544..8ab769757 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,9 @@ def latest_version(package_name): install_requires=[ "pafy{}".format(latest_version("pafy")), "mss{}".format(latest_version("mss")), - "numpy", + "numpy{}".format( + "<=1.19.5" if sys.version_info[:2] == (3, 6) else "" + ), # dropped support for 3.6.x legacies "youtube-dl{}".format(latest_version("youtube-dl")), "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), @@ -166,8 +168,8 @@ def latest_version(package_name): "Topic :: Multimedia :: Video", "Topic :: Scientific/Engineering", "Intended Audience :: Developers", - 'Intended Audience :: Science/Research', - 'Intended Audience :: Education', + "Intended Audience :: Science/Research", + "Intended Audience :: Education", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", From 59bba2b22ddbd918174a0f726af881a2d4394024 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 28 May 2021 22:56:09 +0530 Subject: [PATCH 026/112] =?UTF-8?q?=F0=9F=90=9B=20StreamGear:=20Replaced?= =?UTF-8?q?=20depreciated=20`-min=5Fseg=5Fduration`=20flag=20with=20`-seg?= =?UTF-8?q?=5Fduration`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/streamgear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index e3a8b1e9b..a7c64204d 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -703,8 +703,8 @@ def __generate_dash_stream(self, input_params, output_params): output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) else: # default behaviour - output_params["-min_seg_duration"] = self.__params.pop( - "-min_seg_duration", 5000000 + output_params["-seg_duration"] = self.__params.pop( + "-seg_duration", 5000000 ) # Finally, some hardcoded DASH parameters (Refer FFmpeg docs for more info.) From 0f2e93d529fc08f71f8a87605bde09af66f9bc63 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 28 May 2021 23:04:48 +0530 Subject: [PATCH 027/112] =?UTF-8?q?=F0=9F=9A=A9=20StreamGear:=20Removed=20?= =?UTF-8?q?redundant=20`-re`=20flag=20from=20Real-time=20Frames=20Mode.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/streamgear.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index a7c64204d..97c1e28a6 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -768,7 +768,6 @@ def __Build_n_Execute(self, input_params, output_params): else: ffmpeg_cmd = ( [self.__ffmpeg, "-y"] - + ["-re"] # pseudo live-streaming + hide_banner + ["-f", "rawvideo", "-vcodec", "rawvideo"] + input_commands From 168877cd37fe606ef8eed6a7b16efea54d6caeb8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 28 May 2021 23:10:48 +0530 Subject: [PATCH 028/112] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20Setup:=20Added?= =?UTF-8?q?=20patch=20for=20simplejpeg=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - simplejpeg recently dropped support for python 3.6.x legacies in v1.6.0. See https://gitlab.com/jfolz/simplejpeg/-/blob/master/CHANGELOG --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8ab769757..4c08ebaf2 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,9 @@ def latest_version(package_name): "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), "pyzmq{}".format(latest_version("pyzmq")), - "simplejpeg", + "simplejpeg{}".format( + "==1.5.0" if sys.version_info[:2] == (3, 6) else "" + ), # dropped support for 3.6.x legacies "colorlog", "colorama", "tqdm", From b960aa6cd369842e8db8355498634e94a6580cd8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 29 May 2021 07:32:37 +0530 Subject: [PATCH 029/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20StreamGear:=20Mult?= =?UTF-8?q?iple=20Performance=20Upgrades=20and=20Bugfix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Improved Live-Streaming performance by disabling SegmentTimline - Improved DASH assets detection for removal by using filename prefixes. - Helper added support for filename prefixes in `delete_ext_safe` method. - Added `-seg_duration` to control segment duration. - Fixed improper `-seg_duration` value resulting in broken pipeline. - Improved logging levelnames. --- vidgear/gears/helper.py | 18 +++++++++++++----- vidgear/gears/streamgear.py | 38 ++++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 4fbc4da99..cfd7acdcf 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -599,13 +599,21 @@ def delete_ext_safe(dir_path, extensions=[], logging=False): logger.warning("Invalid input provided for deleting!") return - if logging: - logger.debug("Clearing Assets at `{}`!".format(dir_path)) + logger.critical("Clearing Assets at `{}`!".format(dir_path)) for ext in extensions: - files_ext = [ - os.path.join(dir_path, f) for f in os.listdir(dir_path) if f.endswith(ext) - ] + if len(ext) == 2: + files_ext = [ + os.path.join(dir_path, f) + for f in os.listdir(dir_path) + if f.startswith(ext[0]) and f.endswith(ext[1]) + ] + else: + files_ext = [ + os.path.join(dir_path, f) + for f in os.listdir(dir_path) + if f.endswith(ext) + ] for file in files_ext: delete_file_safe(file) if logging: diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 97c1e28a6..717fbb3f9 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -245,9 +245,22 @@ def __init__( ): # check if given path is directory valid_extension = "mpd" if self.__format == "dash" else "m3u8" + # get all assets extensions + assets_exts = [ + ("chunk-stream", ".m4s"), # 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 + ) if os.path.isdir(abs_path): if self.__clear_assets: - delete_ext_safe(abs_path, [".m4s", ".mpd"], logging=self.__logging) + delete_ext_safe(abs_path, assets_exts, logging=self.__logging) abs_path = os.path.join( abs_path, "{}-{}.{}".format( @@ -259,7 +272,7 @@ def __init__( elif self.__clear_assets and os.path.isfile(abs_path): delete_ext_safe( os.path.dirname(abs_path), - [".m4s", ".mpd"], + assets_exts, logging=self.__logging, ) # check if path has valid file extension @@ -431,7 +444,7 @@ def __PreProcess(self, channels=0, rgb=False): output_parameters["a_bitrate"] = bitrate # temporary handler output_parameters["-core_audio"] = ["-map", "1:a:0"] else: - logger.critical( + logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) ) elif self.__video_source: @@ -606,7 +619,7 @@ def __evaluate_streams(self, streams, output_params, bpp): if self.__logging: logger.debug("All streams processed successfully!") else: - logger.critical("Invalid `-streams` values skipped!") + logger.warning("Invalid `-streams` values skipped!") return output_params @@ -701,17 +714,20 @@ def __generate_dash_stream(self, input_params, output_params): ) # 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) + # Disable (0) the use of a SegmentTimline inside a SegmentTemplate. + output_params["-use_timeline"] = 0 else: # default behaviour - output_params["-seg_duration"] = self.__params.pop( - "-seg_duration", 5000000 - ) + output_params["-seg_duration"] = self.__params.pop("-seg_duration", 5) + # Enable (1) the use of a SegmentTimline inside a SegmentTemplate. + output_params["-use_timeline"] = 1 # Finally, some hardcoded DASH parameters (Refer FFmpeg docs for more info.) - output_params["-use_timeline"] = 1 output_params["-use_template"] = 1 - output_params["-adaptation_sets"] = "id=0,streams=v{}".format( - " id=1,streams=a" if ("-acodec" in output_params) else "" + output_params["-adaptation_sets"] = "id=0,streams=v {}".format( + "id=1,streams=a" if ("-acodec" in output_params) else "" ) # enable dash formatting output_params["-f"] = "dash" @@ -856,7 +872,7 @@ def terminate(self): self.__process.wait() self.__process = None # log it - logger.critical( + logger.debug( "Transcoding Ended. {} Streaming assets are successfully generated at specified path.".format( self.__format.upper() ) From 0a53d5a81168d5ac9725ecb1ed1ab4f96655be01 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 29 May 2021 09:25:54 +0530 Subject: [PATCH 030/112] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20StreamGear:=20D?= =?UTF-8?q?ocs=20Refractoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved StreamGear primary modes to seperate sections for better readibility. - Implemented seperate overview and usage example pages. - Improped StreamGear docs context and simplified language. - Updated README and mkdocs with new changes. - Renamed `overview` page to `introduction`. - Fixed improper hyperlinks. - Added new FAQs. --- README.md | 10 +- docs/bonus/reference/streamgear.md | 2 +- docs/gears.md | 2 +- docs/gears/stabilizer/overview.md | 2 +- docs/gears/streamgear/introduction.md | 133 +++++ docs/gears/streamgear/overview.md | 61 +- docs/gears/streamgear/params.md | 14 +- docs/gears/streamgear/rtfm/overview.md | 58 ++ docs/gears/streamgear/rtfm/usage.md | 551 ++++++++++++++++++ docs/gears/streamgear/ssm/overview.md | 50 ++ docs/gears/streamgear/ssm/usage.md | 203 +++++++ docs/gears/streamgear/usage.md | 766 ------------------------- docs/help/streamgear_faqs.md | 28 +- docs/index.md | 2 +- mkdocs.yml | 9 +- 15 files changed, 1062 insertions(+), 829 deletions(-) create mode 100644 docs/gears/streamgear/introduction.md create mode 100644 docs/gears/streamgear/rtfm/overview.md create mode 100644 docs/gears/streamgear/rtfm/usage.md create mode 100644 docs/gears/streamgear/ssm/overview.md create mode 100644 docs/gears/streamgear/ssm/usage.md delete mode 100644 docs/gears/streamgear/usage.md diff --git a/README.md b/README.md index 37a9e57c6..49590fcba 100644 --- a/README.md +++ b/README.md @@ -446,9 +446,9 @@ SteamGear currently only supports [**MPEG-DASH**](https://www.encoding.com/mpeg- **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** - * **Single-Source Mode:** In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. This mode can be easily activated by assigning suitable video path as input to `-video_source` attribute, during StreamGear initialization. ***Learn more about this mode [here ➶][ss-mode-doc]*** + * **Single-Source Mode:** In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. ***Learn more about this mode [here ➶][ss-mode-doc]*** - * **Real-time Frames Mode:** When no valid input is received on `-video_source` attribute, StreamGear API activates this mode where it directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. 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 video-frames _(as opposed to a entire file)_ into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. 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: @@ -760,7 +760,7 @@ Internal URLs [cm-writegear-doc]:https://abhitronix.github.io/vidgear/latest/gears/writegear/compression/overview/ [ncm-writegear-doc]:https://abhitronix.github.io/vidgear/latest/gears/writegear/non_compression/overview/ [screengear-doc]:https://abhitronix.github.io/vidgear/latest/gears/screengear/overview/ -[streamgear-doc]:https://abhitronix.github.io/vidgear/latest/gears/streamgear/overview/ +[streamgear-doc]:https://abhitronix.github.io/vidgear/latest/gears/streamgear/introduction/ [writegear-doc]:https://abhitronix.github.io/vidgear/latest/gears/writegear/introduction/ [netgear-doc]:https://abhitronix.github.io/vidgear/latest/gears/netgear/overview/ [webgear-doc]:https://abhitronix.github.io/vidgear/latest/gears/webgear/overview/ @@ -780,8 +780,8 @@ Internal URLs [installation]:https://abhitronix.github.io/vidgear/latest/installation/ [gears]:https://abhitronix.github.io/vidgear/latest/gears [switch_from_cv]:https://abhitronix.github.io/vidgear/latest/switch_from_cv/ -[ss-mode-doc]: https://abhitronix.github.io/vidgear/latest/gears/streamgear/usage/#a-single-source-mode -[rtf-mode-doc]: https://abhitronix.github.io/vidgear/latest/gears/streamgear/usage/#b-real-time-frames-mode +[ss-mode-doc]: https://abhitronix.github.io/vidgear/latest/gears/streamgear/ssm/#overview +[rtf-mode-doc]: https://abhitronix.github.io/vidgear/latest/gears/streamgear/rtfm/#overview [webgear-cs]: https://abhitronix.github.io/vidgear/latest/gears/webgear/advanced/#using-webgear-with-a-custom-sourceopencv [webgear_rtc-cs]: https://abhitronix.github.io/vidgear/latest/gears/webgear_rtc/advanced/#using-webgear_rtc-with-a-custom-sourceopencv [webgear_rtc-mc]: https://abhitronix.github.io/vidgear/latest/gears/webgear_rtc/advanced/#using-webgear_rtc-as-real-time-broadcaster diff --git a/docs/bonus/reference/streamgear.md b/docs/bonus/reference/streamgear.md index b3fe9085e..6ea9ceea3 100644 --- a/docs/bonus/reference/streamgear.md +++ b/docs/bonus/reference/streamgear.md @@ -18,7 +18,7 @@ limitations under the License. =============================================== --> -!!! example "StreamGear API usage examples can be found [here ➶](../../../gears/streamgear/usage/)" +!!! example "StreamGear API usage examples for: [Single-Source Mode ➶](../../../gears/streamgear/ssm/usage/) and [Real-time Frames Mode ➶](../../../gears/streamgear/rtfm/usage/)" !!! info "StreamGear API parameters are explained [here ➶](../../../gears/streamgear/params/)" diff --git a/docs/gears.md b/docs/gears.md index 5437fe71d..740340e68 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -54,7 +54,7 @@ These Gears can be classified as follows: > **Basic Function:** Transcodes/Broadcasts files & [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames for streaming. -* [StreamGear](streamgear/overview/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. +* [StreamGear](streamgear/introduction/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. * **Asynchronous I/O Streaming Gear:** diff --git a/docs/gears/stabilizer/overview.md b/docs/gears/stabilizer/overview.md index d6fe2f3b7..acaeb824a 100644 --- a/docs/gears/stabilizer/overview.md +++ b/docs/gears/stabilizer/overview.md @@ -29,7 +29,7 @@ limitations under the License.

VidGear's Stabilizer in Action
(Video Credits
@SIGGRAPH2013)

-!!! info "This video is transcoded with [**StreamGear API**](../../streamgear/overview/) and hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" +!!! info "This video is transcoded with [**StreamGear API**](../../streamgear/introduction/) and hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md new file mode 100644 index 000000000..ff41d8b3e --- /dev/null +++ b/docs/gears/streamgear/introduction.md @@ -0,0 +1,133 @@ + + +# StreamGear API + + +
+ StreamGear Flow Diagram +
StreamGear API's generalized workflow
+
+ + +## Overview + +> StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH)_ in just few lines of python code. + +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. + +SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of fixed 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. + +SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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 only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. + +  + +!!! danger "Important" + + * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../ffmpeg_install/) for its installation. + + * :warning: StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + + * It is advised to enable logging _([`logging=True`](../params/#logging))_ on the first run for easily identifying any runtime errors. + +  + +## Mode of Operations + +StreamGear primarily operates in following independent modes for transcoding: + +- [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. + +- [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. + +  + +## Importing + +You can import StreamGear API in your program as follows: + +```python +from vidgear.gears import StreamGear +``` + +  + + +## Watch Demo + +Watch StreamGear transcoded MPEG-DASH Stream: + +
+
+
+
+
+
+
+

Powered by clappr & shaka-player

+ +!!! info "This video assets _(Manifest and segments)_ are hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" + +!!! quote "Video Credits: [**"Tears of Steel"** - Project Mango Teaser](https://mango.blender.org/download/)" + +  + +## Recommended Players + +=== "GUI Players" + - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. + - [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. + - [x] **[Parole](https://docs.xfce.org/apps/parole/start):** _(UNIX only)_ Parole is a modern simple media player based on the GStreamer framework for Unix and Unix-like operating systems. + +=== "Command-Line Players" + - [x] **[MP4Client](https://github.com/gpac/gpac/wiki/MP4Client-Intro):** [GPAC](https://gpac.wp.imt.fr/home/) provides a highly configurable multimedia player called MP4Client. GPAC itself is an open source multimedia framework developed for research and academic purposes, and used in many media production chains. + - [x] **[ffplay](https://ffmpeg.org/ffplay.html):** FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs. + +=== "Online Players" + !!! tip "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" + + - [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. + - [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. + - [x] **[MediaElementPlayer](https://github.com/mediaelement/mediaelement):** MediaElementPlayer is a complete HTML/CSS audio/video player. + +  + +## Parameters + + + +## References + + + + +## FAQs + + + +  \ No newline at end of file diff --git a/docs/gears/streamgear/overview.md b/docs/gears/streamgear/overview.md index 20fee4db7..ff41d8b3e 100644 --- a/docs/gears/streamgear/overview.md +++ b/docs/gears/streamgear/overview.md @@ -53,19 +53,25 @@ SteamGear currently only supports [**MPEG-DASH**](https://www.encoding.com/mpeg- ## Mode of Operations -StreamGear primarily works in two independent modes for transcoding which serves different purposes. These modes are as follows: +StreamGear primarily operates in following independent modes for transcoding: -### A. Single-Source Mode +- [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. -In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter, during StreamGear initialization. ***Learn more about this mode [here ➶](../usage/#a-single-source-mode)*** +- [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. -### B. Real-time Frames Mode +  + +## Importing -When no valid input is received on [`-video_source`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter, StreamGear API activates this mode where it directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. ***Learn more about this mode [here ➶](../usage/#b-real-time-frames-mode)*** +You can import StreamGear API in your program as follows: +```python +from vidgear.gears import StreamGear +```   + ## Watch Demo Watch StreamGear transcoded MPEG-DASH Stream: @@ -85,45 +91,26 @@ Watch StreamGear transcoded MPEG-DASH Stream:   -## Recommended Stream Players - -### GUI Players - -- [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. -- [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. -- [x] **[Parole](https://docs.xfce.org/apps/parole/start):** _(UNIX only)_ Parole is a modern simple media player based on the GStreamer framework for Unix and Unix-like operating systems. +## Recommended Players -### Command-Line Players +=== "GUI Players" + - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. + - [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. + - [x] **[Parole](https://docs.xfce.org/apps/parole/start):** _(UNIX only)_ Parole is a modern simple media player based on the GStreamer framework for Unix and Unix-like operating systems. -- [x] **[MP4Client](https://github.com/gpac/gpac/wiki/MP4Client-Intro):** [GPAC](https://gpac.wp.imt.fr/home/) provides a highly configurable multimedia player called MP4Client. GPAC itself is an open source multimedia framework developed for research and academic purposes, and used in many media production chains. -- [x] **[ffplay](https://ffmpeg.org/ffplay.html):** FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs. +=== "Command-Line Players" + - [x] **[MP4Client](https://github.com/gpac/gpac/wiki/MP4Client-Intro):** [GPAC](https://gpac.wp.imt.fr/home/) provides a highly configurable multimedia player called MP4Client. GPAC itself is an open source multimedia framework developed for research and academic purposes, and used in many media production chains. + - [x] **[ffplay](https://ffmpeg.org/ffplay.html):** FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs. -### Online Players +=== "Online Players" + !!! tip "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" -!!! tip "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" - -- [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. -- [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. -- [x] **[MediaElementPlayer](https://github.com/mediaelement/mediaelement):** MediaElementPlayer is a complete HTML/CSS audio/video player. + - [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. + - [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. + - [x] **[MediaElementPlayer](https://github.com/mediaelement/mediaelement):** MediaElementPlayer is a complete HTML/CSS audio/video player.   -## Importing - -You can import StreamGear API in your program as follows: - -```python -from vidgear.gears import StreamGear -``` - -  - -## Usage Examples - - - ## Parameters
diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 7b768a2a3..88da99d3c 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -151,7 +151,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 ➶](../usage/#a2-usage-with-additional-streams)" + !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-additional-streams)" ```python stream_params = @@ -164,9 +164,9 @@ 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**](../usage/#a-single-source-mode), 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 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 ➶](../usage/#a1-bare-minimum-usage)" + !!! tip "Usage example can be found [here ➶](../ssm/usage/#bare-minimum-usage)" * **Video Filename**: Valid path to Video file as follows: ```python @@ -187,7 +187,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St !!! failure "Make sure this audio-source is compatible with provided video -source, otherwise you encounter multiple errors, or even no output at all!" - !!! tip "Usage example can be found [here ➶](../usage/#a3-usage-with-custom-audio)" + !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-custom-audio)" * **Audio Filename**: Valid path to Audio file as follows: ```python @@ -215,7 +215,7 @@ StreamGear API provides some exclusive internal parameters to easily generate St * **`-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 ➶](../usage/#b3-bare-minimum-usage-with-controlled-input-framerate)" + !!! 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 @@ -265,7 +265,9 @@ StreamGear API provides some exclusive internal parameters to easily generate St   -* **`-clear_prev_assets`** _(bool)_: ***(optional)*** specify whether to force-delete any previous copies of StreamGear Assets _(i.e. Manifest files(.mpd) & streaming chunks(.m4s))_ 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)*** 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: + + !!! info "In Single-Source Mode, additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." ```python stream_params = {"-clear_prev_assets": True} # will delete all previous assets diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md new file mode 100644 index 000000000..ba228a35e --- /dev/null +++ b/docs/gears/streamgear/rtfm/overview.md @@ -0,0 +1,58 @@ + + +# StreamGear API: Real-time Frames Mode + + +
+ Real-time Frames Mode Flow Diagram +
Real-time Frames Mode generalized workflow
+
+ + +## Overview + +When no valid input is received on [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, StreamGear API activates this mode where it directly transcodes 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. + +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. + +This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. + + +!!! warning + + * 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. + + * Input framerate defaults to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined. + + +  + +## Usage Examples + + + +  \ No newline at end of file diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md new file mode 100644 index 000000000..338f8fa0c --- /dev/null +++ b/docs/gears/streamgear/rtfm/usage.md @@ -0,0 +1,551 @@ + + +# StreamGear API Usage Examples: Real-time Frames Mode + + +!!! warning "Important Information" + + * 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, when no additional streams are defined, ==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. + + +  + + +## Bare-Minimum Usage + +Following is the bare-minimum code you need to get started with StreamGear API in Real-time Frames Mode: + +!!! 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." + +```python +# 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 + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable 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: + +!!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." + +!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." + +!!! 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." + +```python +# 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`) +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", **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + +## 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: + +```python +# 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.terminate() +``` + +  + +## 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: + +!!! 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." + +```python +# 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} + +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + +## 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. The complete usage example is as follows: + +!!! tip "This just a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature/example will work in the similar manner." + +```python +# import required libraries +from vidgear.gears import StreamGear +import cv2 + +# Open suitable video stream, such as webcam on first index(i.e. 0) +stream = cv2.VideoCapture(0) + +# describe a suitable manifest-file location/name +streamer = StreamGear(output="dash_out.mpd") + +# loop over +while True: + + # read frames from stream + (grabbed, frame) = stream.read() + + # check for frame if not grabbed + if not grabbed: + break + + # {do something with the frame here} + # lets convert frame to gray for this example + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + + # send frame to streamer + streamer.stream(gray) + + # Show output window + cv2.imshow("Output Gray Frame", gray) + + # 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.release() + +# safely close streamer +streamer.terminate() +``` + +  + +## 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 _(More detailed information can be found [here ➶](../../params/#a-exclusive-parameters))_ in Real-time Frames Mode. The complete example is as follows: + +!!! 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. + +!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + +```python +# import required libraries +from vidgear.gears import CamGear +from vidgear.gears import StreamGear +import cv2 + +# Open suitable video stream, such as webcam on first index(i.e. 0) +stream = CamGear(source=0).start() + +# 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": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], +} + +# describe a suitable manifest-file location/name and assign params +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 + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + +## Usage with 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 need to input the path of your audio to this attribute as string value, and StreamGear API will automatically validate and map it 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 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." + +!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../../params/#a-exclusive-parameters)" + +```python +# 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() + +# add various streams, along with custom audio +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 + ], + "-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" +} + +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + +## Usage with Hardware Video-Encoder + + +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 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 "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.** + + 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: + + ```sh + ffmpeg -hide_banner -encoders | grep vaapi + + V..... h264_vaapi H.264/AVC (VAAPI) (codec h264) + V..... hevc_vaapi H.265/HEVC (VAAPI) (codec hevc) + V..... mjpeg_vaapi MJPEG (VAAPI) (codec mjpeg) + V..... mpeg2_vaapi MPEG-2 (VAAPI) (codec mpeg2video) + V..... vp8_vaapi VP8 (VAAPI) (codec vp8) + ``` + + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import StreamGear +import cv2 + +# Open suitable video stream, such as webcam on first index(i.e. 0) +stream = VideoGear(source=0).start() + +# add various streams with custom Video Encoder and optimizations +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 + ], + "-vcodec": "h264_vaapi", # define custom Video encoder + "-vaapi_device": "/dev/dri/renderD128", # define device location + "-vf": "format=nv12,hwupload", # define video pixformat +} + +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + +[^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 diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md new file mode 100644 index 000000000..1a7519744 --- /dev/null +++ b/docs/gears/streamgear/ssm/overview.md @@ -0,0 +1,50 @@ + + +# StreamGear API: Single-Source Mode + +
+ StreamGear Flow Diagram +
StreamGear API's generalized workflow
+
+ + +## Overview + +In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. + +This mode provide [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function to process audio-video files into streamable chunks. + +This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#stream_params) dictionary parameter, during StreamGear initialization. + +!!! warning + + * 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`**! + +  + +## Usage Examples + + + +  \ No newline at end of file diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md new file mode 100644 index 000000000..558ec4b2f --- /dev/null +++ b/docs/gears/streamgear/ssm/usage.md @@ -0,0 +1,203 @@ + + +# StreamGear API Usage Examples: Single-Source Mode + +!!! warning "Important Information" + + * 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, when no additional streams are defined, ==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. + + +  + +## Bare-Minimum Usage + +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." + +```python +# import required libraries +from vidgear.gears import StreamGear + +# activate Single-Source Mode with valid video input +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 +streamer.transcode_source() +# terminate +streamer.terminate() +``` + +!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable chunks that contains information about a Primary Stream of same resolution and framerate as the input." + +  + +## 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: + +!!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." + +!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." + +!!! 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." + +```python +# 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) +# trancode source +streamer.transcode_source() +# terminate +streamer.terminate() +``` + +  + +## 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 _(More detailed information can be found [here ➶](../../params/#a-exclusive-parameters))_. The complete example is as follows: + +!!! note "If input video-source contains any audio stream/channel, then it automatically gets assigned 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. + +!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + +```python +# import required libraries +from vidgear.gears import StreamGear + +# activate Single-Source Mode and also define various streams +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 framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], +} +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) +# trancode source +streamer.transcode_source() +# terminate +streamer.terminate() +``` + +  + +## Usage with Custom Audio + +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 StreamGear API will automatically validate and map it 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 encounter multiple errors or 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)" + +```python +# import required libraries +from vidgear.gears import StreamGear + +# activate Single-Source Mode and various streams, along with custom audio +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 + ], + "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" +} +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) +# trancode source +streamer.transcode_source() +# terminate +streamer.terminate() +``` + +  + + +## 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. Hence, 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 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: + + +!!! 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." + +!!! 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!" + + +```python +# 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 + "-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 + "-streams": [ + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-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` +} + +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) +# trancode source +streamer.transcode_source() +# terminate +streamer.terminate() +``` + +  + +[^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 diff --git a/docs/gears/streamgear/usage.md b/docs/gears/streamgear/usage.md deleted file mode 100644 index 81682b439..000000000 --- a/docs/gears/streamgear/usage.md +++ /dev/null @@ -1,766 +0,0 @@ - - -# StreamGear API Usage Examples: - - -!!! warning "Important Information" - - * 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, when no additional streams are defined, ==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. - - -  - -## A. Single-Source Mode - -
- Single-Source Mode Flow Diagram -
Single-Source Mode generalized workflow
-
- -In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. - -This mode provide [`transcode_source()`](../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function to process audio-video files into streamable chunks. - -This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../params/#a-exclusive-parameters) attribute of [`stream_params`](../params/#stream_params) dictionary parameter, during StreamGear initialization. - -!!! warning - - * 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`**! - -  - -### A.1 Bare-Minimum Usage - -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." - -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode with valid video input -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 -streamer.transcode_source() -# terminate -streamer.terminate() -``` - -!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable chunks that contains information about a Primary Stream of same resolution and framerate as the input." - -  - -### A.2 Bare-Minimum Usage with Live-Streaming - -If you want to **Livestream in Single-Source Mode** _(chunks will contain information for few new frames only, and forgets all previous ones)_, you can use exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: - -!!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." - -!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." - -!!! 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." - -```python -# 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) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` - -  - -### A.3 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 _(More detailed information can be found [here ➶](../params/#a-exclusive-parameters))_. The complete example is as follows: - -!!! note "If input video-source contains any audio stream/channel, then it automatically gets assigned 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. - -!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" - -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode and also define various streams -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 framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate - {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate - ], -} -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` - -  - -### A.4 Usage with Custom Audio - -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 StreamGear API will automatically validate and map it 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 encounter multiple errors or 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)" - -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode and various streams, along with custom audio -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 - ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" -} -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` - -  - - -### A.5 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. Hence, 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 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: - - -!!! 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." - -!!! 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!" - - -```python -# 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 - "-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 - "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-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` -} - -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` - -  - -  - -## B. Real-time Frames Mode - -
- Real-time Frames Mode Flow Diagram -
Real-time Frames Mode generalized workflow
-
- -When no valid input is received on [`-video_source`](../params/#a-exclusive-parameters) attribute of [`stream_params`](../params/#supported-parameters) dictionary parameter, StreamGear API activates this mode where it directly transcodes 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. - -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. - -This mode provide [`stream()`](../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. - - -!!! warning - - * 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. - - * Input framerate defaults to `25.0` fps if [`-input_framerate`](../params/#a-exclusive-parameters) attribute value not defined. - - - -  - -### B.1 Bare-Minimum Usage - -Following is the bare-minimum code you need to get started with StreamGear API in Real-time Frames Mode: - -!!! 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." - -```python -# 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 - - - # {do something with the frame here} - - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable chunks that contains information about a Primary Stream of same resolution and framerate[^1] as input _(without any audio)_." - - -  - -### B.2 Bare-Minimum Usage with Live-Streaming - -If you want to **Livestream in Real-time Frames Mode** _(chunks will contain information for few new frames only)_, which is excellent for building Low Latency solutions such as Live Camera Streaming, then you can use exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: - -!!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." - -!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." - -!!! 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." - -```python -# 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`) -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", **stream_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -  - -### B.3 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: - -```python -# 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.terminate() -``` - -  - -### B.4 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: - -!!! 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." - -```python -# 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} - -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {do something with the frame here} - - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -  - -### B.5 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. The complete usage example is as follows: - -!!! tip "This just a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature/example will work in the similar manner." - -```python -# import required libraries -from vidgear.gears import StreamGear -import cv2 - -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = cv2.VideoCapture(0) - -# describe a suitable manifest-file location/name -streamer = StreamGear(output="dash_out.mpd") - -# loop over -while True: - - # read frames from stream - (grabbed, frame) = stream.read() - - # check for frame if not grabbed - if not grabbed: - break - - # {do something with the frame here} - # lets convert frame to gray for this example - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - - - # send frame to streamer - streamer.stream(gray) - - # Show output window - cv2.imshow("Output Gray Frame", gray) - - # 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.release() - -# safely close streamer -streamer.terminate() -``` - -  - -### B.6 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 _(More detailed information can be found [here ➶](../params/#a-exclusive-parameters))_ in Real-time Frames Mode. The complete example is as follows: - -!!! 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. - -!!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" - -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import StreamGear -import cv2 - -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = CamGear(source=0).start() - -# 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": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate - ], -} - -# describe a suitable manifest-file location/name and assign params -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 - - - # {do something with the frame here} - - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -  - -### B.7 Usage with 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 need to input the path of your audio to this attribute as string value, and StreamGear API will automatically validate and map it 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 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." - -!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../params/#a-exclusive-parameters)" - -```python -# 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() - -# add various streams, along with custom audio -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 - ], - "-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" -} - -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {do something with the frame here} - - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -  - -### B.8 Usage with Hardware Video-Encoder - - -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 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 "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.** - - 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: - - ```sh - ffmpeg -hide_banner -encoders | grep vaapi - - V..... h264_vaapi H.264/AVC (VAAPI) (codec h264) - V..... hevc_vaapi H.265/HEVC (VAAPI) (codec hevc) - V..... mjpeg_vaapi MJPEG (VAAPI) (codec mjpeg) - V..... mpeg2_vaapi MPEG-2 (VAAPI) (codec mpeg2video) - V..... vp8_vaapi VP8 (VAAPI) (codec vp8) - ``` - - -```python -# import required libraries -from vidgear.gears import VideoGear -from vidgear.gears import StreamGear -import cv2 - -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = VideoGear(source=0).start() - -# add various streams with custom Video Encoder and optimizations -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 - ], - "-vcodec": "h264_vaapi", # define custom Video encoder - "-vaapi_device": "/dev/dri/renderD128", # define device location - "-vf": "format=nv12,hwupload", # define video pixformat -} - -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {do something with the frame here} - - - # send frame to streamer - streamer.stream(frame) - - # 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.terminate() -``` - -  - -[^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 diff --git a/docs/help/streamgear_faqs.md b/docs/help/streamgear_faqs.md index c49435926..c622f4eef 100644 --- a/docs/help/streamgear_faqs.md +++ b/docs/help/streamgear_faqs.md @@ -24,13 +24,13 @@ limitations under the License. ## What is StreamGear API and what does it do? -**Answer:** StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH)_ in just few lines of python code. _For more info. see [StreamGear doc ➶](../../gears/streamgear/overview/)_ +**Answer:** StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH)_ in just few lines of python code. _For more info. see [StreamGear doc ➶](../../gears/streamgear/introduction/)_   ## How to get started with StreamGear API? -**Answer:** See [StreamGear doc ➶](../../gears/streamgear/overview/). Still in doubt, then ask us on [Gitter ➶](https://gitter.im/vidgear/community) Community channel. +**Answer:** See [StreamGear doc ➶](../../gears/streamgear/introduction/). Still in doubt, then ask us on [Gitter ➶](https://gitter.im/vidgear/community) Community channel.   @@ -42,7 +42,7 @@ limitations under the License. ## How to play Streaming Assets created with StreamGear API? -**Answer:** You can easily feed Manifest file(`.mpd`) to DASH Supported Players Input but sure encoded chunks are present along with it. See this list of [recommended players ➶](../../gears/streamgear/overview/#recommended-stream-players) +**Answer:** You can easily feed Manifest file(`.mpd`) to DASH Supported Players Input but sure encoded chunks are present along with it. See this list of [recommended players ➶](../../gears/streamgear/introduction/#recommended-stream-players)   @@ -60,24 +60,34 @@ limitations under the License. ## How to create additional streams in StreamGear API? -**Answer:** [See this example ➶](../../gears/streamgear/usage/#a2-usage-with-additional-streams) +**Answer:** [See this example ➶](../../gears/streamgear/ssm/usage/#usage-with-additional-streams)   -## How to use StreamGear API with real-time frames? -**Answer:** See [Real-time Frames Mode ➶](../../gears/streamgear/usage/#b-real-time-frames-mode) +## How to use StreamGear API with OpenCV? + +**Answer:** [See this example ➶](../../gears/streamgear/rtfm/usage/bare-minimum-usage-with-opencv)   -## How to use StreamGear API with OpenCV? +## How to use StreamGear API with real-time frames? -**Answer:** [See this example ➶](../../gears/streamgear/usage/#b4-bare-minimum-usage-with-opencv) +**Answer:** See [Real-time Frames Mode ➶](../../gears/streamgear/rtfm/overview)   +## Is Real-time Frames Mode only used for Live-Streaming? + +**Answer:** Real-time Frame Modes and Live-Streaming are completely different terms and not directly related. + +- **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`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. + + ## How to use Hardware/GPU encoder for StreamGear trancoding? -**Answer:** [See this example ➶](../../gears/streamgear/usage/#b7-usage-with-hardware-video-encoder) +**Answer:** [See this example ➶](../../gears/streamgear/rtfm/usage/#usage-with-hardware-video-encoder)   \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 0c8908a2a..d835fb646 100644 --- a/docs/index.md +++ b/docs/index.md @@ -71,7 +71,7 @@ These Gears can be classified as follows: #### Streaming Gears -* [StreamGear](gears/streamgear/overview/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. +* [StreamGear](gears/streamgear/introduction/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. * **Asynchronous I/O Streaming Gear:** diff --git a/mkdocs.yml b/mkdocs.yml index 09500408d..b06e9dbe1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -208,8 +208,13 @@ nav: - References: bonus/reference/writegear.md - FAQs: help/writegear_faqs.md - StreamGear: - - Overview: gears/streamgear/overview.md - - Usage Examples: gears/streamgear/usage.md + - Introduction: gears/streamgear/introduction.md + - Single-Source Mode: + - Overview: gears/streamgear/ssm/overview.md + - Usage Examples: gears/streamgear/ssm/usage.md + - Real-time Frames Mode: + - Overview: gears/streamgear/rtfm/overview.md + - Usage Examples: gears/streamgear/rtfm/usage.md - Advanced: - FFmpeg Installation: gears/streamgear/ffmpeg_install.md - Parameters: gears/streamgear/params.md From 28f8acb6fcc20e8db540b0d2d49776b82a26b981 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 31 May 2021 09:58:24 +0530 Subject: [PATCH 031/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Several=20minor?= =?UTF-8?q?=20tweaks=20and=20typos=20fixed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/bonus/TQM.md | 12 +- docs/changelog.md | 775 +++++++++--------- docs/gears.md | 4 +- .../netgear/advanced/bidirectional_mode.md | 4 +- docs/gears/netgear/params.md | 2 +- docs/gears/stabilizer/overview.md | 2 +- docs/gears/streamgear/ffmpeg_install.md | 2 +- docs/gears/streamgear/overview.md | 133 --- docs/gears/streamgear/rtfm/usage.md | 2 +- docs/gears/streamgear/ssm/usage.md | 2 +- docs/gears/videogear/overview.md | 3 - docs/gears/videogear/usage.md | 3 + .../compression/advanced/ffmpeg_install.md | 2 +- docs/help/camgear_faqs.md | 20 +- docs/index.md | 2 +- docs/installation/pip_install.md | 2 +- docs/installation/source_install.md | 2 +- docs/overrides/assets/stylesheets/custom.css | 2 + mkdocs.yml | 3 +- setup.py | 8 +- 20 files changed, 430 insertions(+), 555 deletions(-) delete mode 100644 docs/gears/streamgear/overview.md diff --git a/docs/bonus/TQM.md b/docs/bonus/TQM.md index 7e06f1eb0..78957ad23 100644 --- a/docs/bonus/TQM.md +++ b/docs/bonus/TQM.md @@ -27,7 +27,7 @@ limitations under the License.
Threaded-Queue-Mode: generalized timing diagram
-> Threaded Queue Mode is designed exclusively for VidGear's Videocapture Gears _(namely CamGear, ScreenGear, VideoGear)_ and few Network Gears _(such as NetGear(Client's end))_ for achieving high-performance, synchronized, and error-free video-frames handling with their **Internal Threaded Frame Extractor Daemons**. +> Threaded Queue Mode is designed exclusively for VidGear's Videocapture Gears _(namely CamGear, ScreenGear, VideoGear)_ and few Network Gears _(such as NetGear(Client's end))_ for achieving high-performance, synchronized, and error-free video-frames handling. !!! tip "Threaded-Queue-Mode is enabled by default, but [can be disabled](#manually-disabling-threaded-queue-mode), only if extremely necessary." @@ -43,15 +43,15 @@ Threaded-Queue-Mode helps VidGear do the Threaded Video-Processing tasks in sync > In case you don't already know, OpenCV's' [`read()`](https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a473055e77dd7faa4d26d686226b292c1) is a [**Blocking I/O**](https://luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking) function for reading and decoding the next video-frame, and consumes much of the I/O bound memory depending upon our video source properties & system hardware. This essentially means, the corresponding thread that reads data from it, is continuously blocked from retrieving the next frame. As a result, our python program appears slow and sluggish even without any type of computationally expensive image processing operations. This problem is far more severe on low memory SBCs like Raspberry Pis. -In Threaded-Queue-Mode, VidGear creates several [**Python Threads**](https://docs.python.org/3/library/threading.html) within one process to offload the frame-decoding task to a different thread. Thereby, VidGear is able to execute different Video I/O-bounded operations at the same time by overlapping there waiting times. Moreover, threads are managed by operating system itself and is capable of distributing them between available CPU cores efficiently. In this way, Threaded-Queue-Mode keeps on processing frames faster in the [background](https://en.wikipedia.org/wiki/Daemon_(computing)) without waiting for blocked I/O operations and not affected by sluggishness in our main python thread. +In Threaded-Queue-Mode, VidGear creates several [**Python Threads**](https://docs.python.org/3/library/threading.html) within one process to offload the frame-decoding task to a different thread. Thereby, VidGear is able to execute different Video I/O-bounded operations at the same time by overlapping there waiting times. Moreover, threads are managed by operating system itself and is capable of distributing them between available CPU cores efficiently. In this way, Threaded-Queue-Mode keeps on processing frames faster in the [background](https://en.wikipedia.org/wiki/Daemon_(computing)) without waiting for blocked I/O operations or sluggishness in our main python program thread. ### B. Utilizes Fixed-Size Queues -> Although Multi-threading is fast, easy, and efficient, it can lead to some serious undesired effects like _frame-skipping, Global Interpreter Locks, race conditions, etc._ This is because there is no isolation whatsoever in python threads, if there is any crash, it may cause not only one particular thread to crash but the whole process to crash. That's not all, the biggest difficulty is that memory of the process where threads work is shared by different threads and that may result in frequent process crashes due to unwanted race conditions. +> Although Multi-threading is fast, easy, and efficient, it can lead to some serious undesired effects like _frame-skipping, GIL, race conditions, etc._ This is because there is no isolation whatsoever in python threads, if there is any crash, it may cause not only one particular thread to crash but the whole process to crash. That's not all, the biggest difficulty is that memory of the process where threads work is shared by different threads and that may result in frequent process crashes due to unwanted race conditions. -These problems are avoided in Threaded-Queue-Mode by utilizing **Thread-Safe, Memory-Efficient, and Fixed-Size [`Queues`](https://docs.python.org/3/library/queue.html#module-queue)** _(with approximately same O(1) performance in both directions)_, that single handedly monitors the synchronized access to frame-decoding thread and basically isolates it from other threads and thus prevents [**Global Interpreter Lock**](https://realpython.com/python-gil/). +These problems are avoided in Threaded-Queue-Mode by utilizing **Thread-Safe, Memory-Efficient, and Fixed-Size [`Queues`](https://docs.python.org/3/library/queue.html#module-queue)** _(with approximately same O(1) performance in both directions)_, that indpendently monitors the synchronized access to frame-decoding thread and isolates it from any other parallel threads which in turn prevents [**Global Interpreter Lock**](https://realpython.com/python-gil/). -### C. Accelerates frame processing +### C. Accelerates Frame Processing With queues, VidGear always maintains a fixed-length frames buffer in the memory and blocks the thread temporarily if the queue is full to avoid possible frame drops or otherwise pops out the frames synchronously without any obstructions. This significantly accelerates frame processing rate (and therefore our overall video processing pipeline) comes from dramatically reducing latency — since we don’t have to wait for the `read()` method to finish reading and decoding a frame; instead, there is always a pre-decoded frame ready for us to process. @@ -80,7 +80,7 @@ To manually disable Threaded-Queue-Mode, VidGear provides `THREADED_QUEUE_MODE` !!! warning "Important Warnings" - * Disabling Threaded-Queue-Mode will **NOT disables Multi-Threading.** + * Disabling Threaded-Queue-Mode does **NOT disables Multi-Threading.** * `THREADED_QUEUE_MODE` attribute does **NOT** work with Live feed, such as Camera Devices/Modules. diff --git a/docs/changelog.md b/docs/changelog.md index 50351f675..9194da776 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -24,62 +24,62 @@ limitations under the License. ??? tip "New Features" - [x] **NetGear:** - * [ ] New SSH Tunneling Mode for connecting ZMQ sockets across machines via SSH tunneling. - * [ ] Added new `ssh_tunnel_mode` attribute to enable ssh tunneling at provide address at server end only. - * [ ] Implemented new `check_open_port` helper method to validate availability of host at given open port. - * [ ] Added new attributes `ssh_tunnel_keyfile` and `ssh_tunnel_pwd` to easily validate ssh connection. - * [ ] Extended this feature to be compatible with bi-directional mode and auto-reconnection. - * [ ] Initially disabled support for exclusive Multi-Server and Multi-Clients modes. - * [ ] Implemented logic to automatically enable `paramiko` support if installed. - * [ ] Reserved port-47 for testing. + * [x] New SSH Tunneling Mode for connecting ZMQ sockets across machines via SSH tunneling. + * [x] Added new `ssh_tunnel_mode` attribute to enable ssh tunneling at provide address at server end only. + * [x] Implemented new `check_open_port` helper method to validate availability of host at given open port. + * [x] Added new attributes `ssh_tunnel_keyfile` and `ssh_tunnel_pwd` to easily validate ssh connection. + * [x] Extended this feature to be compatible with bi-directional mode and auto-reconnection. + * [x] Initially disabled support for exclusive Multi-Server and Multi-Clients modes. + * [x] Implemented logic to automatically enable `paramiko` support if installed. + * [x] Reserved port-47 for testing. - [x] **WebGear_RTC:** - * [ ] Added native support for middlewares. - * [ ] Added new global `middleware` variable for easily defining Middlewares as list. - * [ ] Added validity check for Middlewares. - * [ ] Added tests for middlewares support. - * [ ] Added example for middlewares support. - * [ ] Added related imports. + * [x] Added native support for middlewares. + * [x] Added new global `middleware` variable for easily defining Middlewares as list. + * [x] Added validity check for Middlewares. + * [x] Added tests for middlewares support. + * [x] Added example for middlewares support. + * [x] Added related imports. - [x] **CI:** - * [ ] Added new `no-response` work-flow for stale issues. - * [ ] Added NetGear CI Tests - * [ ] Added new CI tests for SSH Tunneling Mode. - * [ ] Added "paramiko" to CI dependencies. + * [x] Added new `no-response` work-flow for stale issues. + * [x] Added NetGear CI Tests + * [x] Added new CI tests for SSH Tunneling Mode. + * [x] Added "paramiko" to CI dependencies. - [x] **Docs:** - * [ ] Added Zenodo DOI badge and its reference in BibTex citations. - * [ ] Added `pymdownx.striphtml` plugin for stripping comments. - * [ ] Added complete docs for SSH Tunneling Mode. - * [ ] Added complete docs for NetGear's SSH Tunneling Mode. - * [ ] Added new usage example and related information. - * [ ] Added new image assets for ssh tunneling example. - * [ ] New admonitions and beautified css + * [x] Added Zenodo DOI badge and its reference in BibTex citations. + * [x] Added `pymdownx.striphtml` plugin for stripping comments. + * [x] Added complete docs for SSH Tunneling Mode. + * [x] Added complete docs for NetGear's SSH Tunneling Mode. + * [x] Added new usage example and related information. + * [x] Added new image assets for ssh tunneling example. + * [x] New admonitions and beautified css ??? success "Updates/Improvements" - [x] Added exception for RunTimeErrors in NetGear CI tests. - [x] Extended Middlewares support to WebGear API too. - [x] Docs: - * [ ] Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. - * [ ] Re-positioned few docs comments at bottom for easier detection during stripping. - * [ ] Updated dark theme to `dark orange`. - * [ ] Updated fonts to `Source Sans Pro`. - * [ ] Fixed missing heading in VideoGear. - * [ ] Update setup.py update link for assets. - * [ ] Added missing StreamGear Code docs. - * [ ] Several minor tweaks and typos fixed. - * [ ] Updated 404 page and workflow. - * [ ] Updated README.md and mkdocs.yml with new additions. - * [ ] Re-written Threaded-Queue-Mode from scratch with elaborated functioning. - * [ ] Replace Paypal with Liberpay in FUNDING.yml - * [ ] Updated FFmpeg Download links. - * [ ] Restructured docs. - * [ ] Updated mkdocs.yml. + * [x] Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. + * [x] Re-positioned few docs comments at bottom for easier detection during stripping. + * [x] Updated dark theme to `dark orange`. + * [x] Updated fonts to `Source Sans Pro`. + * [x] Fixed missing heading in VideoGear. + * [x] Update setup.py update link for assets. + * [x] Added missing StreamGear Code docs. + * [x] Several minor tweaks and typos fixed. + * [x] Updated 404 page and workflow. + * [x] Updated README.md and mkdocs.yml with new additions. + * [x] Re-written Threaded-Queue-Mode from scratch with elaborated functioning. + * [x] Replace Paypal with Liberpay in FUNDING.yml + * [x] Updated FFmpeg Download links. + * [x] Restructured docs. + * [x] Updated mkdocs.yml. - [x] Helper: - * [ ] Implemented new `delete_file_safe` to safely delete files at given path. - * [ ] Replaced `os.remove` calls with `delete_file_safe`. + * [x] Implemented new `delete_file_safe` to safely delete files at given path. + * [x] Replaced `os.remove` calls with `delete_file_safe`. - [x] CI: - * [ ] Updated VidGear Docs Deployer Workflow - * [ ] Updated test + * [x] Updated VidGear Docs Deployer Workflow + * [x] Updated test - [x] Updated issue templates and labels. ??? danger "Breaking Updates/Changes" @@ -88,17 +88,17 @@ limitations under the License. ??? bug "Bug-fixes" - [x] Critical Bugfix related to OpenCV Binaries import. - * [ ] Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. - * [ ] Replaced `packaging.parse_version` with more robust `distutils.version`. - * [ ] Removed redundant imports. + * [x] Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. + * [x] Replaced `packaging.parse_version` with more robust `distutils.version`. + * [x] Removed redundant imports. - [x] Setup: - * [ ] Removed `latest_version` variable support from `simplejpeg`. - * [ ] Fixed minor typos in dependencies. + * [x] Removed `latest_version` variable support from `simplejpeg`. + * [x] Fixed minor typos in dependencies. - [x] Setup_cfg: Replaced dashes with underscores to remove warnings. - [x] Docs: - * [ ] Fixed 404 page does not work outside the site root with mkdocs. - * [ ] Fixed markdown files comments not stripped when converted to HTML. - * [ ] Fixed typos + * [x] Fixed 404 page does not work outside the site root with mkdocs. + * [x] Fixed markdown files comments not stripped when converted to HTML. + * [x] Fixed typos ??? question "Pull Requests" @@ -115,42 +115,42 @@ limitations under the License. ??? tip "New Features" - [x] **WebGear_RTC:** - * [ ] A new API that is similar to WeGear API in all aspects but utilizes WebRTC standard instead of Motion JPEG for streaming. - * [ ] Now it is possible to share data and perform teleconferencing peer-to-peer, without requiring that the user install plugins or any other third-party software. - * [ ] Added a flexible backend for `aiortc` - a python library for Web Real-Time Communication (WebRTC). - * [ ] Integrated all functionality and parameters of WebGear into WebGear_RTC API. - * [ ] Implemented JSON Response with a WebRTC Peer Connection of Video Server. - * [ ] Added a internal `RTC_VideoServer` server on WebGear_RTC, a inherit-class to aiortc's VideoStreamTrack API. - * [ ] New Standalone UI Default theme v0.1.1 for WebGear_RTC from scratch without using 3rd-party assets. (by @abhiTronix) - * [ ] New `custom.js` and `custom.css` for custom responsive behavior. - * [ ] Added WebRTC support to `custom.js` and ensured compatibility with WebGear_RTC. - * [ ] Added example support for ICE framework and STUN protocol like WebRTC features to `custom.js`. - * [ ] Added `resize()` function to `custom.js` to automatically adjust `video` & `img` tags for smaller screens. - * [ ] Added WebGear_RTC support in main.py for easy access through terminal using `--mode` flag. - * [ ] Integrated all WebGear_RTC enhancements to WebGear Themes. - * [ ] Added CI test for WebGear_RTC. - * [ ] Added complete docs for WebGear_RTC API. - * [ ] Added bare-minimum as well as advanced examples usage code. - * [ ] Added new theme images. - * [ ] Added Reference and FAQs. + * [x] A new API that is similar to WeGear API in all aspects but utilizes WebRTC standard instead of Motion JPEG for streaming. + * [x] Now it is possible to share data and perform teleconferencing peer-to-peer, without requiring that the user install plugins or any other third-party software. + * [x] Added a flexible backend for `aiortc` - a python library for Web Real-Time Communication (WebRTC). + * [x] Integrated all functionality and parameters of WebGear into WebGear_RTC API. + * [x] Implemented JSON Response with a WebRTC Peer Connection of Video Server. + * [x] Added a internal `RTC_VideoServer` server on WebGear_RTC, a inherit-class to aiortc's VideoStreamTrack API. + * [x] New Standalone UI Default theme v0.1.1 for WebGear_RTC from scratch without using 3rd-party assets. (by @abhiTronix) + * [x] New `custom.js` and `custom.css` for custom responsive behavior. + * [x] Added WebRTC support to `custom.js` and ensured compatibility with WebGear_RTC. + * [x] Added example support for ICE framework and STUN protocol like WebRTC features to `custom.js`. + * [x] Added `resize()` function to `custom.js` to automatically adjust `video` & `img` tags for smaller screens. + * [x] Added WebGear_RTC support in main.py for easy access through terminal using `--mode` flag. + * [x] Integrated all WebGear_RTC enhancements to WebGear Themes. + * [x] Added CI test for WebGear_RTC. + * [x] Added complete docs for WebGear_RTC API. + * [x] Added bare-minimum as well as advanced examples usage code. + * [x] Added new theme images. + * [x] Added Reference and FAQs. - [x] **CamGear API:** - * [ ] New Improved Pure-Python Multiple-Threaded Implementation: + * [x] New Improved Pure-Python Multiple-Threaded Implementation: + Optimized Threaded-Queue-Mode Performance. (PR by @bml1g12) + Replaced regular `queue.full` checks followed by sleep with implicit sleep with blocking `queue.put`. + Replaced regular `queue.empty` checks followed by queue. + Replaced `nowait_get` with a blocking `queue.get` natural empty check. + Up-to 2x performance boost than previous implementations. - * [ ] New `THREAD_TIMEOUT` attribute to prevent deadlocks: + * [x] New `THREAD_TIMEOUT` attribute to prevent deadlocks: + Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. + Updated CI Tests and docs. - [x] **WriteGear API:** - * [ ] New more robust handling of default video-encoder in compression mode: + * [x] New more robust handling of default video-encoder in compression mode: + Implemented auto-switching of default video-encoder automatically based on availability. + API now selects Default encoder based on priority: `"libx264" > "libx265" > "libxvid" > "mpeg4"`. + Added `get_supported_vencoders` Helper method to enumerate Supported Video Encoders. + Added common handler for `-c:v` and `-vcodec` flags. - [x] **NetGear API:** - * [ ] New Turbo-JPEG compression with simplejpeg + * [x] New Turbo-JPEG compression with simplejpeg + Implemented JPEG compression algorithm for 4-5% performance boost at cost of minor loss in quality. + Utilized `encode_jpeg` and `decode_jpeg` methods to implement turbo-JPEG transcoding with `simplejpeg`. + Added options to control JPEG frames quality, enable fastest dct, fast upsampling to boost performance. @@ -158,13 +158,13 @@ limitations under the License. + Enabled fast dct by default with JPEG frames at 90%. + Added Docs for JPEG Frame Compression. - [x] **WebGear API:** - * [ ] New modular and flexible configuration for Custom Sources: + * [x] New modular and flexible configuration for Custom Sources: + Implemented more convenient approach for handling custom source configuration. + Added new `config` global variable for this new behavior. + Now None-type `source` parameter value is allowed for defining own custom sources. + Added new Example case and Updates Docs for this feature. + Added new CI Tests. - * [ ] New Browser UI Updates: + * [x] New Browser UI Updates: + New Standalone UI Default theme v0.1.0 for browser (by @abhiTronix) + Completely rewritten theme from scratch with only local resources. + New `custom.js` and `custom.css` for custom responsive behavior. @@ -173,144 +173,144 @@ limitations under the License. + Removed all third-party theme dependencies. + Update links to new github server `abhiTronix/vidgear-vitals` + Updated docs with new theme's screenshots. - * [ ] Added `enable_infinite_frames` attribute for enabling infinite frames. - * [ ] Added New modular and flexible configuration for Custom Sources. - * [ ] Bumped WebGear Theme Version to v0.1.1. - * [ ] Updated Docs and CI tests. + * [x] Added `enable_infinite_frames` attribute for enabling infinite frames. + * [x] Added New modular and flexible configuration for Custom Sources. + * [x] Bumped WebGear Theme Version to v0.1.1. + * [x] Updated Docs and CI tests. - [x] **ScreenGear API:** - * [ ] Implemented Improved Pure-Python Multiple-Threaded like CamGear. - * [ ] Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. + * [x] Implemented Improved Pure-Python Multiple-Threaded like CamGear. + * [x] Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. - [X] **StreamGear API:** - * [ ] Enabled pseudo live-streaming flag `re` for live content. + * [x] Enabled pseudo live-streaming flag `re` for live content. - [x] **Docs:** - * [ ] Added new native docs versioning to mkdocs-material. - * [ ] Added new examples and few visual tweaks. - * [ ] Updated Stylesheet for versioning. - * [ ] Added new DASH video chunks at https://github.com/abhiTronix/vidgear-docs-additionals for StreamGear and Stabilizer streams. - * [ ] Added open-sourced "Tears of Steel" * [ ] project Mango Teaser video chunks. - * [ ] Added open-sourced "Subspace Video Stabilization" http://web.cecs.pdx.edu/~fliu/project/subspace_stabilization/ video chunks. - * [ ] Added support for DASH Video Thumbnail preview in Clappr within `custom.js`. - * [ ] Added responsive clappr DASH player with bootstrap's `embed-responsive`. - * [ ] Added new permalink icon and slugify to toc. - * [ ] Added "back-to-top" button for easy navigation. + * [x] Added new native docs versioning to mkdocs-material. + * [x] Added new examples and few visual tweaks. + * [x] Updated Stylesheet for versioning. + * [x] Added new DASH video chunks at https://github.com/abhiTronix/vidgear-docs-additionals for StreamGear and Stabilizer streams. + * [x] Added open-sourced "Tears of Steel" * [x] project Mango Teaser video chunks. + * [x] Added open-sourced "Subspace Video Stabilization" http://web.cecs.pdx.edu/~fliu/project/subspace_stabilization/ video chunks. + * [x] Added support for DASH Video Thumbnail preview in Clappr within `custom.js`. + * [x] Added responsive clappr DASH player with bootstrap's `embed-responsive`. + * [x] Added new permalink icon and slugify to toc. + * [x] Added "back-to-top" button for easy navigation. - [x] **Helper:** - * [ ] New GitHub Mirror with latest Auto-built FFmpeg Static Binaries: + * [x] New GitHub Mirror with latest Auto-built FFmpeg Static Binaries: + Replaced new GitHub Mirror `abhiTronix/FFmpeg-Builds` in helper.py + New CI maintained Auto-built FFmpeg Static Binaries. + Removed all 3rd-party and old links for better compatibility and Open-Source reliability. + Updated Related CI tests. - - Added auto-font-scaling for `create_blank_frame` method. - * [ ] Added `c_name` parameter to `generate_webdata` and `download_webdata` to specify class. - * [ ] A more robust Implementation of Downloading Artifacts: - * [ ] Added a custom HTTP `TimeoutHTTPAdapter` Adapter with a default timeout for all HTTP calls based on [this GitHub comment](). - * [ ] Implemented http client and the `send()` method to ensure that the default timeout is used if a timeout argument isn't provided. - * [ ] Implemented Requests session`with` block to exit properly even if there are unhandled exceptions. - * [ ] Add a retry strategy to custom `TimeoutHTTPAdapter` Adapter with max 3 retries and sleep(`backoff_factor=1`) between failed requests. - * [ ] Added `create_blank_frame` method to create bland frames with suitable text. + * [x] Added auto-font-scaling for `create_blank_frame` method. + * [x] Added `c_name` parameter to `generate_webdata` and `download_webdata` to specify class. + * [x] A more robust Implementation of Downloading Artifacts: + + Added a custom HTTP `TimeoutHTTPAdapter` Adapter with a default timeout for all HTTP calls based on [this GitHub comment](). + + Implemented http client and the `send()` method to ensure that the default timeout is used if a timeout argument isn't provided. + + Implemented Requests session`with` block to exit properly even if there are unhandled exceptions. + + Add a retry strategy to custom `TimeoutHTTPAdapter` Adapter with max 3 retries and sleep(`backoff_factor=1`) between failed requests. + * [x] Added `create_blank_frame` method to create bland frames with suitable text. - [x] **[CI] Continuous Integration:** - * [ ] Added new fake frame generated for fake `picamera` class with numpy. - * [ ] Added new `create_bug` parameter to fake `picamera` class for emulating various artificial bugs. - * [ ] Added float/int instance check on `time_delay` for camgear and pigear. - * [ ] Added `EXIT_CODE` to new timeout implementation for pytests to upload codecov report when no timeout. - * [ ] Added auxiliary classes to fake `picamera` for facilitating the emulation. - * [ ] Added new CI tests for PiGear Class for testing on all platforms. - * [ ] Added `shutdown()` function to gracefully terminate WebGear_RTC API. - * [ ] Added new `coreutils` brew dependency. - * [ ] Added handler for variable check on exit and codecov upload. - * [ ] Added `is_running` flag to WebGear_RTC to exit safely. + * [x] Added new fake frame generated for fake `picamera` class with numpy. + * [x] Added new `create_bug` parameter to fake `picamera` class for emulating various artificial bugs. + * [x] Added float/int instance check on `time_delay` for camgear and pigear. + * [x] Added `EXIT_CODE` to new timeout implementation for pytests to upload codecov report when no timeout. + * [x] Added auxiliary classes to fake `picamera` for facilitating the emulation. + * [x] Added new CI tests for PiGear Class for testing on all platforms. + * [x] Added `shutdown()` function to gracefully terminate WebGear_RTC API. + * [x] Added new `coreutils` brew dependency. + * [x] Added handler for variable check on exit and codecov upload. + * [x] Added `is_running` flag to WebGear_RTC to exit safely. - [x] **Setup:** - * [ ] New automated latest version retriever for packages: + * [x] New automated latest version retriever for packages: + Implemented new `latest_version` method to automatically retrieve latest version for packages. + Added Some Dependencies. - * [ ] Added `simplejpeg` package for all platforms. + * [x] Added `simplejpeg` package for all platforms. ??? success "Updates/Improvements" - [x] Added exception for RunTimeErrors in NetGear CI tests. - [x] WriteGear: Critical file write access checking method: - * [ ] Added new `check_WriteAccess` Helper method. - * [ ] Implemented a new robust algorithm to check if given directory has write-access. - * [ ] Removed old behavior which gives irregular results. + * [x] Added new `check_WriteAccess` Helper method. + * [x] Implemented a new robust algorithm to check if given directory has write-access. + * [x] Removed old behavior which gives irregular results. - [x] Helper: Maintenance Updates - * [ ] Added workaround for Python bug. - * [ ] Added `safe_mkdir` to `check_WriteAccess` to automatically create non-existential parent folder in path. - * [ ] Extended `check_WriteAccess` Patch to StreamGear. - * [ ] Simplified `check_WriteAccess` to handle Windows envs easily. - * [ ] Updated FFmpeg Static Download URL for WriteGear. - * [ ] Implemented fallback option for auto-calculating bitrate from extracted audio sample-rate in `validate_audio` method. + * [x] Added workaround for Python bug. + * [x] Added `safe_mkdir` to `check_WriteAccess` to automatically create non-existential parent folder in path. + * [x] Extended `check_WriteAccess` Patch to StreamGear. + * [x] Simplified `check_WriteAccess` to handle Windows envs easily. + * [x] Updated FFmpeg Static Download URL for WriteGear. + * [x] Implemented fallback option for auto-calculating bitrate from extracted audio sample-rate in `validate_audio` method. - [x] Docs: General UI Updates - * [ ] Updated Meta tags for og site and twitter cards. - * [ ] Replaced Custom dark theme toggle with mkdocs-material's official Color palette toggle - * [ ] Added example for external audio input and creating segmented MP4 video in WriteGear FAQ. - * [ ] Added example for YouTube streaming with WriteGear. - * [ ] Removed custom `dark-material.js` and `header.html` files from theme. - * [ ] Added blogpost link for detailed information on Stabilizer Working. - * [ ] Updated `mkdocs.yml` and `custom.css` configuration. - * [ ] Remove old hack to resize clappr DASH player with css. - * [ ] Updated Admonitions. - * [ ] Improved docs contexts. - * [ ] Updated CSS for version-selector-button. - * [ ] Adjusted files to match new themes. - * [ ] Updated welcome-bot message for typos. - * [ ] Removed redundant FAQs from NetGear Docs. - * [ ] Updated Assets Images. - * [ ] Updated spacing. + * [x] Updated Meta tags for og site and twitter cards. + * [x] Replaced Custom dark theme toggle with mkdocs-material's official Color palette toggle + * [x] Added example for external audio input and creating segmented MP4 video in WriteGear FAQ. + * [x] Added example for YouTube streaming with WriteGear. + * [x] Removed custom `dark-material.js` and `header.html` files from theme. + * [x] Added blogpost link for detailed information on Stabilizer Working. + * [x] Updated `mkdocs.yml` and `custom.css` configuration. + * [x] Remove old hack to resize clappr DASH player with css. + * [x] Updated Admonitions. + * [x] Improved docs contexts. + * [x] Updated CSS for version-selector-button. + * [x] Adjusted files to match new themes. + * [x] Updated welcome-bot message for typos. + * [x] Removed redundant FAQs from NetGear Docs. + * [x] Updated Assets Images. + * [x] Updated spacing. - [x] CI: - * [ ] Removed unused `github.ref` from yaml. - * [ ] Updated OpenCV Bash Script for Linux envs. - * [ ] Added `timeout-minutes` flag to github-actions workflow. - * [ ] Added `timeout` flag to pytest. - * [ ] Replaced Threaded Gears with OpenCV VideoCapture API. - * [ ] Moved files and Removed redundant code. - * [ ] Replaced grayscale frames with color frames for WebGear tests. - * [ ] Updated pytest timeout value to 15mins. - * [ ] Removed `aiortc` automated install on Windows platform within setup.py. - * [ ] Added new timeout logic to continue to run on external timeout for GitHub Actions Workflows. - * [ ] Removed unreliable old timeout solution from WebGear_RTC. - * [ ] Removed `timeout_decorator` and `asyncio_timeout` dependencies for CI. - * [ ] Removed WebGear_RTC API exception from codecov. - * [ ] Implemented new fake `picamera` class to CI utils for emulating RPi Camera-Module Real-time capabilities. - * [ ] Implemented new `get_RTCPeer_payload` method to receive WebGear_RTC peer payload. - * [ ] Removed PiGear from Codecov exceptions. - * [ ] Disable Frame Compression in few NetGear tests failing on frame matching. - * [ ] Updated NetGear CI tests to support new attributes - * [ ] Removed warnings and updated yaml - * [ ] Added `pytest.ini` to address multiple warnings. - * [ ] Updated azure workflow condition syntax. - * [ ] Update `mike` settings for mkdocs versioning. - * [ ] Updated codecov configurations. - * [ ] Minor logging and docs updates. - * [ ] Implemented pytest timeout for azure pipelines for macOS envs. - * [ ] Added `aiortc` as external dependency in `appveyor.yml`. - * [ ] Re-implemented WebGear_RTC improper offer-answer handshake in CI tests. - * [ ] WebGear_RTC CI Updated with `VideoTransformTrack` to test stream play. - * [ ] Implemented fake `AttributeError` for fake picamera class. - * [ ] Updated PiGear CI tests to increment codecov. - * [ ] Update Tests docs and other minor tweaks to increase overall coverage. - * [ ] Enabled debugging and disabled exit 1 on error in azure pipeline. - * [ ] Removed redundant benchmark tests. + * [x] Removed unused `github.ref` from yaml. + * [x] Updated OpenCV Bash Script for Linux envs. + * [x] Added `timeout-minutes` flag to github-actions workflow. + * [x] Added `timeout` flag to pytest. + * [x] Replaced Threaded Gears with OpenCV VideoCapture API. + * [x] Moved files and Removed redundant code. + * [x] Replaced grayscale frames with color frames for WebGear tests. + * [x] Updated pytest timeout value to 15mins. + * [x] Removed `aiortc` automated install on Windows platform within setup.py. + * [x] Added new timeout logic to continue to run on external timeout for GitHub Actions Workflows. + * [x] Removed unreliable old timeout solution from WebGear_RTC. + * [x] Removed `timeout_decorator` and `asyncio_timeout` dependencies for CI. + * [x] Removed WebGear_RTC API exception from codecov. + * [x] Implemented new fake `picamera` class to CI utils for emulating RPi Camera-Module Real-time capabilities. + * [x] Implemented new `get_RTCPeer_payload` method to receive WebGear_RTC peer payload. + * [x] Removed PiGear from Codecov exceptions. + * [x] Disable Frame Compression in few NetGear tests failing on frame matching. + * [x] Updated NetGear CI tests to support new attributes + * [x] Removed warnings and updated yaml + + Added `pytest.ini` to address multiple warnings. + + Updated azure workflow condition syntax. + * [x] Update `mike` settings for mkdocs versioning. + * [x] Updated codecov configurations. + * [x] Minor logging and docs updates. + * [x] Implemented pytest timeout for azure pipelines for macOS envs. + * [x] Added `aiortc` as external dependency in `appveyor.yml`. + * [x] Re-implemented WebGear_RTC improper offer-answer handshake in CI tests. + * [x] WebGear_RTC CI Updated with `VideoTransformTrack` to test stream play. + * [x] Implemented fake `AttributeError` for fake picamera class. + * [x] Updated PiGear CI tests to increment codecov. + * [x] Update Tests docs and other minor tweaks to increase overall coverage. + * [x] Enabled debugging and disabled exit 1 on error in azure pipeline. + * [x] Removed redundant benchmark tests. - [x] Helper: Added missing RSTP URL scheme to `is_valid_url` method. - [x] NetGear_Async: Added fix for uvloop only supporting python>=3.7 legacies. - [x] Extended WebGear's Video-Handler scope to `https`. - [x] CI: Remove all redundant 32-bit Tests from Appveyor: - * [ ] Appveyor 32-bit Windows envs are actually running on 64-bit machines. - * [ ] More information here: https://help.appveyor.com/discussions/questions/20637-is-it-possible-to-force-running-tests-on-both-32-bit-and-64-bit-windows + * [x] Appveyor 32-bit Windows envs are actually running on 64-bit machines. + * [x] More information here: https://help.appveyor.com/discussions/questions/20637-is-it-possible-to-force-running-tests-on-both-32-bit-and-64-bit-windows - [x] Setup: Removed `latest_version` behavior from some packages. - [x] NetGear_Async: Revised logic for handling uvloop for all platforms and legacies. - [x] Setup: Updated logic to install uvloop-"v0.14.0" for python-3.6 legacies. - [x] Removed any redundant code from webgear. - [x] StreamGear: - * [ ] Replaced Ordinary dict with Ordered Dict to use `move_to_end` method. - * [ ] Moved external audio input to output parameters dict. - * [ ] Added additional imports. - * [ ] Updated docs to reflect changes. + * [x] Replaced Ordinary dict with Ordered Dict to use `move_to_end` method. + * [x] Moved external audio input to output parameters dict. + * [x] Added additional imports. + * [x] Updated docs to reflect changes. - [x] Numerous Updates to Readme and `mkdocs.yml`. - [x] Updated font to `FONT_HERSHEY_SCRIPT_COMPLEX` and enabled logging in create_blank_frame. - [x] Separated channels for downloading and storing theme files for WebGear and WebGear_RTC APIs. - [x] Removed `logging` condition to always inform user in a event of FFmpeg binary download failure. - [x] WebGear_RTC: - * [ ] Improved auto internal termination. - * [ ] More Performance updates through `setCodecPreferences`. - * [ ] Moved default Video RTC video launcher to `__offer`. + * [x] Improved auto internal termination. + * [x] More Performance updates through `setCodecPreferences`. + * [x] Moved default Video RTC video launcher to `__offer`. - [x] NetGear_Async: Added timeout to client in CI tests. - [x] Reimplemented and updated `changelog.md`. - [x] Updated code comments. @@ -330,37 +330,37 @@ limitations under the License. - [x] NetGear_Async: Fixed `source` parameter missing `None` as default value. - [x] Fixed uvloops only supporting python>=3.7 in NetGear_Async. - [x] Helper: - * [ ] Fixed Zombie processes in `check_output` method due a hidden bug in python. For reference: https://bugs.python.org/issue37380 - * [ ] Fixed regex in `validate_video` method. + * [x] Fixed Zombie processes in `check_output` method due a hidden bug in python. For reference: https://bugs.python.org/issue37380 + * [x] Fixed regex in `validate_video` method. - [x] Docs: - * [ ] Invalid `site_url` bug patched in mkdocs.yml - * [ ] Remove redundant mike theme support and its files. - * [ ] Fixed video not centered when DASH video in fullscreen mode with clappr. - * [ ] Fixed Incompatible new mkdocs-docs theme. - * [ ] Fixed missing hyperlinks. + * [x] Invalid `site_url` bug patched in mkdocs.yml + * [x] Remove redundant mike theme support and its files. + * [x] Fixed video not centered when DASH video in fullscreen mode with clappr. + * [x] Fixed Incompatible new mkdocs-docs theme. + * [x] Fixed missing hyperlinks. - [x] CI: - * [ ] Fixed NetGear Address bug - * [ ] Fixed bugs related to termination in WebGear_RTC. - * [ ] Fixed random CI test failures and code cleanup. - * [ ] Fixed string formating bug in Helper.py. - * [ ] Fixed F821 undefined name bugs in WebGear_RTC tests. - * [ ] NetGear_Async Tests fixes. - * [ ] Fixed F821 undefined name bugs. - * [ ] Fixed typo bugs in `main.py`. - * [ ] Fixed Relative import bug in PiGear. - * [ ] Fixed regex bug in warning filter. - * [ ] Fixed WebGear_RTC frozen threads on exit. - * [ ] Fixed bugs in codecov bash uploader setting for azure pipelines. - * [ ] Fixed False-positive `picamera` import due to improper sys.module settings. - * [ ] Fixed Frozen Threads on exit in WebGear_RTC API. - * [ ] Fixed deploy error in `VidGear Docs Deployer` workflow - * [ ] Fixed low timeout bug. - * [ ] Fixed bugs in PiGear tests. - * [ ] Patched F821 undefined name bug. + * [x] Fixed NetGear Address bug + * [x] Fixed bugs related to termination in WebGear_RTC. + * [x] Fixed random CI test failures and code cleanup. + * [x] Fixed string formating bug in Helper.py. + * [x] Fixed F821 undefined name bugs in WebGear_RTC tests. + * [x] NetGear_Async Tests fixes. + * [x] Fixed F821 undefined name bugs. + * [x] Fixed typo bugs in `main.py`. + * [x] Fixed Relative import bug in PiGear. + * [x] Fixed regex bug in warning filter. + * [x] Fixed WebGear_RTC frozen threads on exit. + * [x] Fixed bugs in codecov bash uploader setting for azure pipelines. + * [x] Fixed False-positive `picamera` import due to improper sys.module settings. + * [x] Fixed Frozen Threads on exit in WebGear_RTC API. + * [x] Fixed deploy error in `VidGear Docs Deployer` workflow + * [x] Fixed low timeout bug. + * [x] Fixed bugs in PiGear tests. + * [x] Patched F821 undefined name bug. - [x] StreamGear: - * [ ] Fixed StreamGear throwing `Picture size 0x0 is invalid` bug with external audio. - * [ ] Fixed default input framerate value getting discarded in Real-time Frame Mode. - * [ ] Fixed internal list-formatting bug. + * [x] Fixed StreamGear throwing `Picture size 0x0 is invalid` bug with external audio. + * [x] Fixed default input framerate value getting discarded in Real-time Frame Mode. + * [x] Fixed internal list-formatting bug. - [x] Fixed E999 SyntaxError bug in `main.py`. - [x] Fixed Typo in bash script. - [x] Fixed WebGear freeze on reloading bug. @@ -389,20 +389,20 @@ limitations under the License. ??? tip "New Features" - [x] **CamGear API:** - * [ ] Support for various Live-Video-Streaming services: + * [x] Support for various Live-Video-Streaming services: + Added seamless support for live video streaming sites like Twitch, LiveStream, Dailymotion etc. + Implemented flexible framework around `streamlink` python library with easy control over parameters and quality. + Stream Mode can now automatically detects whether `source` belong to YouTube or elsewhere, and handles it with appropriate API. - * [ ] Re-implemented YouTube URLs Handler: + * [x] Re-implemented YouTube URLs Handler: + Re-implemented CamGear's YouTube URLs Handler completely from scratch. + New Robust Logic to flexibly handing video and video-audio streams. + Intelligent stream selector for selecting best possible stream compatible with OpenCV. + Added support for selecting stream qualities and parameters. + Implemented new `get_supported_quality` helper method for handling specified qualities + Fixed Live-Stream URLs not supported by OpenCV's Videocapture and its FFmpeg. - * [ ] Added additional `STREAM_QUALITY` and `STREAM_PARAMS` attributes. + * [x] Added additional `STREAM_QUALITY` and `STREAM_PARAMS` attributes. - [x] **ScreenGear API:** - * [ ] Multiple Backends Support: + * [x] Multiple Backends Support: + Added new multiple backend support with new [`pyscreenshot`](https://github.com/ponty/pyscreenshot) python library. + Made `pyscreenshot` the default API for ScreenGear, replaces `mss`. + Added new `backend` parameter for this feature while retaining previous behavior. @@ -413,90 +413,90 @@ limitations under the License. + Updated ScreenGear Docs. + Updated ScreenGear CI tests. - [X] **StreamGear API:** - * [ ] Changed default behaviour to support complete video transcoding. - * [ ] Added `-livestream` attribute to support live-streaming. - * [ ] Added additional parameters for `-livestream` attribute functionality. - * [ ] Updated StreamGear Tests. - * [ ] Updated StreamGear docs. + * [x] Changed default behaviour to support complete video transcoding. + * [x] Added `-livestream` attribute to support live-streaming. + * [x] Added additional parameters for `-livestream` attribute functionality. + * [x] Updated StreamGear Tests. + * [x] Updated StreamGear docs. - [x] **Stabilizer Class:** - * [ ] New Robust Error Handling with Blank Frames: + * [x] New Robust Error Handling with Blank Frames: + Elegantly handles all crashes due to Empty/Blank/Dark frames. + Stabilizer throws Warning with this new behavior instead of crashing. + Updated CI test for this feature. - [x] **Docs:** - * [ ] Automated Docs Versioning: + * [x] Automated Docs Versioning: + Implemented Docs versioning through `mike` API. + Separate new workflow steps to handle different versions. + Updated docs deploy worflow to support `release` and `dev` builds. + Added automatic version extraction from github events. + Added `version-select.js` and `version-select.css` files. - * [ ] Toggleable Dark-White Docs Support: + * [x] Toggleable Dark-White Docs Support: + Toggle-button to easily switch dark, white and preferred theme. + New Updated Assets for dark backgrounds + New css, js files/content to implement this behavior. + New material icons for button. + Updated scheme to `slate` in `mkdocs.yml`. - * [ ] New Theme and assets: + * [x] New Theme and assets: + New `purple` theme with `dark-purple` accent color. + New images assets with updated transparent background. + Support for both dark and white theme. + Increased `rebufferingGoal` for dash videos. + New updated custom 404 page for docs. - * [ ] Issue and PR automated-bots changes + * [x] Issue and PR automated-bots changes + New `need_info.yml` YAML Workflow. + New `needs-more-info.yml` Request-Info template. + Replaced Request-Info templates. + Improved PR and Issue welcome formatting. - * [ ] Added custom HTML pages. - * [ ] Added `show_root_heading` flag to disable headings in References. - * [ ] Added new `inserAfter` function to version-select.js. - * [ ] Adjusted hue for dark-theme for better contrast. - * [ ] New usage examples and FAQs. - * [ ] Added `gitmoji` for commits. + * [x] Added custom HTML pages. + * [x] Added `show_root_heading` flag to disable headings in References. + * [x] Added new `inserAfter` function to version-select.js. + * [x] Adjusted hue for dark-theme for better contrast. + * [x] New usage examples and FAQs. + * [x] Added `gitmoji` for commits. - [x] **Continuous Integration:** - * [ ] Maintenance Updates: + * [x] Maintenance Updates: + Added support for new `VIDGEAR_LOGFILE` environment variable in Travis CI. + Added missing CI tests. + Added logging for helper functions. - * [ ] Azure-Pipeline workflow for MacOS envs + * [x] Azure-Pipeline workflow for MacOS envs + Added Azure-Pipeline Workflow for testing MacOS environment. + Added codecov support. - * [ ] GitHub Actions workflow for Linux envs + * [x] GitHub Actions workflow for Linux envs + Added GitHub Action work-flow for testing Linux environment. - * [ ] New YAML to implement GitHub Action workflow for python 3.6, 3.7, 3,8 & 3.9 matrices. - * [ ] Added Upload coverage to Codecov GitHub Action workflow. - * [ ] New codecov-bash uploader for Azure Pipelines. + * [x] New YAML to implement GitHub Action workflow for python 3.6, 3.7, 3,8 & 3.9 matrices. + * [x] Added Upload coverage to Codecov GitHub Action workflow. + * [x] New codecov-bash uploader for Azure Pipelines. - [x] **Logging:** - * [ ] Added file support + * [x] Added file support + Added `VIDGEAR_LOGFILE` environment variable to manually add file/dir path. + Reworked `logger_handler()` Helper methods (in asyncio too). + Added new formatter and Filehandler for handling logger files. - * [ ] Added `restore_levelnames` auxiliary method for restoring logging levelnames. + * [x] Added `restore_levelnames` auxiliary method for restoring logging levelnames. - [x] Added auto version extraction from package `version.py` in setup.py. ??? success "Updates/Improvements" - [x] Added missing Lazy-pirate auto-reconnection support for Multi-Servers and Multi-Clients Mode in NetGear API. - [x] Added new FFmpeg test path to Bash-Script and updated README broken links. - [x] Asset Cleanup: - * [ ] Removed all third-party javascripts from projects. - * [ ] Linked all third-party javascript directly. - * [ ] Cleaned up necessary code from CSS and JS files. - * [ ] Removed any copyrighted material or links. + * [x] Removed all third-party javascripts from projects. + * [x] Linked all third-party javascript directly. + * [x] Cleaned up necessary code from CSS and JS files. + * [x] Removed any copyrighted material or links. - [x] Rewritten Docs from scratch: - * [ ] Improved complete docs formatting. - * [ ] Simplified language for easier understanding. - * [ ] Fixed `mkdocstrings` showing root headings. - * [ ] Included all APIs methods to `mkdocstrings` docs. - * [ ] Removed unnecessary information from docs. - * [ ] Corrected Spelling and typos. - * [ ] Fixed context and grammar. - * [ ] Removed `motivation.md`. - * [ ] Renamed many terms. - * [ ] Fixed hyper-links. - * [ ] Reformatted missing or improper information. - * [ ] Fixed context and spellings in Docs files. - * [ ] Simplified language for easy understanding. - * [ ] Updated image sizes for better visibility. + * [x] Improved complete docs formatting. + * [x] Simplified language for easier understanding. + * [x] Fixed `mkdocstrings` showing root headings. + * [x] Included all APIs methods to `mkdocstrings` docs. + * [x] Removed unnecessary information from docs. + * [x] Corrected Spelling and typos. + * [x] Fixed context and grammar. + * [x] Removed `motivation.md`. + * [x] Renamed many terms. + * [x] Fixed hyper-links. + * [x] Reformatted missing or improper information. + * [x] Fixed context and spellings in Docs files. + * [x] Simplified language for easy understanding. + * [x] Updated image sizes for better visibility. - [x] Bash Script: Updated to Latest OpenCV Binaries version and related changes - [x] Docs: Moved version-selector to header and changed default to alias. - [x] Docs: Updated `deploy_docs.yml` for releasing dev, stable, and release versions. @@ -516,8 +516,8 @@ limitations under the License. - [x] Docs: Version Selector UI reworked and other minor changes. ??? danger "Breaking Updates/Changes" - - [ ] :warning: `y_tube` parameter renamed as `stream_mode` in CamGear API! - - [ ] :warning: Removed Travis support and `travis.yml` deleted. + - [ ] :warning: `y_tube` parameter renamed as `stream_mode` in CamGear API! + - [ ] :warning: Removed Travis support and `travis.yml` deleted. ??? bug "Bug-fixes" - [x] Fixed StreamGear API Limited Segments Bug @@ -537,7 +537,7 @@ limitations under the License. - [x] CI: Codecov bugfixes. - [x] Azure-Pipelines Codecov BugFixes. - [x] Fixed `version.json` not detecting properly in `version-select.js`. - - [x] Fixed images not centered inside
tag. + - [x] Fixed images not centered inside `
` tag. - [x] Fixed Asset Colors. - [x] Fixed failing CI tests. - [x] Fixed Several logging bugs. @@ -560,23 +560,23 @@ limitations under the License. ??? tip "New Features" - [x] **StreamGear API:** - * [ ] New API that automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats. - * [ ] Implemented multi-platform , standalone, highly extensible and flexible wrapper around FFmpeg for generating chunked-encoded media segments of the media, and easily accessing almost all of its parameters. - * [ ] API automatically transcodes videos/audio files & real-time frames into a sequence of multiple smaller chunks/segments and also creates a Manifest file. - * [ ] Added initial support for [MPEG-DASH](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_. - * [ ] Constructed default behavior in StreamGear, for auto-creating a Primary Stream of same resolution and framerate as source. - * [ ] Added [TQDM](https://github.com/tqdm/tqdm) progress bar in non-debugged output for visual representation of internal processes. - * [ ] Implemented several internal methods for preprocessing FFmpeg and internal parameters for producing streams. - * [ ] Several standalone internal checks to ensure robust performance. - * [ ] New `terminate()` function to terminate StremGear Safely. - * [ ] New StreamGear Dual Modes of Operation: + * [x] New API that automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats. + * [x] Implemented multi-platform , standalone, highly extensible and flexible wrapper around FFmpeg for generating chunked-encoded media segments of the media, and easily accessing almost all of its parameters. + * [x] API automatically transcodes videos/audio files & real-time frames into a sequence of multiple smaller chunks/segments and also creates a Manifest file. + * [x] Added initial support for [MPEG-DASH](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_. + * [x] Constructed default behavior in StreamGear, for auto-creating a Primary Stream of same resolution and framerate as source. + * [x] Added [TQDM](https://github.com/tqdm/tqdm) progress bar in non-debugged output for visual representation of internal processes. + * [x] Implemented several internal methods for preprocessing FFmpeg and internal parameters for producing streams. + * [x] Several standalone internal checks to ensure robust performance. + * [x] New `terminate()` function to terminate StremGear Safely. + * [x] New StreamGear Dual Modes of Operation: + Implemented *Single-Source* and *Real-time Frames* like independent Transcoding Modes. + Linked `-video_source` attribute for activating these modes + **Single-Source Mode**, transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller segments for streaming + **Real-time Frames Mode**, directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller segments for streaming + Added separate functions, `stream()` for Real-time Frame Mode and `transcode_source()` for Single-Source Mode for easy transcoding. + Included auto-colorspace detection and RGB Mode like features _(extracted from WriteGear)_, into StreamGear. - * [ ] New StreamGear Parameters: + * [x] New StreamGear Parameters: + Developed several new parameters such as: + `output`: handles assets directory + `formats`: handles adaptive HTTP streaming format. @@ -592,7 +592,7 @@ limitations under the License. + `-gop` to manually specify GOP length. + `-ffmpeg_download_path` to handle custom FFmpeg download path on windows. + `-clear_prev_assets` to remove any previous copies of SteamGear Assets. - * [ ] New StreamGear docs, MPEG-DASH demo, and recommended DASH players list: + * [x] New StreamGear docs, MPEG-DASH demo, and recommended DASH players list: + Added new StreamGear docs, usage examples, parameters, references, new FAQs. + Added Several StreamGear usage examples w.r.t Mode of Operation. + Implemented [**Clappr**](https://github.com/clappr/clappr) based on [**Shaka-Player**](https://github.com/google/shaka-player), as Demo Player. @@ -603,65 +603,65 @@ limitations under the License. + Recommended tested Online, Command-line and GUI Adaptive Stream players. + Implemented separate FFmpeg installation doc for StreamGear API. + Reduced `rebufferingGoal` for faster response. - * [ ] New StreamGear CI tests: + * [x] New StreamGear CI tests: + Added IO and API initialization CI tests for its Modes. + Added various mode Streaming check CI tests. - [x] **NetGear_Async API:** - * [ ] Added new `send_terminate_signal` internal method. - * [ ] Added `WindowsSelectorEventLoopPolicy()` for windows 3.8+ envs. - * [ ] Moved Client auto-termination to separate method. - * [ ] Implemented graceful termination with `signal` API on UNIX machines. - * [ ] Added new `timeout` attribute for controlling Timeout in Connections. - * [ ] Added missing termination optimizer (`linger=0`) flag. - * [ ] Several ZMQ Optimizer Flags added to boost performance. + * [x] Added new `send_terminate_signal` internal method. + * [x] Added `WindowsSelectorEventLoopPolicy()` for windows 3.8+ envs. + * [x] Moved Client auto-termination to separate method. + * [x] Implemented graceful termination with `signal` API on UNIX machines. + * [x] Added new `timeout` attribute for controlling Timeout in Connections. + * [x] Added missing termination optimizer (`linger=0`) flag. + * [x] Several ZMQ Optimizer Flags added to boost performance. - [x] **WriteGear API:** - * [ ] Added support for adding duplicate FFmpeg parameters to `output_params`: + * [x] Added support for adding duplicate FFmpeg parameters to `output_params`: + Added new `-clones` attribute in `output_params` parameter for handing this behavior.. + Support to pass FFmpeg parameters as list, while maintaining the exact order it was specified. + Built support for `zmq.REQ/zmq.REP` and `zmq.PUB/zmq.SUB` patterns in this mode. + Added new CI tests debugging this behavior. + Updated docs accordingly. - * [ ] Added support for Networks URLs in Compression Mode: + * [x] Added support for Networks URLs in Compression Mode: + `output_filename` parameter supports Networks URLs in compression modes only + Added automated handling of non path/file Networks URLs as input. + Implemented new `is_valid_url` helper method to easily validate assigned URLs value. + Validates whether the given URL value has scheme/protocol supported by assigned/installed ffmpeg or not. + WriteGear will throw `ValueError` if `-output_filename` is not supported. + Added related CI tests and docs. - * [ ] Added `disable_force_termination` attribute in WriteGear to disable force-termination. + * [x] Added `disable_force_termination` attribute in WriteGear to disable force-termination. - [x] **NetGear API:** - * [ ] Added option to completely disable Native Frame-Compression: + * [x] Added option to completely disable Native Frame-Compression: + Checks if any Incorrect/Invalid value is assigned on `compression_format` attribute. + Completely disables Native Frame-Compression. + Updated docs accordingly. - [x] **CamGear API:** - * [ ] Added new and robust regex for identifying YouTube URLs. - * [ ] Moved `youtube_url_validator` to Helper. + * [x] Added new and robust regex for identifying YouTube URLs. + * [x] Moved `youtube_url_validator` to Helper. - [x] **New `helper.py` methods:** - * [ ] Added `validate_video` function to validate video_source. - * [ ] Added `extract_time` Extract time from give string value. - * [ ] Added `get_video_bitrate` to calculate video birate from resolution, framerate, bits-per-pixels values. - * [ ] Added `delete_safe` to safely delete files of given extension. - * [ ] Added `validate_audio` to validate audio source. - * [ ] Added new Helper CI tests. + * [x] Added `validate_video` function to validate video_source. + * [x] Added `extract_time` Extract time from give string value. + * [x] Added `get_video_bitrate` to calculate video birate from resolution, framerate, bits-per-pixels values. + * [x] Added `delete_safe` to safely delete files of given extension. + * [x] Added `validate_audio` to validate audio source. + * [x] Added new Helper CI tests. + Added new `check_valid_mpd` function to test MPD files validity. + Added `mpegdash` library to CI requirements. - [x] **Deployed New Docs Upgrades:** - * [ ] Added new assets like _images, gifs, custom scripts, javascripts fonts etc._ for achieving better visual graphics in docs. - * [ ] Added `clappr.min.js`, `dash-shaka-playback.js`, `clappr-level-selector.min.js` third-party javascripts locally. - * [ ] Extended Overview docs Hyperlinks to include all major sub-pages _(such as Usage Examples, Reference, FAQs etc.)_. - * [ ] Replaced GIF with interactive MPEG-DASH Video Example in Stabilizer Docs. - * [ ] Added new `pymdownx.keys` to replace `[Ctrl+C]/[⌘+C]` formats. - * [ ] Added new `custom.css` stylescripts variables for fluid animations in docs. - * [ ] Overridden announce bar and added donation button. - * [ ] Lossless WEBP compressed all PNG assets for faster loading. - * [ ] Enabled lazy-loading for GIFS and Images for performance. - * [ ] Reimplemented Admonitions contexts and added new ones. - * [ ] Added StreamGear and its different modes Docs Assets. - * [ ] Added patch for images & unicodes for PiP flavored markdown in `setup.py`. + * [x] Added new assets like _images, gifs, custom scripts, javascripts fonts etc._ for achieving better visual graphics in docs. + * [x] Added `clappr.min.js`, `dash-shaka-playback.js`, `clappr-level-selector.min.js` third-party javascripts locally. + * [x] Extended Overview docs Hyperlinks to include all major sub-pages _(such as Usage Examples, Reference, FAQs etc.)_. + * [x] Replaced GIF with interactive MPEG-DASH Video Example in Stabilizer Docs. + * [x] Added new `pymdownx.keys` to replace `[Ctrl+C]/[⌘+C]` formats. + * [x] Added new `custom.css` stylescripts variables for fluid animations in docs. + * [x] Overridden announce bar and added donation button. + * [x] Lossless WEBP compressed all PNG assets for faster loading. + * [x] Enabled lazy-loading for GIFS and Images for performance. + * [x] Reimplemented Admonitions contexts and added new ones. + * [x] Added StreamGear and its different modes Docs Assets. + * [x] Added patch for images & unicodes for PiP flavored markdown in `setup.py`. - [x] **Added `Request Info` and `Welcome` GitHub Apps to automate PR and issue workflow** - * [ ] Added new `config.yml` for customizations. - * [ ] Added various suitable configurations. + * [x] Added new `config.yml` for customizations. + * [x] Added various suitable configurations. - [x] Added new `-clones` attribute to handle FFmpeg parameter clones in StreamGear and WriteGear API. - [x] Added new Video-only and Audio-Only sources in bash script. - [x] Added new paths in bash script for storing StreamGear & WriteGear assets temporarily. @@ -775,47 +775,48 @@ limitations under the License. ## v0.1.8 (2020-06-12) ??? tip "New Features" - - [x] **Multiple Clients support in NetGear API:** - * [ ] Implemented support for handling any number of Clients simultaneously with a single Server in this mode. - * [ ] Added new `multiclient_mode` attribute for enabling this mode easily. - * [ ] Built support for `zmq.REQ/zmq.REP` and `zmq.PUB/zmq.SUB` patterns in this mode. - * [ ] Implemented ability to receive data from all Client(s) along with frames with `zmq.REQ/zmq.REP` pattern only. - * [ ] Updated related CI tests - - [x] **Support for robust Lazy Pirate pattern(auto-reconnection) in NetGear API for both server and client ends:** - * [ ] Implemented a algorithm where NetGear rather than doing a blocking receive, will now: - + Poll the socket and receive from it only when it's sure a reply has arrived. - + Attempt to reconnect, if no reply has arrived within a timeout period. - + Abandon the connection if there is still no reply after several requests. - * [ ] Implemented its default support for `REQ/REP` and `PAIR` messaging patterns internally. - * [ ] Added new `max_retries` and `request_timeout`(in seconds) for handling polling. - * [ ] Added `DONTWAIT` flag for interruption-free data receiving. - * [ ] Both Server and Client can now reconnect even after a premature termination. - - [x] **Performance Updates for NetGear API:** - * [ ] Added default Frame Compression support for Bidirectional frame transmission in Bidirectional mode. - * [ ] Added support for `Reducer()` function in Helper.py to aid reducing frame-size on-the-go for more performance. - * [ ] Added small delay in `recv()` function at client's end to reduce system load. - * [ ] Reworked and Optimized NetGear termination, and also removed/changed redundant definitions and flags. - - [x] **Docs Migration to Mkdocs:** - * [ ] Implemented a beautiful, static documentation site based on [MkDocs](https://www.mkdocs.org/) which will then be hosted on GitHub Pages. - * [ ] Crafted base mkdocs with third-party elegant & simplistic [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/) theme. - * [ ] Implemented new `mkdocs.yml` for Mkdocs with relevant data. - * [ ] Added new `docs` folder to handle markdown pages and its assets. - * [ ] Added new Markdown pages(`.md`) to docs folder, which are carefully crafted documents - [x] based on previous Wiki's docs, and some completely new additions. - * [ ] Added navigation under tabs for easily accessing each document. - * [ ] **New Assets:** + - [x] **NetGear API:** + * [x] Multiple Clients support: + + Implemented support for handling any number of Clients simultaneously with a single Server in this mode. + + Added new `multiclient_mode` attribute for enabling this mode easily. + + Built support for `zmq.REQ/zmq.REP` and `zmq.PUB/zmq.SUB` patterns in this mode. + + Implemented ability to receive data from all Client(s) along with frames with `zmq.REQ/zmq.REP` pattern only. + + Updated related CI tests + * [x] Support for robust Lazy Pirate pattern(auto-reconnection) in NetGear API for both server and client ends: + + Implemented a algorithm where NetGear rather than doing a blocking receive, will now: + + Poll the socket and receive from it only when it's sure a reply has arrived. + + Attempt to reconnect, if no reply has arrived within a timeout period. + + Abandon the connection if there is still no reply after several requests. + + Implemented its default support for `REQ/REP` and `PAIR` messaging patterns internally. + + Added new `max_retries` and `request_timeout`(in seconds) for handling polling. + + Added `DONTWAIT` flag for interruption-free data receiving. + + Both Server and Client can now reconnect even after a premature termination. + * [x] Performance Updates: + + Added default Frame Compression support for Bidirectional frame transmission in Bidirectional mode. + + Added support for `Reducer()` function in Helper.py to aid reducing frame-size on-the-go for more performance. + + Added small delay in `recv()` function at client's end to reduce system load. + + Reworked and Optimized NetGear termination, and also removed/changed redundant definitions and flags. + - [x] **Docs: Migration to Mkdocs** + * [x] Implemented a beautiful, static documentation site based on [MkDocs](https://www.mkdocs.org/) which will then be hosted on GitHub Pages. + * [x] Crafted base mkdocs with third-party elegant & simplistic [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/) theme. + * [x] Implemented new `mkdocs.yml` for Mkdocs with relevant data. + * [x] Added new `docs` folder to handle markdown pages and its assets. + * [x] Added new Markdown pages(`.md`) to docs folder, which are carefully crafted documents - [x] based on previous Wiki's docs, and some completely new additions. + * [x] Added navigation under tabs for easily accessing each document. + * [x] New Assets: + Added new assets like _gifs, images, custom scripts, favicons, site.webmanifest etc._ for bringing standard and quality to docs visual design. + Designed brand new logo and banner for VidGear Documents. + Deployed all assets under separate [*Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License*](https://creativecommons.org/licenses/by-nc-sa/4.0/). - * [ ] **Added Required Plugins and Extensions:** + * [x] Added Required Plugins and Extensions: + Added support for all [pymarkdown-extensions](https://facelessuser.github.io/pymdown-extensions/). + Added support for some important `admonition`, `attr_list`, `codehilite`, `def_list`, `footnotes`, `meta`, and `toc` like Mkdocs extensions. + Enabled `search`, `minify` and `git-revision-date-localized` plugins support. + Added various VidGear's social links to yaml. + Added support for `en` _(English)_ language. - * [ ] **Auto-Build API Reference with `mkdocstrings:`** + * [x] Auto-Build API Reference with `mkdocstrings:` + Added support for [`mkdocstrings`](https://github.com/pawamoy/mkdocstrings) plugin for auto-building each VidGear's API references. + Added python handler for parsing python source-code to `mkdocstrings`. - * [ ] **Auto-Deploy Docs with GitHub Actions:** + * [x] Auto-Deploy Docs with GitHub Actions: + Implemented Automated Docs Deployment on gh-pages through GitHub Actions workflow. + Added new workflow yaml with minimal configuration for automated docs deployment. + Added all required python dependencies and environment for this workflow. @@ -884,38 +885,38 @@ limitations under the License. ??? tip "New Features" - [x] **WebGear API:** - * [ ] Added a robust Live Video Server API that can transfer live video frames to any web browser on the network in real-time. - * [ ] Implemented a flexible asyncio wrapper around [`starlette`](https://www.starlette.io/) ASGI Application Server. - * [ ] Added seamless access to various starlette's Response classes, Routing tables, Static Files, Template engine(with Jinja2), etc. - * [ ] Added a special internal access to VideoGear API and all its parameters. - * [ ] Implemented a new Auto-Generation Work-flow to generate/download & thereby validate WebGear API data files from its GitHub server automatically. - * [ ] Added on-the-go dictionary parameter in WebGear to tweak performance, Route Tables and other internal properties easily. - * [ ] Added new simple & elegant default Bootstrap Cover Template for WebGear Server. - * [ ] Added `__main__.py` to directly run WebGear Server through the terminal. - * [ ] Added new gif and related docs for WebGear API. - * [ ] Added and Updated various CI tests for this API. + * [x] Added a robust Live Video Server API that can transfer live video frames to any web browser on the network in real-time. + * [x] Implemented a flexible asyncio wrapper around [`starlette`](https://www.starlette.io/) ASGI Application Server. + * [x] Added seamless access to various starlette's Response classes, Routing tables, Static Files, Template engine(with Jinja2), etc. + * [x] Added a special internal access to VideoGear API and all its parameters. + * [x] Implemented a new Auto-Generation Work-flow to generate/download & thereby validate WebGear API data files from its GitHub server automatically. + * [x] Added on-the-go dictionary parameter in WebGear to tweak performance, Route Tables and other internal properties easily. + * [x] Added new simple & elegant default Bootstrap Cover Template for WebGear Server. + * [x] Added `__main__.py` to directly run WebGear Server through the terminal. + * [x] Added new gif and related docs for WebGear API. + * [x] Added and Updated various CI tests for this API. - [x] **NetGear_Async API:** - * [ ] Designed NetGear_Async asynchronous network API built upon ZeroMQ's asyncio API. - * [ ] Implemented support for state-of-the-art asyncio event loop [`uvloop`](https://github.com/MagicStack/uvloop) at its backend. - * [ ] Achieved Unmatchable high-speed and lag-free video streaming over the network with minimal resource constraint. - * [ ] Added exclusive internal wrapper around VideoGear API for this API. - * [ ] Implemented complete server-client handling and options to use variable protocols/patterns for this API. - * [ ] Implemented support for all four ZeroMQ messaging patterns: i.e `zmq.PAIR`, `zmq.REQ/zmq.REP`, `zmq.PUB/zmq.SUB`, and `zmq.PUSH/zmq.PULL`. - * [ ] Implemented initial support for `tcp` and `ipc` protocols. - * [ ] Added new Coverage CI tests for NetGear_Async Network Gear. - * [ ] Added new Benchmark tests for benchmarking NetGear_Async against NetGear. + * [x] Designed NetGear_Async asynchronous network API built upon ZeroMQ's asyncio API. + * [x] Implemented support for state-of-the-art asyncio event loop [`uvloop`](https://github.com/MagicStack/uvloop) at its backend. + * [x] Achieved Unmatchable high-speed and lag-free video streaming over the network with minimal resource constraint. + * [x] Added exclusive internal wrapper around VideoGear API for this API. + * [x] Implemented complete server-client handling and options to use variable protocols/patterns for this API. + * [x] Implemented support for all four ZeroMQ messaging patterns: i.e `zmq.PAIR`, `zmq.REQ/zmq.REP`, `zmq.PUB/zmq.SUB`, and `zmq.PUSH/zmq.PULL`. + * [x] Implemented initial support for `tcp` and `ipc` protocols. + * [x] Added new Coverage CI tests for NetGear_Async Network Gear. + * [x] Added new Benchmark tests for benchmarking NetGear_Async against NetGear. - [x] **Asynchronous Enhancements:** - * [ ] Added `asyncio` package to for handling asynchronous APIs. - * [ ] Moved WebGear API(webgear.py) to `asyncio` and created separate asyncio `helper.py` for it. - * [ ] Various Performance tweaks for Asyncio APIs with concurrency within a single thread. - * [ ] Moved `__main__.py` to asyncio for easier access to WebGear API through the terminal. - * [ ] Updated `setup.py` with new dependencies and separated asyncio dependencies. + * [x] Added `asyncio` package to for handling asynchronous APIs. + * [x] Moved WebGear API(webgear.py) to `asyncio` and created separate asyncio `helper.py` for it. + * [x] Various Performance tweaks for Asyncio APIs with concurrency within a single thread. + * [x] Moved `__main__.py` to asyncio for easier access to WebGear API through the terminal. + * [x] Updated `setup.py` with new dependencies and separated asyncio dependencies. - [x] **General Enhancements:** - * [ ] Added new highly-precise Threaded FPS class for accurate benchmarking with `time.perf_counter` python module. - * [ ] Added a new [Gitter](https://gitter.im/vidgear/community) community channel. - * [ ] Added a new *Reducer* function to reduce the frame size on-the-go. - * [ ] Add *Flake8* tests to Travis CI to find undefined names. (PR by @cclauss) - * [ ] Added a new unified `logging handler` helper function for vidgear. + * [x] Added new highly-precise Threaded FPS class for accurate benchmarking with `time.perf_counter` python module. + * [x] Added a new [Gitter](https://gitter.im/vidgear/community) community channel. + * [x] Added a new *Reducer* function to reduce the frame size on-the-go. + * [x] Add *Flake8* tests to Travis CI to find undefined names. (PR by @cclauss) + * [x] Added a new unified `logging handler` helper function for vidgear. ??? success "Updates/Improvements" - [x] Re-implemented and simplified logic for NetGear Async server-end. @@ -989,49 +990,49 @@ limitations under the License. ??? tip "New Features" - [x] **NetGear API:** - * [ ] **Added powerful ZMQ Authentication & Data Encryption features for NetGear API:** + * [x] Added powerful ZMQ Authentication & Data Encryption features for NetGear API: + Added exclusive `secure_mode` param for enabling it. + Added support for two most powerful `Stonehouse` & `Ironhouse` ZMQ security mechanisms. + Added smart auth-certificates/key generation and validation features. - * [ ] **Implemented Robust Multi-Servers support for NetGear API:** + * [x] Implemented Robust Multi-Servers support for NetGear API: + Enables Multiple Servers messaging support with a single client. + Added exclusive `multiserver_mode` param for enabling it. + Added support for `REQ/REP` & `PUB/SUB` patterns for this mode. + Added ability to send additional data of any datatype along with the frame in realtime in this mode. - * [ ] **Introducing exclusive Bidirectional Mode for bidirectional data transmission:** + * [x] Introducing exclusive Bidirectional Mode for bidirectional data transmission: + Added new `return_data` parameter to `recv()` function. + Added new `bidirectional_mode` attribute for enabling this mode. + Added support for `PAIR` & `REQ/REP` patterns for this mode + Added support for sending data of any python datatype. + Added support for `message` parameter for non-exclusive primary modes for this mode. - * [ ] **Implemented compression support with on-the-fly flexible frame encoding for the Server-end:** + * [x] Implemented compression support with on-the-fly flexible frame encoding for the Server-end: + Added initial support for `JPEG`, `PNG` & `BMP` encoding formats . + Added exclusive options attribute `compression_format` & `compression_param` to tweak this feature. + Client-end will now decode frame automatically based on the encoding as well as support decoding flags. - * [ ] Added `force_terminate` attribute flag for handling force socket termination at the Server-end if there's latency in the network. - * [ ] Implemented new *Publish/Subscribe(`zmq.PUB/zmq.SUB`)* pattern for seamless Live Streaming in NetGear API. + * [x] Added `force_terminate` attribute flag for handling force socket termination at the Server-end if there's latency in the network. + * [x] Implemented new *Publish/Subscribe(`zmq.PUB/zmq.SUB`)* pattern for seamless Live Streaming in NetGear API. - [x] **PiGear API:** - * [ ] Added new threaded internal timing function for PiGear to handle any hardware failures/frozen threads. - * [ ] PiGear will not exit safely with `SystemError` if Picamera ribbon cable is pulled out to save resources. - * [ ] Added support for new user-defined `HWFAILURE_TIMEOUT` options attribute to alter timeout. + * [x] Added new threaded internal timing function for PiGear to handle any hardware failures/frozen threads. + * [x] PiGear will not exit safely with `SystemError` if Picamera ribbon cable is pulled out to save resources. + * [x] Added support for new user-defined `HWFAILURE_TIMEOUT` options attribute to alter timeout. - [x] **VideoGear API:** - * [ ] Added `framerate` global variable and removed redundant function. - * [ ] Added `CROP_N_ZOOM` attribute in Videogear API for supporting Crop and Zoom stabilizer feature. + * [x] Added `framerate` global variable and removed redundant function. + * [x] Added `CROP_N_ZOOM` attribute in Videogear API for supporting Crop and Zoom stabilizer feature. - [x] **WriteGear API:** - * [ ] Added new `execute_ffmpeg_cmd` function to pass a custom command to its FFmpeg pipeline. + * [x] Added new `execute_ffmpeg_cmd` function to pass a custom command to its FFmpeg pipeline. - [x] **Stabilizer class:** - * [ ] Added new Crop and Zoom feature. + * [x] Added new Crop and Zoom feature. + Added `crop_n_zoom` param for enabling this feature. - * [ ] Updated docs. + * [x] Updated docs. - [x] **CI & Tests updates:** - * [ ] Replaced python 3.5 matrices with latest python 3.8 matrices in Linux environment. - * [ ] Added full support for **Codecov** in all CI environments. - * [ ] Updated OpenCV to v4.2.0-pre(master branch). - * [ ] Added various Netgear API tests. - * [ ] Added initial Screengear API test. - * [ ] More test RTSP feeds added with better error handling in CamGear network test. - * [ ] Added tests for ZMQ authentication certificate generation. - * [ ] Added badge and Minor doc updates. + * [x] Replaced python 3.5 matrices with latest python 3.8 matrices in Linux environment. + * [x] Added full support for **Codecov** in all CI environments. + * [x] Updated OpenCV to v4.2.0-pre(master branch). + * [x] Added various Netgear API tests. + * [x] Added initial Screengear API test. + * [x] More test RTSP feeds added with better error handling in CamGear network test. + * [x] Added tests for ZMQ authentication certificate generation. + * [x] Added badge and Minor doc updates. - [x] Added VidGear's official native support for MacOS environments. @@ -1229,7 +1230,7 @@ limitations under the License. ??? bug "Bug-fixes" - [x] Patched Major PiGear Bug: Incorrect import of PiRGBArray function in PiGear Class - - [x] Several Fixes** for backend `picamera` API handling during frame capture(PiGear) + - [x] Several Fixes for backend `picamera` API handling during frame capture(PiGear) - [x] Fixed missing frame variable initialization. - [x] Fixed minor typos diff --git a/docs/gears.md b/docs/gears.md index 740340e68..f9a3bf9b0 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -29,7 +29,7 @@ limitations under the License. VidGear is built on Standalone APIs - also known as **Gears**, each with some unique functionality. Each Gears is designed exclusively to handle/control/process different data-specific & device-specific video streams, network streams, and media encoders/decoders. -These Gears provides the user an easy-to-use, dynamic, extensible, and exposed Multi-Threaded + Asyncio optimized internal layer above state-of-the-art libraries to work with, while silently delivering robust error-handling. +These Gears provide the user with an easy-to-use, extensible, exposed, and optimized parallel framework above many state-of-the-art libraries, while silently delivering robust error handling and unmatched real-time performance. ## Gears Classification @@ -64,7 +64,7 @@ These Gears can be classified as follows: ### D. Network Gears -> **Basic Function:** Sends/Receives [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames over connected network. +> **Basic Function:** Sends/Receives [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames over connected networks. * [NetGear](netgear/overview/): Handles High-Performance Video-Frames & Data Transfer between interconnecting systems over the network. diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index f693e0bc2..59464c4b8 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -513,9 +513,7 @@ client.close() ## Using Bidirectional Mode for Video-Frames Transfer with Frame Compression :fire: - -See complete usage example [here ➶](../../advanced/compression/#using-bidirectional-mode-for-video-frames-transfer-with-frame-compression) - +!!! example "This usage examples can be found [here ➶](../../advanced/compression/#using-bidirectional-mode-for-video-frames-transfer-with-frame-compression)"   diff --git a/docs/gears/netgear/params.md b/docs/gears/netgear/params.md index 637768715..f52d70629 100644 --- a/docs/gears/netgear/params.md +++ b/docs/gears/netgear/params.md @@ -149,7 +149,7 @@ This parameter provides the flexibility to alter various NetGear API's internal * **`bidirectional_mode`** (_boolean_) : This internal attribute activates the exclusive [**Bidirectional Mode**](../advanced/bidirectional_mode/), if enabled(`True`). - * **`ssh_tunnel_mode`** (_string_) : This internal attribute activates the exclusive [**SSH Tunneling Mode**](../advanced/secure_mode/) ==at the Server-end only==. + * **`ssh_tunnel_mode`** (_string_) : This internal attribute activates the exclusive [**SSH Tunneling Mode**](../advanced/ssh_tunnel/) ==at the Server-end only==. * **`ssh_tunnel_pwd`** (_string_): In SSH Tunneling Mode, This internal attribute sets the password required to authorize Host for SSH Connection ==at the Server-end only==. More information can be found [here ➶](../advanced/ssh_tunnel/#supported-attributes) diff --git a/docs/gears/stabilizer/overview.md b/docs/gears/stabilizer/overview.md index acaeb824a..51c41d440 100644 --- a/docs/gears/stabilizer/overview.md +++ b/docs/gears/stabilizer/overview.md @@ -37,7 +37,7 @@ limitations under the License. > Stabilizer is an auxiliary class that enables Video Stabilization for vidgear with minimalistic latency, and at the expense of little to no additional computational requirements. -The basic idea behind it is to tracks and save the salient feature array for the given number of frames and then uses these anchor point to cancel out all perturbations relative to it for the incoming frames in the queue. This class relies heavily on [**Threaded Queue mode**](../../../bonus/TQM/) for error-free & ultra-fast frame handling. +The basic idea behind it is to tracks and save the salient feature array for the given number of frames and then uses these anchor point to cancel out all perturbations relative to it for the incoming frames in the queue. This class relies on [**Fixed-Size Python Queues**](../../../bonus/TQM/#b-utilizes-fixed-size-queues) for error-free & ultra-fast frame handling. !!! tip "For more detailed information on Stabilizer working, See [this blogpost ➶](https://learnopencv.com/video-stabilization-using-point-feature-matching-in-opencv/)" diff --git a/docs/gears/streamgear/ffmpeg_install.md b/docs/gears/streamgear/ffmpeg_install.md index 5d1fd3c7d..d41ab7b49 100644 --- a/docs/gears/streamgear/ffmpeg_install.md +++ b/docs/gears/streamgear/ffmpeg_install.md @@ -68,7 +68,7 @@ The StreamGear API supports _Auto-Installation_ and _Manual Configuration_ metho !!! quote "This is a recommended approach on Windows Machines" -If StreamGear API not receives any input from the user on [**`custom_ffmpeg`**](../params/#custom_ffmpeg) parameter, then on Windows system StreamGear API **auto-generates** the required FFmpeg Static Binaries, according to your system specifications, into the temporary directory _(for e.g. `C:\Temp`)_ of your machine. +If StreamGear API not receives any input from the user on [**`custom_ffmpeg`**](../params/#custom_ffmpeg) parameter, then on Windows system StreamGear API **auto-generates** the required FFmpeg Static Binaries from a dedicated [**Github Server**](https://github.com/abhiTronix/FFmpeg-Builds) into the temporary directory _(for e.g. `C:\Temp`)_ of your machine. !!! warning Important Information diff --git a/docs/gears/streamgear/overview.md b/docs/gears/streamgear/overview.md deleted file mode 100644 index ff41d8b3e..000000000 --- a/docs/gears/streamgear/overview.md +++ /dev/null @@ -1,133 +0,0 @@ - - -# StreamGear API - - -
- StreamGear Flow Diagram -
StreamGear API's generalized workflow
-
- - -## Overview - -> StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH)_ in just few lines of python code. - -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. - -SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of fixed 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. - -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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 only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. - -  - -!!! danger "Important" - - * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../ffmpeg_install/) for its installation. - - * :warning: StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - - * It is advised to enable logging _([`logging=True`](../params/#logging))_ on the first run for easily identifying any runtime errors. - -  - -## Mode of Operations - -StreamGear primarily operates in following independent modes for transcoding: - -- [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. - -- [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. - -  - -## Importing - -You can import StreamGear API in your program as follows: - -```python -from vidgear.gears import StreamGear -``` - -  - - -## Watch Demo - -Watch StreamGear transcoded MPEG-DASH Stream: - -
-
-
-
-
-
-
-

Powered by clappr & shaka-player

- -!!! info "This video assets _(Manifest and segments)_ are hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" - -!!! quote "Video Credits: [**"Tears of Steel"** - Project Mango Teaser](https://mango.blender.org/download/)" - -  - -## Recommended Players - -=== "GUI Players" - - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. - - [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. - - [x] **[Parole](https://docs.xfce.org/apps/parole/start):** _(UNIX only)_ Parole is a modern simple media player based on the GStreamer framework for Unix and Unix-like operating systems. - -=== "Command-Line Players" - - [x] **[MP4Client](https://github.com/gpac/gpac/wiki/MP4Client-Intro):** [GPAC](https://gpac.wp.imt.fr/home/) provides a highly configurable multimedia player called MP4Client. GPAC itself is an open source multimedia framework developed for research and academic purposes, and used in many media production chains. - - [x] **[ffplay](https://ffmpeg.org/ffplay.html):** FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs. - -=== "Online Players" - !!! tip "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" - - - [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. - - [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. - - [x] **[MediaElementPlayer](https://github.com/mediaelement/mediaelement):** MediaElementPlayer is a complete HTML/CSS audio/video player. - -  - -## Parameters - - - -## References - - - - -## FAQs - - - -  \ No newline at end of file diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 338f8fa0c..c3aa05d01 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -88,7 +88,7 @@ stream.stop() streamer.terminate() ``` -!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable chunks that contains information about a Primary Stream of same resolution and framerate[^1] as input _(without any audio)_." +!!! 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)_."   diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 558ec4b2f..79cc9f7c0 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -53,7 +53,7 @@ streamer.transcode_source() streamer.terminate() ``` -!!! success "After running these bare-minimum commands, StreamGear will produce a Manifest file _(`dash.mpd`)_ with steamable chunks that contains information about a Primary Stream of 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."   diff --git a/docs/gears/videogear/overview.md b/docs/gears/videogear/overview.md index 75768e1e7..76336449c 100644 --- a/docs/gears/videogear/overview.md +++ b/docs/gears/videogear/overview.md @@ -41,9 +41,6 @@ VideoGear is ideal when you need to switch to different video sources without ch * It is advised to enable logging(`logging = True`) on the first run for easily identifying any runtime errors. - -!!! warning "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using PiGear API, otherwise nothing will work." -   ## Importing diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index fe9cf1e77..7beb267a2 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -75,6 +75,8 @@ stream.stop() Following is the bare-minimum code you need to access PiGear API with VideoGear: +!!! warning "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using PiGear Backend, otherwise nothing will work." + ```python # import required libraries from vidgear.gears import VideoGear @@ -169,6 +171,7 @@ The usage example of VideoGear API with Variable PiCamera Properties is as follo !!! info "This example is basically a VideoGear API implementation of this [PiGear usage example](../../pigear/usage/#using-pigear-with-variable-camera-properties). Thereby, any [CamGear](../../camgear/usage/) or [PiGear](../../pigear/usage/) usage examples can be implemented with VideoGear API in the similar manner." +!!! warning "Make sure to [enable Raspberry Pi hardware-specific settings](https://picamera.readthedocs.io/en/release-1.13/quickstart.html) prior using PiGear Backend, otherwise nothing will work." ```python # import required libraries diff --git a/docs/gears/writegear/compression/advanced/ffmpeg_install.md b/docs/gears/writegear/compression/advanced/ffmpeg_install.md index 39a9a4cee..af7471b5a 100644 --- a/docs/gears/writegear/compression/advanced/ffmpeg_install.md +++ b/docs/gears/writegear/compression/advanced/ffmpeg_install.md @@ -69,7 +69,7 @@ The WriteGear API supports _Auto-Installation_ and _Manual Configuration_ method !!! quote "This is a recommended approach on Windows Machines" -If WriteGear API not receives any input from the user on [**`custom_ffmpeg`**](../../params/#custom_ffmpeg) parameter, then on Windows system WriteGear API **auto-generates** the required FFmpeg Static Binaries, according to your system specifications, into the temporary directory _(for e.g. `C:\Temp`)_ of your machine. +If WriteGear API not receives any input from the user on [**`custom_ffmpeg`**](../../params/#custom_ffmpeg) parameter, then on Windows system WriteGear API **auto-generates** the required FFmpeg Static Binaries from a dedicated [**Github Server**](https://github.com/abhiTronix/FFmpeg-Builds) into the temporary directory _(for e.g. `C:\Temp`)_ of your machine. !!! warning Important Information diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index de516e2fa..3f454a627 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -49,17 +49,23 @@ limitations under the License. ## How to compile OpenCV with GStreamer support? -**Answer:** For compiling OpenCV with GSstreamer(`>=v1.0.0`) support, checkout this [tutorial](https://web.archive.org/web/20201225140454/https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c) for Linux and Windows OSes, and **for MacOS do as follows:** +**Answer:** For compiling OpenCV with GSstreamer(`>=v1.0.0`) support: -**Step-1:** First Brew install GStreamer: +=== "On Linux OSes" -```sh -brew update -brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav -``` + - [x] **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** + +=== "On Windows OSes" -**Step-2:** Then, Follow [this tutorial ➶](https://www.learnopencv.com/install-opencv-4-on-macos/) + - [x] **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** + +=== "FOn MAC OSes" + + - [x] **Follow [this tutorial ➶](https://www.learnopencv.com/install-opencv-4-on-macos/) but make sure to brew install GStreamer as follows:** + ```sh + brew install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav + ```   diff --git a/docs/index.md b/docs/index.md index d835fb646..ddfd3498f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ limitations under the License. > VidGear is a High-Performance **Video-Processing** Framework for building complex real-time media applications in python :fire: -VidGear provides an easy-to-use, highly extensible, **[Multi-Threaded](../bonus/TQM/#threaded-queue-mode) + Asyncio Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. +VidGear provides an easy-to-use, highly extensible, **[Multi-Threaded](bonus/TQM/#threaded-queue-mode) + [Asyncio](https://docs.python.org/3/library/asyncio.html) Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. > _"Write Less and Accomplish More"_ — VidGear's Motto diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 5928c63bf..99b69e3ed 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -42,7 +42,7 @@ Must require OpenCV(3.0+) python binaries installed for all core functions. You ### FFmpeg -Must require for the video compression and encoding compatibilities within [StreamGear](#streamgear) and [**Compression Mode**](../../gears/writegear/compression/overview/) in [WriteGear](#writegear) API. +Must require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) and [**WriteGear's Compression Mode**](../../gears/writegear/compression/overview/). !!! tip "FFmpeg Installation" diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index 0ad39d706..0418ffd79 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -34,7 +34,7 @@ When installing VidGear from source, FFmpeg and Aiortc is the only dependency yo ### FFmpeg -Must require for the video compression and encoding compatibilities within [StreamGear](#streamgear) and [**Compression Mode**](../../gears/writegear/compression/overview/) in [WriteGear](#writegear) API. +Must require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) and [**WriteGear's Compression Mode**](../../gears/writegear/compression/overview/). !!! tip "FFmpeg Installation" diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index f428681f6..97d692b4c 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -196,6 +196,8 @@ th { } .md-typeset .task-list-control .task-list-indicator::before { background-color: #FF0000; + -webkit-mask-image: var(--md-admonition-icon--failure); + mask-image: var(--md-admonition-icon--failure); } blockquote { padding: 0.5em 10px; diff --git a/mkdocs.yml b/mkdocs.yml index b06e9dbe1..093034ba9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -215,8 +215,7 @@ nav: - Real-time Frames Mode: - Overview: gears/streamgear/rtfm/overview.md - Usage Examples: gears/streamgear/rtfm/usage.md - - Advanced: - - FFmpeg Installation: gears/streamgear/ffmpeg_install.md + - FFmpeg Installation: gears/streamgear/ffmpeg_install.md - Parameters: gears/streamgear/params.md - References: bonus/reference/streamgear.md - FAQs: help/streamgear_faqs.md diff --git a/setup.py b/setup.py index 4c08ebaf2..b2782ffa4 100644 --- a/setup.py +++ b/setup.py @@ -92,14 +92,16 @@ def latest_version(package_name): "pafy{}".format(latest_version("pafy")), "mss{}".format(latest_version("mss")), "numpy{}".format( - "<=1.19.5" if sys.version_info[:2] == (3, 6) else "" + "<=1.19.5" if sys.version_info[:2] < (3, 7) else "" ), # dropped support for 3.6.x legacies "youtube-dl{}".format(latest_version("youtube-dl")), "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format( - "==1.5.0" if sys.version_info[:2] == (3, 6) else "" + latest_version("simplejpeg") + if sys.version_info[:2] >= (3, 7) + else "==1.5.0" ), # dropped support for 3.6.x legacies "colorlog", "colorama", @@ -130,7 +132,7 @@ def latest_version(package_name): + ( ( ["uvloop{}".format(latest_version("uvloop"))] - if sys.version_info[:2] >= (3, 7) + if sys.version_info[:2] >= (3, 7) # dropped support for 3.6.x legacies else ["uvloop==0.14.0"] ) if (platform.system() != "Windows") From 93fa516745db73efa632b22f9fd2799b80b26c16 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 3 Jun 2021 05:54:18 +0530 Subject: [PATCH 032/112] =?UTF-8?q?=F0=9F=90=9B=20Helper:=20Critical=20Reg?= =?UTF-8?q?ex=20bugs=20fixed(Fixes=20#218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New improved regex for discovering supported encoders in `get_supported_vencoders`. - Reimplemented check for extracting only valid output protocols in `is_valid_url`. - Minor tweaks for better regex compatibility. - Typo fixed --- vidgear/gears/helper.py | 10 ++++------ vidgear/gears/writegear.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index cfd7acdcf..b560e30f9 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -329,11 +329,11 @@ def get_supported_vencoders(path): if x.decode("utf-8").strip().startswith("V") ] # compile regex - finder = re.compile(r"\.\.\s[a-z0-9_-]+") + finder = re.compile(r"[A-Z]*[\.]+[A-Z]*\s[a-z0-9_-]*") # find all outputs outputs = finder.findall("\n".join(supported_vencoders)) # return outputs - return [s.replace(".. ", "") for s in outputs] + return [[s for s in o.split(" ")][-1] for o in outputs] def is_valid_url(path, url=None, logging=False): @@ -357,10 +357,8 @@ def is_valid_url(path, url=None, logging=False): extracted_scheme_url = url.split("://", 1)[0] # extract all FFmpeg supported protocols protocols = check_output([path, "-hide_banner", "-protocols"]) - splitted = protocols.split(b"\n") - supported_protocols = [ - x.decode("utf-8").strip() for x in splitted[2 : len(splitted) - 1] - ] + splitted = [x.decode("utf-8").strip() for x in protocols.split(b"\n")] + supported_protocols = splitted[splitted.index("Output:") + 1 : len(splitted) - 1] supported_protocols += ["rtsp"] # rtsp not included somehow # Test and return result whether scheme is supported if extracted_scheme_url and extracted_scheme_url in supported_protocols: diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index e38171100..32ebb8a7c 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -230,7 +230,7 @@ def __init__( # display confirmation if logging is enabled/disabled if self.__compression and self.__ffmpeg: - # check whether is valid url instead + # check whether url is valid instead if self.__out_file is None: if is_valid_url( self.__ffmpeg, url=output_filename, logging=self.__logging From efdb1e7a17178668eb07f939fb3ea96c115ee483 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 10 Jun 2021 09:40:18 +0530 Subject: [PATCH 033/112] :ambulance: CamGear: Hotfix for Live Camera Streams (Fixes #220) - Added new event flag to keep check on stream read. - Implemented event wait for `read()` to block it when source stream is busy. - Added and Linked `THREAD_TIMEOUT` with event wait timout. - Improved backward compatibility of new additions. - Typos in links and code comments fixed. --- .github/ISSUE_TEMPLATE/question.md | 2 +- docs/index.md | 2 +- vidgear/gears/camgear.py | 45 ++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index e596c033f..5c4239f17 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -18,7 +18,7 @@ _Kindly describe the issue here._ - [ ] I have searched the [issues](https://github.com/abhiTronix/vidgear/issues) for my issue and found nothing related or helpful. -- [ ] I have read the [FAQs](https://abhitronix.github.io/vidgear/help/get_help/#frequently-asked-questions). +- [ ] I have read the [FAQs](https://abhitronix.github.io/vidgear/latest/help/get_help/#frequently-asked-questions). - [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear). diff --git a/docs/index.md b/docs/index.md index ddfd3498f..858ea84ef 100644 --- a/docs/index.md +++ b/docs/index.md @@ -46,7 +46,7 @@ VidGear focuses on simplicity, and thereby lets programmers and software develop - [x] Also, if you're already familar with [OpenCV][opencv] library, then see [Switching from OpenCV Library ➶](switch_from_cv.md) -- [x] Or, if you're just getting started with OpenCV with Python, then see [here ➶](../help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects) +- [x] Or, if you're just getting started with OpenCV with Python, then see [here ➶](help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects)   diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index 7ba087591..3feef0388 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -246,12 +246,6 @@ def __init__( logger.debug( "Enabling Threaded Queue Mode for the current video source!" ) - if self.__thread_timeout: - logger.debug( - "Setting Video-Thread Timeout to {}s.".format( - self.__thread_timeout - ) - ) else: # otherwise disable it self.__threaded_queue_mode = False @@ -261,6 +255,11 @@ 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) + ) + # stream variable initialization self.stream = None @@ -321,7 +320,7 @@ def __init__( self.__queue.put(self.frame) else: raise RuntimeError( - "[CamGear:ERROR] :: Source is invalid, CamGear failed to intitialize stream on this source!" + "[CamGear:ERROR] :: Source is invalid, CamGear failed to initialize stream on this source!" ) # thread initialization @@ -330,6 +329,9 @@ def __init__( # initialize termination flag event self.__terminate = Event() + # initialize stream read flag event + self.__stream_read = Event() + def start(self): """ Launches the internal *Threaded Frames Extractor* daemon. @@ -348,16 +350,24 @@ def __update(self): until the thread is terminated, or frames runs out. """ - # keep iterating infinitely until the thread is terminated or frames runs out + # keep iterating infinitely + # until the thread is terminated + # or frames runs out while True: # if the thread indicator variable is set, stop the thread if self.__terminate.is_set(): break + # stream not read yet + self.__stream_read.clear() + # otherwise, read the next frame from the stream (grabbed, frame) = self.stream.read() - # check for valid frames + # stream read completed + self.__stream_read.set() + + # check for valid frame if received if not grabbed: # no frames received, then safely exit if self.__threaded_queue_mode: @@ -397,8 +407,10 @@ def __update(self): if self.__threaded_queue_mode: self.__queue.put(self.frame) + # indicate immediate termination self.__threaded_queue_mode = False - self.frame = None + self.__terminate.set() + self.__stream_read.set() # release resources self.stream.release() @@ -411,7 +423,14 @@ def read(self): """ while self.__threaded_queue_mode: return self.__queue.get(timeout=self.__thread_timeout) - return self.frame + # return current frame + # only after stream is read + return ( + self.frame + if not self.__terminate.is_set() # check if already terminated + and self.__stream_read.wait(timeout=self.__thread_timeout) # wait for it + else None + ) def stop(self): """ @@ -423,8 +442,10 @@ def stop(self): if self.__threaded_queue_mode: self.__threaded_queue_mode = False - # indicate that the thread should be terminate + # indicate that the thread + # should be terminated immediately self.__terminate.set() + self.__stream_read.set() # wait until stream resources are released (producer thread might be still grabbing frame) if self.__thread is not None: From 85d6ab95342fe0622471c3651cc47fd51c8b2764 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 14 Jun 2021 06:37:25 +0530 Subject: [PATCH 034/112] =?UTF-8?q?=F0=9F=91=B7=20WebGear=5FRTC:=20Updated?= =?UTF-8?q?=20CI=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asyncio_tests/test_webgear_rtc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 8a4c57154..d18ad906c 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -289,12 +289,18 @@ def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): }, { "frame_size_reduction": "invalid_value", - "overwrite_default_files": True, - "enable_infinite_frames": False, "enable_live_broadcast": False, "custom_data_location": "im_wrong", }, - {"custom_data_location": tempfile.gettempdir()}, + { + "custom_data_location": tempfile.gettempdir(), + "enable_infinite_frames": False, + }, + { + "overwrite_default_files": True, + "enable_live_broadcast": True, + "frame_size_reduction": 99, + }, ] @@ -434,4 +440,5 @@ def test_webgear_rtc_routes_validity(): web.routes.clear() # test client = TestClient(web(), raise_server_exceptions=True) + # close web.shutdown() From 05119c938514fd0ec662c952a57aec08fa683995 Mon Sep 17 00:00:00 2001 From: Abhishek Thakur Date: Mon, 14 Jun 2021 12:02:02 +0530 Subject: [PATCH 035/112] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Added=20exception?= =?UTF-8?q?=20in=20WebGear=5FRTC=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asyncio_tests/test_webgear_rtc.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) 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 d18ad906c..57fc2b7ae 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -314,22 +314,23 @@ def test_webgear_rtc_options(options): client = TestClient(web(), raise_server_exceptions=True) response = client.get("/") assert response.status_code == 200 - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - run(offer_pc.close()) + if not "enable_live_broadcast" in options: + (offer_pc, data) = get_RTCPeer_payload() + response_rtc_answer = client.post( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + params = response_rtc_answer.json() + answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) + run(offer_pc.setRemoteDescription(answer)) + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + run(offer_pc.close()) web.shutdown() except Exception as e: if isinstance(e, AssertionError): From efe36706f0427dc3a45637f0d0087bb1c2c816da Mon Sep 17 00:00:00 2001 From: Abhishek Thakur Date: Mon, 14 Jun 2021 12:05:15 +0530 Subject: [PATCH 036/112] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/streamer_tests/asyncio_tests/test_webgear_rtc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 57fc2b7ae..b44190403 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -314,7 +314,10 @@ def test_webgear_rtc_options(options): client = TestClient(web(), raise_server_exceptions=True) response = client.get("/") assert response.status_code == 200 - if not "enable_live_broadcast" in options: + if ( + not "enable_live_broadcast" in options + or options["enable_live_broadcast"] == False + ): (offer_pc, data) = get_RTCPeer_payload() response_rtc_answer = client.post( "/offer", From 4de88c77dc3ce9109f2ae85240b0440ff715d7d0 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 15 Jun 2021 07:26:37 +0530 Subject: [PATCH 037/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20WebGear=5FRTC:=20F?= =?UTF-8?q?ixed=20stream=20freezes=20after=20web-page=20reloading=20(Solve?= =?UTF-8?q?d=20#219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented new algorithm to continue stream even when webpage is reloaded. - Inherit and modified `next_timestamp` VideoStreamTrack method for generating accurate timestamps. - Implemented `reset_connections` callable to reset all peer connections and recreate Video-Server timestamps.(Implemented by @kpetrykin) - Added `close_connection` endpoint in JavaScript to inform server page refreshing.(Thanks to @kpetrykin) - Added exclusive reset connection node `/close_connection` in routes. - Added `reset()` method to Video-Server class for manually resetting timestamp clock. - Added `reset_enabled` flag to keep check on reloads. - 🐛 Fixed premature webpage auto-reloading. - Added additional imports. - Fixed code typos and links. 💄 WebGear_RTC Theming: - Implemented new responsive video tag scaling according to screen aspect ratios. - 🎨 Added bootstrap CSS properties to implement auto-scaling. - Removed old `resize()` hack. - Beautify files syntax and updated files checksum. - ♻️ Refactored files and removed redundant code. - Bumped theme version to `v0.1.2` --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +- .github/ISSUE_TEMPLATE/question.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/config.yml | 10 +-- vidgear/gears/asyncio/webgear_rtc.py | 111 +++++++++++++++++++++------ 5 files changed, 96 insertions(+), 35 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e3f8d3a50..44fd156b5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,8 +27,8 @@ _Kindly explain the issue here._ - [ ] I have searched the [issues](https://github.com/abhiTronix/vidgear/issues) for my issue and found nothing related or helpful. -- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear). -- [ ] I have read the [Issue Guidelines](https://abhitronix.github.io/vidgear/contribution/issue/#submitting-an-issue-guidelines). +- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear/latest). +- [ ] I have read the [Issue Guidelines](https://abhitronix.github.io/vidgear/latest/contribution/issue/#submitting-an-issue-guidelines). ### Environment diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 5c4239f17..3da69a789 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -19,7 +19,7 @@ _Kindly describe the issue here._ - [ ] I have searched the [issues](https://github.com/abhiTronix/vidgear/issues) for my issue and found nothing related or helpful. - [ ] I have read the [FAQs](https://abhitronix.github.io/vidgear/latest/help/get_help/#frequently-asked-questions). -- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear). +- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear/latest). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7f811e4f9..c3c1a176e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,8 +11,8 @@ _Kindly explain the changes you made here._ -- [ ] I have read the [PR Guidelines](https://abhitronix.github.io/vidgear/contribution/PR/#submitting-pull-requestpr-guidelines). -- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear). +- [ ] I have read the [PR Guidelines](https://abhitronix.github.io/vidgear/latest/contribution/PR/#submitting-pull-requestpr-guidelines). +- [ ] I have read the [Documentation](https://abhitronix.github.io/vidgear/latest). - [ ] I have updated the documentation accordingly(if required). diff --git a/.github/config.yml b/.github/config.yml index 3f8db32a9..b118fb12d 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -2,9 +2,9 @@ newPRWelcomeComment: | Thanks so much for opening your first PR here, a maintainer will get back to you shortly! ### In the meantime: - - Read our [Pull Request(PR) Guidelines](https://abhitronix.github.io/vidgear/contribution/PR/#submitting-pull-requestpr-guidelines) for submitting a valid PR for VidGear. - - Submit a [issue](https://abhitronix.github.io/vidgear/contribution/issue/#submitting-an-issue-guidelines) beforehand for your Pull Request. - - Go briefly through our [PR FAQ section](https://abhitronix.github.io/vidgear/contribution/PR/#frequently-asked-questions). + - Read our [Pull Request(PR) Guidelines](https://abhitronix.github.io/vidgear/latest/contribution/PR/#submitting-pull-requestpr-guidelines) for submitting a valid PR for VidGear. + - Submit a [issue](https://abhitronix.github.io/vidgear/latest/contribution/issue/#submitting-an-issue-guidelines) beforehand for your Pull Request. + - Go briefly through our [PR FAQ section](https://abhitronix.github.io/vidgear/latest/contribution/PR/#frequently-asked-questions). firstPRMergeComment: | Congrats on merging your first pull request here! :tada: You're awesome! @@ -14,6 +14,6 @@ newIssueWelcomeComment: | Thanks for opening this issue, a maintainer will get back to you shortly! ### In the meantime: - - Read our [Issue Guidelines](https://abhitronix.github.io/vidgear/contribution/issue/#submitting-an-issue-guidelines), and update your issue accordingly. Please note that your issue will be fixed much faster if you spend about half an hour preparing it, including the exact reproduction steps and a demo. - - Go comprehensively through our dedicated [FAQ & Troubleshooting section](https://abhitronix.github.io/vidgear/help/get_help/#frequently-asked-questions). + - Read our [Issue Guidelines](https://abhitronix.github.io/vidgear/latest/contribution/issue/#submitting-an-issue-guidelines), and update your issue accordingly. Please note that your issue will be fixed much faster if you spend about half an hour preparing it, including the exact reproduction steps and a demo. + - Go comprehensively through our dedicated [FAQ & Troubleshooting section](https://abhitronix.github.io/vidgear/latest/help/get_help/#frequently-asked-questions). - For any quick questions and typos, please refrain from opening an issue, as you can reach us on [Gitter](https://gitter.im/vidgear/community) community channel. diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 12651f56d..df92c07fd 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -22,6 +22,8 @@ import os import cv2 import sys +import time +import fractions import asyncio import logging as log from collections import deque @@ -30,11 +32,16 @@ from starlette.staticfiles import StaticFiles from starlette.applications import Starlette from starlette.middleware import Middleware -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, PlainTextResponse from aiortc.rtcrtpsender import RTCRtpSender -from aiortc import RTCPeerConnection, RTCSessionDescription, VideoStreamTrack +from aiortc import ( + RTCPeerConnection, + RTCSessionDescription, + VideoStreamTrack, +) from aiortc.contrib.media import MediaRelay +from aiortc.mediastreams import MediaStreamError from av import VideoFrame @@ -47,13 +54,18 @@ from ..videogear import VideoGear # define logger -logger = log.getLogger("WeGear_RTC") +logger = log.getLogger("WebGear_RTC") if logger.hasHandlers(): logger.handlers.clear() logger.propagate = False logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) +# add global vars +VIDEO_CLOCK_RATE = 90000 +VIDEO_PTIME = 1 / 30 # 30fps +VIDEO_TIME_BASE = fractions.Fraction(1, VIDEO_CLOCK_RATE) + class RTC_VideoServer(VideoStreamTrack): """ @@ -91,7 +103,7 @@ def __init__( colorspace (str): selects the colorspace of the input stream. logging (bool): enables/disables logging. time_delay (int): time delay (in sec) before start reading the frames. - options (dict): provides ability to alter Tweak Parameters of WeGear_RTC, CamGear, PiGear & Stabilizer. + options (dict): provides ability to alter Tweak Parameters of WebGear_RTC, CamGear, PiGear & Stabilizer. """ super().__init__() # don't forget this! @@ -100,8 +112,8 @@ def __init__( self.__logging = logging self.__enable_inf = False # continue frames even when video ends. self.__frame_size_reduction = 20 # 20% reduction - self.is_running = True # check if running self.is_launched = False # check if launched already + self.__is_running = False # check if running if options: if "frame_size_reduction" in options: @@ -147,6 +159,9 @@ def __init__( # initialize blank frame self.blank_frame = None + # handles reset signal + self.__reset_enabled = False + def launch(self): """ Launches VideoGear stream @@ -154,8 +169,33 @@ def launch(self): if self.__logging: logger.debug("Launching Internal RTC Video-Server") self.is_launched = True + self.__is_running = True self.__stream.start() + async def next_timestamp(self): + """ + VideoStreamTrack internal method for generating accurate timestamp. + """ + # check if ready state not live + if self.readyState != "live": + # otherwise reset + self.stop() + if hasattr(self, "_timestamp") and not self.__reset_enabled: + self._timestamp += int(VIDEO_PTIME * VIDEO_CLOCK_RATE) + wait = self._start + (self._timestamp / VIDEO_CLOCK_RATE) - time.time() + await asyncio.sleep(wait) + else: + if self.__logging: + logger.debug( + "{} timestamps".format( + "Resetting" if self.__reset_enabled else "Setting" + ) + ) + self._start = time.time() + self._timestamp = 0 + self.__reset_enabled = False + return self._timestamp, VIDEO_TIME_BASE + async def recv(self): """ A coroutine function that yields `av.frame.Frame`. @@ -172,11 +212,13 @@ async def recv(self): # display blank if NoneType if f_stream is None: - if self.blank_frame is None or not self.is_running: + if self.blank_frame is None or not self.__is_running: return None else: f_stream = self.blank_frame[:] - if not self.__enable_inf: + if not self.__enable_inf and not self.__reset_enabled: + if self.__logging: + logger.debug("Video-Stream Ended.") self.terminate() else: # create blank @@ -199,14 +241,20 @@ async def recv(self): # return `av.frame.Frame` return frame + async def reset(self): + """ + Resets timestamp clock + """ + self.__reset_enabled = True + self.__is_running = False + def terminate(self): """ Gracefully terminates VideoGear stream """ - # log if not (self.__stream is None): # terminate running flag - self.is_running = False + self.__is_running = False self.is_launched = False if self.__logging: logger.debug("Terminating Internal RTC Video-Server") @@ -254,7 +302,7 @@ def __init__( ): """ - This constructor method initializes the object state and attributes of the WeGear_RTC class. + This constructor method initializes the object state and attributes of the WebGear_RTC class. Parameters: enablePiCamera (bool): provide access to PiGear(if True) or CamGear(if False) APIs respectively. @@ -268,14 +316,14 @@ def __init__( colorspace (str): selects the colorspace of the input stream. logging (bool): enables/disables logging. time_delay (int): time delay (in sec) before start reading the frames. - options (dict): provides ability to alter Tweak Parameters of WeGear_RTC, CamGear, PiGear & Stabilizer. + options (dict): provides ability to alter Tweak Parameters of WebGear_RTC, CamGear, PiGear & Stabilizer. """ # initialize global params self.__logging = logging custom_data_location = "" # path to save data-files to custom location - data_path = "" # path to WeGear_RTC data-files + data_path = "" # path to WebGear_RTC data-files overwrite_default = False self.__relay = None # act as broadcaster @@ -289,12 +337,12 @@ def __init__( if isinstance(value, str): assert os.access( value, os.W_OK - ), "[WeGear_RTC:ERROR] :: Permission Denied!, cannot write WeGear_RTC data-files to '{}' directory!".format( + ), "[WebGear_RTC:ERROR] :: Permission Denied!, cannot write WebGear_RTC data-files to '{}' directory!".format( value ) assert os.path.isdir( os.path.abspath(value) - ), "[WeGear_RTC:ERROR] :: `custom_data_location` value must be the path to a directory and not to a file!" + ), "[WebGear_RTC:ERROR] :: `custom_data_location` value must be the path to a directory and not to a file!" custom_data_location = os.path.abspath(value) else: logger.warning("Skipped invalid `custom_data_location` value!") @@ -347,7 +395,7 @@ def __init__( # log it if self.__logging: logger.debug( - "`{}` is the default location for saving WeGear_RTC data-files.".format( + "`{}` is the default location for saving WebGear_RTC data-files.".format( data_path ) ) @@ -395,24 +443,25 @@ def __init__( ) # define default frame generator in configuration self.config = {"server": self.__default_rtc_server} - + # add exclusive reset connection node + self.routes.append( + Route("/close_connection", self.__reset_connections, methods=["POST"]) + ) # copying original routing tables for further validation self.__rt_org_copy = self.routes[:] - # keeps check if producer loop should be running - self.__isrunning = True # collects peer RTC connections self.__pcs = set() def __call__(self): """ - Implements a custom Callable method for WeGear_RTC application. + Implements a custom Callable method for WebGear_RTC application. """ # validate routing tables assert not (self.routes is None), "Routing tables are NoneType!" if not isinstance(self.routes, list) or not all( x in self.routes for x in self.__rt_org_copy ): - raise RuntimeError("[WeGear_RTC:ERROR] :: Routing tables are not valid!") + raise RuntimeError("[WebGear_RTC:ERROR] :: Routing tables are not valid!") # validate middlewares assert not (self.middleware is None), "Middlewares are NoneType!" @@ -420,9 +469,9 @@ def __call__(self): not isinstance(self.middleware, list) or not all(isinstance(x, Middleware) for x in self.middleware) ): - raise RuntimeError("[WeGear_RTC:ERROR] :: Middlewares are not valid!") + raise RuntimeError("[WebGear_RTC:ERROR] :: Middlewares are not valid!") - # validate assigned RTC video-server in WeGear_RTC configuration + # validate assigned RTC video-server in WebGear_RTC configuration if isinstance(self.config, dict) and "server" in self.config: # check if assigned RTC server class is inherit from `VideoStreamTrack` API.i if self.config["server"] is None or not issubclass( @@ -430,7 +479,7 @@ def __call__(self): ): # otherwise raise error raise ValueError( - "[WeGear_RTC:ERROR] :: Invalid configuration. {}. Refer Docs for more information!".format( + "[WebGear_RTC:ERROR] :: Invalid configuration. {}. Refer Docs for more information!".format( "Video-Server not assigned" if self.config["server"] is None else "Assigned Video-Server class must be inherit from `aiortc.VideoStreamTrack` only" @@ -443,12 +492,12 @@ def __call__(self): ): # otherwise raise error raise ValueError( - "[WeGear_RTC:ERROR] :: Invalid configuration. Assigned Video-Server Class must have `terminate` method defined. Refer Docs for more information!" + "[WebGear_RTC:ERROR] :: Invalid configuration. Assigned Video-Server Class must have `terminate` method defined. Refer Docs for more information!" ) else: # raise error if validation fails raise RuntimeError( - "[WeGear_RTC:ERROR] :: Assigned configuration is invalid!" + "[WebGear_RTC:ERROR] :: Assigned configuration is invalid!" ) # return Starlette application if self.__logging: @@ -542,6 +591,18 @@ async def __server_error(self, request, exc): "500.html", {"request": request}, status_code=500 ) + async def __reset_connections(self, request): + """ + Resets all connections and recreates VideoServer timestamps + """ + logger.critical("Resetting Server") + # collects peer RTC connections + coros = [pc.close() for pc in self.__pcs] + await asyncio.gather(*coros) + self.__pcs.clear() + await self.__default_rtc_server.reset() + return PlainTextResponse("OK") + async def __on_shutdown(self): """ Implements a Callable to be run on application shutdown From e8df8b5f22997b7d1b881ded77baeaa40b480c04 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 16 Jun 2021 10:00:40 +0530 Subject: [PATCH 038/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Added=20test=20for?= =?UTF-8?q?=20testing=20WebGear=5FRTC=20API=20against=20Webpage=20reload?= =?UTF-8?q?=20disruptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asyncio_tests/test_webgear_rtc.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) 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 b44190403..97811ed84 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -344,6 +344,71 @@ def test_webgear_rtc_options(options): pytest.fail(str(e)) +def test_webpage_reload(): + """ + Test for testing WebGear_RTC API against Webpage reload + disruptions + """ + try: + # initialize webgear_rtc + web = WebGear_RTC(source=return_testvideo_path(), logging=True) + client = TestClient(web(), raise_server_exceptions=True) + response = client.get("/") + assert response.status_code == 200 + + # create offer and receive + (offer_pc, data) = get_RTCPeer_payload() + response_rtc_answer = client.post( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + params = response_rtc_answer.json() + answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) + run(offer_pc.setRemoteDescription(answer)) + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + + # simulate webpage reload + response_rtc_reload = client.post( + "/close_connection", + data="1", + ) + logger.debug(response_rtc_reload.text) + assert response_rtc_reload.text == "OK", "Test Failed!" + # close offer + run(offer_pc.close()) + offer_pc = None + data = None + + # recreate offer and continue receive + (offer_pc, data) = get_RTCPeer_payload() + response_rtc_answer = client.post( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + params = response_rtc_answer.json() + answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) + run(offer_pc.setRemoteDescription(answer)) + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + + # shutdown + run(offer_pc.close()) + web.shutdown() + except Exception as e: + pytest.fail(str(e)) + + test_data_class = [ (None, False), ("Invalid", False), From 4cf1539483040cd10e89b63a180710921f3f2bdb Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 16 Jun 2021 10:44:13 +0530 Subject: [PATCH 039/112] =?UTF-8?q?=F0=9F=90=9B=20WebGear=5FRTC:=20Disable?= =?UTF-8?q?=20webpage=20reload=20behavior=20handling=20for=20Live=20broadc?= =?UTF-8?q?asting.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/asyncio/webgear_rtc.py | 18 ++++++----- .../asyncio_tests/test_webgear_rtc.py | 31 ++++++++++++++----- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index df92c07fd..311531a71 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -595,13 +595,17 @@ async def __reset_connections(self, request): """ Resets all connections and recreates VideoServer timestamps """ - logger.critical("Resetting Server") - # collects peer RTC connections - coros = [pc.close() for pc in self.__pcs] - await asyncio.gather(*coros) - self.__pcs.clear() - await self.__default_rtc_server.reset() - return PlainTextResponse("OK") + # check if `enable_infinite_frames` is enabled + if self.__relay is None: + logger.critical("Resetting Server") + # collects peer RTC connections + coros = [pc.close() for pc in self.__pcs] + await asyncio.gather(*coros) + self.__pcs.clear() + await self.__default_rtc_server.reset() + return PlainTextResponse("OK") + else: + return PlainTextResponse("DISABLED") async def __on_shutdown(self): """ 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 97811ed84..f5422f291 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -344,14 +344,26 @@ def test_webgear_rtc_options(options): pytest.fail(str(e)) -def test_webpage_reload(): +test_data = [ + { + "frame_size_reduction": 40, + }, + { + "enable_live_broadcast": True, + "frame_size_reduction": 40, + }, +] + + +@pytest.mark.parametrize("options", test_data) +def test_webpage_reload(options): """ Test for testing WebGear_RTC API against Webpage reload disruptions """ + web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: - # initialize webgear_rtc - web = WebGear_RTC(source=return_testvideo_path(), logging=True) + # run webgear_rtc client = TestClient(web(), raise_server_exceptions=True) response = client.get("/") assert response.status_code == 200 @@ -378,12 +390,13 @@ def test_webpage_reload(): "/close_connection", data="1", ) - logger.debug(response_rtc_reload.text) - assert response_rtc_reload.text == "OK", "Test Failed!" # close offer run(offer_pc.close()) offer_pc = None data = None + # verify response + logger.debug(response_rtc_reload.text) + assert response_rtc_reload.text == "OK", "Test Failed!" # recreate offer and continue receive (offer_pc, data) = get_RTCPeer_payload() @@ -404,9 +417,13 @@ def test_webpage_reload(): # shutdown run(offer_pc.close()) - web.shutdown() except Exception as e: - pytest.fail(str(e)) + if "enable_live_broadcast" in options and isinstance(e, AssertionError): + pytest.xfail("Test Passed") + else: + pytest.fail(str(e)) + finally: + web.shutdown() test_data_class = [ From 3555dd79e52aedd3018447146866831f3870ab41 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 16 Jun 2021 11:22:36 +0530 Subject: [PATCH 040/112] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20=20WebGear=5FRTC:?= =?UTF-8?q?=20Fixed=20code=20typos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/asyncio/webgear_rtc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 311531a71..2e31ff767 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -595,7 +595,7 @@ async def __reset_connections(self, request): """ Resets all connections and recreates VideoServer timestamps """ - # check if `enable_infinite_frames` is enabled + # check if Live Broadcasting is enabled if self.__relay is None: logger.critical("Resetting Server") # collects peer RTC connections @@ -605,6 +605,7 @@ async def __reset_connections(self, request): await self.__default_rtc_server.reset() return PlainTextResponse("OK") else: + # if does, then do nothing return PlainTextResponse("DISABLED") async def __on_shutdown(self): From 26904bd2520f17c923b2196dd7bd8c0aa1c3b72a Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 16 Jun 2021 22:45:35 +0530 Subject: [PATCH 041/112] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20=20WebGear=5FRTC:?= =?UTF-8?q?=20Called=20shutdown=20on=20any=20failed=20ICE=20connection.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gears/webgear/usage.md | 4 +--- docs/gears/webgear_rtc/advanced.md | 6 +++--- docs/gears/webgear_rtc/usage.md | 28 +++++++++++++++++++++++++--- vidgear/gears/asyncio/webgear_rtc.py | 5 ++--- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/docs/gears/webgear/usage.md b/docs/gears/webgear/usage.md index 53281dedc..91b0960ae 100644 --- a/docs/gears/webgear/usage.md +++ b/docs/gears/webgear/usage.md @@ -85,7 +85,7 @@ Let's implement our Bare-Minimum usage example with these [**Performance Enhanci You can access and run WebGear VideoStreamer Server programmatically in your python script in just a few lines of code, as follows: -!!! tip "For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../../help/webgear_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear)" +!!! tip "For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../help/webgear_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear)" ```python @@ -133,8 +133,6 @@ which can also be accessed on any browser on the network at http://localhost:800 You can run `#!py3 python3 -m vidgear.gears.asyncio -h` help command to see all the advanced settings, as follows: - !!! warning "If you're using `--options/-op` flag, then kindly wrap your dictionary value in single `''` quotes." - ```sh usage: python -m vidgear.gears.asyncio [-h] [-m MODE] [-s SOURCE] [-ep ENABLEPICAMERA] [-S STABILIZE] [-cn CAMERA_NUM] [-yt stream_mode] [-b BACKEND] [-cs COLORSPACE] diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index f53a53060..e36038092 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -34,7 +34,7 @@ Let's implement a bare-minimum example using WebGear_RTC as Real-time Broadcaste !!! info "[`enable_infinite_frames`](../params/#webgear_rtc-specific-attributes) is enforced by default with this(`enable_live_broadcast`) attribute." -!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" +!!! tip "For accessing WebGear_RTC on different Client Devices on the network, we use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" ```python # import required libraries @@ -50,8 +50,8 @@ options = { # initialize WebGear_RTC app web = WebGear_RTC(source="foo.mp4", logging=True, **options) -# run this app on Uvicorn server at address http://localhost:8000/ -uvicorn.run(web(), host="localhost", port=8000) +# run this app on Uvicorn server at address http://0.0.0.0:8000/ +uvicorn.run(web(), host="0.0.0.0", port=8000) # close app safely web.shutdown() diff --git a/docs/gears/webgear_rtc/usage.md b/docs/gears/webgear_rtc/usage.md index bce579ac5..e576512eb 100644 --- a/docs/gears/webgear_rtc/usage.md +++ b/docs/gears/webgear_rtc/usage.md @@ -31,6 +31,30 @@ WebGear_RTC API is the part of `asyncio` package of VidGear, thereby you need to pip install vidgear[asyncio] ``` +### Aiortc + +Must Required only if you're using [WebGear_RTC API](../../gears/webgear_rtc/overview/). You can easily install it via pip: + +??? error "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: + + !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." + + - Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). + - Alternative link to Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019). + - Offline installer: [vs_buildtools.exe](https://aka.ms/vs/16/release/vs_buildtools.exe) + + Afterwards, Select: Workloads → Desktop development with C++, then for Individual Components, select only: + + - [x] Windows 10 SDK + - [x] C++ x64/x86 build tools + + Finally, proceed installing `aiortc` via pip. + +```sh + pip install aiortc +``` ### ASGI Server @@ -48,7 +72,7 @@ Let's implement a Bare-Minimum usage example: You can access and run WebGear_RTC VideoStreamer Server programmatically in your python script in just a few lines of code, as follows: -!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" +!!! tip "For accessing WebGear_RTC on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. More information can be found [here ➶](../../../help/webgear_rtc_faqs/#is-it-possible-to-stream-on-a-different-device-on-the-network-with-webgear_rtc)" !!! info "We are using `frame_size_reduction` attribute for frame size reduction _(in percentage)_ to be streamed with its [`options`](../params/#options) dictionary parameter to cope with performance-throttling in this example." @@ -94,8 +118,6 @@ which can also be accessed on any browser on the network at http://localhost:800 You can run `#!py3 python3 -m vidgear.gears.asyncio -h` help command to see all the advanced settings, as follows: - !!! warning "If you're using `--options/-op` flag, then kindly wrap your dictionary value in single `''` quotes." - ```sh usage: python -m vidgear.gears.asyncio [-h] [-m MODE] [-s SOURCE] [-ep ENABLEPICAMERA] [-S STABILIZE] [-cn CAMERA_NUM] [-yt stream_mode] [-b BACKEND] [-cs COLORSPACE] diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 2e31ff767..828d0a449 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -538,9 +538,8 @@ async def __offer(self, request): async def on_iceconnectionstatechange(): logger.debug("ICE connection state is %s" % pc.iceConnectionState) if pc.iceConnectionState == "failed": - logger.error("ICE connection state failed!") - await pc.close() - self.__pcs.discard(pc) + logger.error("ICE connection failed. Exiting!") + await self.__on_shutdown() # Change the remote description associated with the connection. await pc.setRemoteDescription(offer) From d78668ebb8ab89f1954ae8405589b8d3c2ba047b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 17 Jun 2021 10:05:54 +0530 Subject: [PATCH 042/112] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20WebGear=5FRTC:?= =?UTF-8?q?=20Code=20Refractored?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Improved handling of failed ICE connection. - Fixed web-page reloading bug after stream ended. - Made `is_running` variable globally available for internal use. - Disable reload CI test on Windows machines due to random failures. - Reverted previous commit. - Fixed logging comments. --- vidgear/gears/asyncio/webgear_rtc.py | 31 ++++++++++++------- .../asyncio_tests/test_webgear_rtc.py | 2 ++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 828d0a449..0a257b32c 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -113,7 +113,7 @@ def __init__( self.__enable_inf = False # continue frames even when video ends. self.__frame_size_reduction = 20 # 20% reduction self.is_launched = False # check if launched already - self.__is_running = False # check if running + self.is_running = False # check if running if options: if "frame_size_reduction" in options: @@ -169,7 +169,7 @@ def launch(self): if self.__logging: logger.debug("Launching Internal RTC Video-Server") self.is_launched = True - self.__is_running = True + self.is_running = True self.__stream.start() async def next_timestamp(self): @@ -193,7 +193,9 @@ async def next_timestamp(self): ) self._start = time.time() self._timestamp = 0 - self.__reset_enabled = False + if self.__reset_enabled: + self.__reset_enabled = False + self.is_running = True return self._timestamp, VIDEO_TIME_BASE async def recv(self): @@ -212,7 +214,7 @@ async def recv(self): # display blank if NoneType if f_stream is None: - if self.blank_frame is None or not self.__is_running: + if self.blank_frame is None or not self.is_running: return None else: f_stream = self.blank_frame[:] @@ -246,7 +248,7 @@ async def reset(self): Resets timestamp clock """ self.__reset_enabled = True - self.__is_running = False + self.is_running = False def terminate(self): """ @@ -254,8 +256,7 @@ def terminate(self): """ if not (self.__stream is None): # terminate running flag - self.__is_running = False - self.is_launched = False + self.is_running = False if self.__logging: logger.debug("Terminating Internal RTC Video-Server") # terminate @@ -365,7 +366,7 @@ def __init__( "enable_infinite_frames" ] = True # enforce infinite frames logger.critical( - "Enabled live broadcasting with emulated infinite frames." + "Enabled live broadcasting for Peer connection(s)." ) else: None @@ -538,8 +539,12 @@ async def __offer(self, request): async def on_iceconnectionstatechange(): logger.debug("ICE connection state is %s" % pc.iceConnectionState) if pc.iceConnectionState == "failed": - logger.error("ICE connection failed. Exiting!") - await self.__on_shutdown() + logger.error("ICE connection state failed.") + # check if Live Broadcasting is enabled + if self.__relay is None: + # if not, close connection. + await pc.close() + self.__pcs.discard(pc) # Change the remote description associated with the connection. await pc.setRemoteDescription(offer) @@ -595,7 +600,11 @@ async def __reset_connections(self, request): Resets all connections and recreates VideoServer timestamps """ # check if Live Broadcasting is enabled - if self.__relay is None: + if ( + self.__relay is None + and not (self.__default_rtc_server is None) + and (self.__default_rtc_server.is_running) + ): logger.critical("Resetting Server") # collects peer RTC connections coros = [pc.close() for pc in self.__pcs] 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 f5422f291..b03b928fb 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -23,6 +23,7 @@ import cv2 import pytest import asyncio +import platform import logging as log import requests import tempfile @@ -355,6 +356,7 @@ def test_webgear_rtc_options(options): ] +@pytest.mark.skipif((platform.system() == "Windows"), reason="Random Failures!") @pytest.mark.parametrize("options", test_data) def test_webpage_reload(options): """ From 5c982366c787ed18abbaadf04ab1a93e356c21dc Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 21 Jun 2021 10:18:58 +0530 Subject: [PATCH 043/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Minor=20content?= =?UTF-8?q?=20changes=20&=20fixed=20context=20and=20typos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- docs/bonus/TQM.md | 4 +- docs/changelog.md | 718 +++++++++--------- docs/contribution.md | 2 +- docs/gears.md | 2 +- docs/gears/camgear/params.md | 6 +- docs/gears/camgear/usage.md | 5 +- docs/gears/pigear/usage.md | 68 +- docs/gears/streamgear/introduction.md | 11 +- docs/gears/streamgear/rtfm/overview.md | 7 +- .../writegear/compression/advanced/cciw.md | 2 +- docs/gears/writegear/compression/usage.md | 53 +- docs/help/camgear_faqs.md | 2 +- docs/help/general_faqs.md | 2 +- docs/index.md | 4 +- docs/installation/pip_install.md | 40 +- docs/switch_from_cv.md | 2 +- mkdocs.yml | 5 +- 18 files changed, 534 insertions(+), 403 deletions(-) diff --git a/README.md b/README.md index 49590fcba..250111d90 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ limitations under the License.   -VidGear is a **High-Performance Video Processing Python Library** that provides an easy-to-use, highly extensible, thoroughly optimised **Multi-Threaded + Asyncio Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* serving at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering **robust error-handling and real-time performance 🔥** +VidGear is a **High-Performance Video Processing Python Library** that provides an easy-to-use, highly extensible, thoroughly optimised **Multi-Threaded + Asyncio API Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* serving at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering **robust error-handling and real-time performance 🔥** VidGear primarily focuses on simplicity, and thereby lets programmers and software developers to easily integrate and perform Complex Video Processing Tasks, in just a few lines of code. @@ -85,7 +85,7 @@ The following **functional block diagram** clearly depicts the generalized funct #### What is vidgear? -> *"VidGear is a High-Performance Framework that provides an one-stop **Video-Processing** solution for building complex real-time media applications in python."* +> *"VidGear is a cross-platform High-Performance Framework that provides an one-stop **Video-Processing** solution for building complex real-time media applications in python."* #### What does it do? diff --git a/docs/bonus/TQM.md b/docs/bonus/TQM.md index 78957ad23..0351c5a36 100644 --- a/docs/bonus/TQM.md +++ b/docs/bonus/TQM.md @@ -27,7 +27,7 @@ limitations under the License.
Threaded-Queue-Mode: generalized timing diagram
-> Threaded Queue Mode is designed exclusively for VidGear's Videocapture Gears _(namely CamGear, ScreenGear, VideoGear)_ and few Network Gears _(such as NetGear(Client's end))_ for achieving high-performance, synchronized, and error-free video-frames handling. +> Threaded Queue Mode is designed exclusively for VidGear's Videocapture Gears _(namely CamGear, ScreenGear, VideoGear)_ and few Network Gears _(such as NetGear(Client's end))_ for achieving high-performance, asynchronous, error-free video-frames handling. !!! tip "Threaded-Queue-Mode is enabled by default, but [can be disabled](#manually-disabling-threaded-queue-mode), only if extremely necessary." @@ -37,7 +37,7 @@ limitations under the License. ## What does Threaded-Queue-Mode exactly do? -Threaded-Queue-Mode helps VidGear do the Threaded Video-Processing tasks in synchronized, well-organized, and most competent way possible: +Threaded-Queue-Mode helps VidGear do the Threaded Video-Processing tasks in highly optimized, well-organized, and most competent way possible: ### A. Enables Multi-Threading diff --git a/docs/changelog.md b/docs/changelog.md index 9194da776..b8d77c64b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -24,62 +24,62 @@ limitations under the License. ??? tip "New Features" - [x] **NetGear:** - * [x] New SSH Tunneling Mode for connecting ZMQ sockets across machines via SSH tunneling. - * [x] Added new `ssh_tunnel_mode` attribute to enable ssh tunneling at provide address at server end only. - * [x] Implemented new `check_open_port` helper method to validate availability of host at given open port. - * [x] Added new attributes `ssh_tunnel_keyfile` and `ssh_tunnel_pwd` to easily validate ssh connection. - * [x] Extended this feature to be compatible with bi-directional mode and auto-reconnection. - * [x] Initially disabled support for exclusive Multi-Server and Multi-Clients modes. - * [x] Implemented logic to automatically enable `paramiko` support if installed. - * [x] Reserved port-47 for testing. + * New SSH Tunneling Mode for connecting ZMQ sockets across machines via SSH tunneling. + * Added new `ssh_tunnel_mode` attribute to enable ssh tunneling at provide address at server end only. + * Implemented new `check_open_port` helper method to validate availability of host at given open port. + * Added new attributes `ssh_tunnel_keyfile` and `ssh_tunnel_pwd` to easily validate ssh connection. + * Extended this feature to be compatible with bi-directional mode and auto-reconnection. + * Initially disabled support for exclusive Multi-Server and Multi-Clients modes. + * Implemented logic to automatically enable `paramiko` support if installed. + * Reserved port-47 for testing. - [x] **WebGear_RTC:** - * [x] Added native support for middlewares. - * [x] Added new global `middleware` variable for easily defining Middlewares as list. - * [x] Added validity check for Middlewares. - * [x] Added tests for middlewares support. - * [x] Added example for middlewares support. - * [x] Added related imports. + * Added native support for middlewares. + * Added new global `middleware` variable for easily defining Middlewares as list. + * Added validity check for Middlewares. + * Added tests for middlewares support. + * Added example for middlewares support. + * Added related imports. - [x] **CI:** - * [x] Added new `no-response` work-flow for stale issues. - * [x] Added NetGear CI Tests - * [x] Added new CI tests for SSH Tunneling Mode. - * [x] Added "paramiko" to CI dependencies. + * Added new `no-response` work-flow for stale issues. + * Added NetGear CI Tests + * Added new CI tests for SSH Tunneling Mode. + * Added "paramiko" to CI dependencies. - [x] **Docs:** - * [x] Added Zenodo DOI badge and its reference in BibTex citations. - * [x] Added `pymdownx.striphtml` plugin for stripping comments. - * [x] Added complete docs for SSH Tunneling Mode. - * [x] Added complete docs for NetGear's SSH Tunneling Mode. - * [x] Added new usage example and related information. - * [x] Added new image assets for ssh tunneling example. - * [x] New admonitions and beautified css + * Added Zenodo DOI badge and its reference in BibTex citations. + * Added `pymdownx.striphtml` plugin for stripping comments. + * Added complete docs for SSH Tunneling Mode. + * Added complete docs for NetGear's SSH Tunneling Mode. + * Added new usage example and related information. + * Added new image assets for ssh tunneling example. + * New admonitions and beautified css ??? success "Updates/Improvements" - [x] Added exception for RunTimeErrors in NetGear CI tests. - [x] Extended Middlewares support to WebGear API too. - [x] Docs: - * [x] Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. - * [x] Re-positioned few docs comments at bottom for easier detection during stripping. - * [x] Updated dark theme to `dark orange`. - * [x] Updated fonts to `Source Sans Pro`. - * [x] Fixed missing heading in VideoGear. - * [x] Update setup.py update link for assets. - * [x] Added missing StreamGear Code docs. - * [x] Several minor tweaks and typos fixed. - * [x] Updated 404 page and workflow. - * [x] Updated README.md and mkdocs.yml with new additions. - * [x] Re-written Threaded-Queue-Mode from scratch with elaborated functioning. - * [x] Replace Paypal with Liberpay in FUNDING.yml - * [x] Updated FFmpeg Download links. - * [x] Restructured docs. - * [x] Updated mkdocs.yml. + * Added `extra.homepage` parameter, which allows for setting a dedicated URL for `site_url`. + * Re-positioned few docs comments at bottom for easier detection during stripping. + * Updated dark theme to `dark orange`. + * Updated fonts to `Source Sans Pro`. + * Fixed missing heading in VideoGear. + * Update setup.py update link for assets. + * Added missing StreamGear Code docs. + * Several minor tweaks and typos fixed. + * Updated 404 page and workflow. + * Updated README.md and mkdocs.yml with new additions. + * Re-written Threaded-Queue-Mode from scratch with elaborated functioning. + * Replace Paypal with Liberpay in FUNDING.yml + * Updated FFmpeg Download links. + * Restructured docs. + * Updated mkdocs.yml. - [x] Helper: - * [x] Implemented new `delete_file_safe` to safely delete files at given path. - * [x] Replaced `os.remove` calls with `delete_file_safe`. + * Implemented new `delete_file_safe` to safely delete files at given path. + * Replaced `os.remove` calls with `delete_file_safe`. - [x] CI: - * [x] Updated VidGear Docs Deployer Workflow - * [x] Updated test + * Updated VidGear Docs Deployer Workflow + * Updated test - [x] Updated issue templates and labels. ??? danger "Breaking Updates/Changes" @@ -88,17 +88,17 @@ limitations under the License. ??? bug "Bug-fixes" - [x] Critical Bugfix related to OpenCV Binaries import. - * [x] Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. - * [x] Replaced `packaging.parse_version` with more robust `distutils.version`. - * [x] Removed redundant imports. + * Bug fixed for OpenCV import comparsion test failing with Legacy versions and throwing ImportError. + * Replaced `packaging.parse_version` with more robust `distutils.version`. + * Removed redundant imports. - [x] Setup: - * [x] Removed `latest_version` variable support from `simplejpeg`. - * [x] Fixed minor typos in dependencies. + * Removed `latest_version` variable support from `simplejpeg`. + * Fixed minor typos in dependencies. - [x] Setup_cfg: Replaced dashes with underscores to remove warnings. - [x] Docs: - * [x] Fixed 404 page does not work outside the site root with mkdocs. - * [x] Fixed markdown files comments not stripped when converted to HTML. - * [x] Fixed typos + * Fixed 404 page does not work outside the site root with mkdocs. + * Fixed markdown files comments not stripped when converted to HTML. + * Fixed typos ??? question "Pull Requests" @@ -115,42 +115,42 @@ limitations under the License. ??? tip "New Features" - [x] **WebGear_RTC:** - * [x] A new API that is similar to WeGear API in all aspects but utilizes WebRTC standard instead of Motion JPEG for streaming. - * [x] Now it is possible to share data and perform teleconferencing peer-to-peer, without requiring that the user install plugins or any other third-party software. - * [x] Added a flexible backend for `aiortc` - a python library for Web Real-Time Communication (WebRTC). - * [x] Integrated all functionality and parameters of WebGear into WebGear_RTC API. - * [x] Implemented JSON Response with a WebRTC Peer Connection of Video Server. - * [x] Added a internal `RTC_VideoServer` server on WebGear_RTC, a inherit-class to aiortc's VideoStreamTrack API. - * [x] New Standalone UI Default theme v0.1.1 for WebGear_RTC from scratch without using 3rd-party assets. (by @abhiTronix) - * [x] New `custom.js` and `custom.css` for custom responsive behavior. - * [x] Added WebRTC support to `custom.js` and ensured compatibility with WebGear_RTC. - * [x] Added example support for ICE framework and STUN protocol like WebRTC features to `custom.js`. - * [x] Added `resize()` function to `custom.js` to automatically adjust `video` & `img` tags for smaller screens. - * [x] Added WebGear_RTC support in main.py for easy access through terminal using `--mode` flag. - * [x] Integrated all WebGear_RTC enhancements to WebGear Themes. - * [x] Added CI test for WebGear_RTC. - * [x] Added complete docs for WebGear_RTC API. - * [x] Added bare-minimum as well as advanced examples usage code. - * [x] Added new theme images. - * [x] Added Reference and FAQs. + * A new API that is similar to WeGear API in all aspects but utilizes WebRTC standard instead of Motion JPEG for streaming. + * Now it is possible to share data and perform teleconferencing peer-to-peer, without requiring that the user install plugins or any other third-party software. + * Added a flexible backend for `aiortc` - a python library for Web Real-Time Communication (WebRTC). + * Integrated all functionality and parameters of WebGear into WebGear_RTC API. + * Implemented JSON Response with a WebRTC Peer Connection of Video Server. + * Added a internal `RTC_VideoServer` server on WebGear_RTC, a inherit-class to aiortc's VideoStreamTrack API. + * New Standalone UI Default theme v0.1.1 for WebGear_RTC from scratch without using 3rd-party assets. (by @abhiTronix) + * New `custom.js` and `custom.css` for custom responsive behavior. + * Added WebRTC support to `custom.js` and ensured compatibility with WebGear_RTC. + * Added example support for ICE framework and STUN protocol like WebRTC features to `custom.js`. + * Added `resize()` function to `custom.js` to automatically adjust `video` & `img` tags for smaller screens. + * Added WebGear_RTC support in main.py for easy access through terminal using `--mode` flag. + * Integrated all WebGear_RTC enhancements to WebGear Themes. + * Added CI test for WebGear_RTC. + * Added complete docs for WebGear_RTC API. + * Added bare-minimum as well as advanced examples usage code. + * Added new theme images. + * Added Reference and FAQs. - [x] **CamGear API:** - * [x] New Improved Pure-Python Multiple-Threaded Implementation: + * New Improved Pure-Python Multiple-Threaded Implementation: + Optimized Threaded-Queue-Mode Performance. (PR by @bml1g12) + Replaced regular `queue.full` checks followed by sleep with implicit sleep with blocking `queue.put`. + Replaced regular `queue.empty` checks followed by queue. + Replaced `nowait_get` with a blocking `queue.get` natural empty check. + Up-to 2x performance boost than previous implementations. - * [x] New `THREAD_TIMEOUT` attribute to prevent deadlocks: + * New `THREAD_TIMEOUT` attribute to prevent deadlocks: + Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. + Updated CI Tests and docs. - [x] **WriteGear API:** - * [x] New more robust handling of default video-encoder in compression mode: + * New more robust handling of default video-encoder in compression mode: + Implemented auto-switching of default video-encoder automatically based on availability. + API now selects Default encoder based on priority: `"libx264" > "libx265" > "libxvid" > "mpeg4"`. + Added `get_supported_vencoders` Helper method to enumerate Supported Video Encoders. + Added common handler for `-c:v` and `-vcodec` flags. - [x] **NetGear API:** - * [x] New Turbo-JPEG compression with simplejpeg + * New Turbo-JPEG compression with simplejpeg + Implemented JPEG compression algorithm for 4-5% performance boost at cost of minor loss in quality. + Utilized `encode_jpeg` and `decode_jpeg` methods to implement turbo-JPEG transcoding with `simplejpeg`. + Added options to control JPEG frames quality, enable fastest dct, fast upsampling to boost performance. @@ -158,13 +158,13 @@ limitations under the License. + Enabled fast dct by default with JPEG frames at 90%. + Added Docs for JPEG Frame Compression. - [x] **WebGear API:** - * [x] New modular and flexible configuration for Custom Sources: + * New modular and flexible configuration for Custom Sources: + Implemented more convenient approach for handling custom source configuration. + Added new `config` global variable for this new behavior. + Now None-type `source` parameter value is allowed for defining own custom sources. + Added new Example case and Updates Docs for this feature. + Added new CI Tests. - * [x] New Browser UI Updates: + * New Browser UI Updates: + New Standalone UI Default theme v0.1.0 for browser (by @abhiTronix) + Completely rewritten theme from scratch with only local resources. + New `custom.js` and `custom.css` for custom responsive behavior. @@ -173,144 +173,144 @@ limitations under the License. + Removed all third-party theme dependencies. + Update links to new github server `abhiTronix/vidgear-vitals` + Updated docs with new theme's screenshots. - * [x] Added `enable_infinite_frames` attribute for enabling infinite frames. - * [x] Added New modular and flexible configuration for Custom Sources. - * [x] Bumped WebGear Theme Version to v0.1.1. - * [x] Updated Docs and CI tests. + * Added `enable_infinite_frames` attribute for enabling infinite frames. + * Added New modular and flexible configuration for Custom Sources. + * Bumped WebGear Theme Version to v0.1.1. + * Updated Docs and CI tests. - [x] **ScreenGear API:** - * [x] Implemented Improved Pure-Python Multiple-Threaded like CamGear. - * [x] Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. + * Implemented Improved Pure-Python Multiple-Threaded like CamGear. + * Added support for `THREAD_TIMEOUT` attribute to its `options` parameter. - [X] **StreamGear API:** - * [x] Enabled pseudo live-streaming flag `re` for live content. + * Enabled pseudo live-streaming flag `re` for live content. - [x] **Docs:** - * [x] Added new native docs versioning to mkdocs-material. - * [x] Added new examples and few visual tweaks. - * [x] Updated Stylesheet for versioning. - * [x] Added new DASH video chunks at https://github.com/abhiTronix/vidgear-docs-additionals for StreamGear and Stabilizer streams. - * [x] Added open-sourced "Tears of Steel" * [x] project Mango Teaser video chunks. - * [x] Added open-sourced "Subspace Video Stabilization" http://web.cecs.pdx.edu/~fliu/project/subspace_stabilization/ video chunks. - * [x] Added support for DASH Video Thumbnail preview in Clappr within `custom.js`. - * [x] Added responsive clappr DASH player with bootstrap's `embed-responsive`. - * [x] Added new permalink icon and slugify to toc. - * [x] Added "back-to-top" button for easy navigation. + * Added new native docs versioning to mkdocs-material. + * Added new examples and few visual tweaks. + * Updated Stylesheet for versioning. + * Added new DASH video chunks at https://github.com/abhiTronix/vidgear-docs-additionals for StreamGear and Stabilizer streams. + * Added open-sourced "Tears of Steel" * project Mango Teaser video chunks. + * Added open-sourced "Subspace Video Stabilization" http://web.cecs.pdx.edu/~fliu/project/subspace_stabilization/ video chunks. + * Added support for DASH Video Thumbnail preview in Clappr within `custom.js`. + * Added responsive clappr DASH player with bootstrap's `embed-responsive`. + * Added new permalink icon and slugify to toc. + * Added "back-to-top" button for easy navigation. - [x] **Helper:** - * [x] New GitHub Mirror with latest Auto-built FFmpeg Static Binaries: + * New GitHub Mirror with latest Auto-built FFmpeg Static Binaries: + Replaced new GitHub Mirror `abhiTronix/FFmpeg-Builds` in helper.py + New CI maintained Auto-built FFmpeg Static Binaries. + Removed all 3rd-party and old links for better compatibility and Open-Source reliability. + Updated Related CI tests. - * [x] Added auto-font-scaling for `create_blank_frame` method. - * [x] Added `c_name` parameter to `generate_webdata` and `download_webdata` to specify class. - * [x] A more robust Implementation of Downloading Artifacts: + * Added auto-font-scaling for `create_blank_frame` method. + * Added `c_name` parameter to `generate_webdata` and `download_webdata` to specify class. + * A more robust Implementation of Downloading Artifacts: + Added a custom HTTP `TimeoutHTTPAdapter` Adapter with a default timeout for all HTTP calls based on [this GitHub comment](). + Implemented http client and the `send()` method to ensure that the default timeout is used if a timeout argument isn't provided. + Implemented Requests session`with` block to exit properly even if there are unhandled exceptions. + Add a retry strategy to custom `TimeoutHTTPAdapter` Adapter with max 3 retries and sleep(`backoff_factor=1`) between failed requests. - * [x] Added `create_blank_frame` method to create bland frames with suitable text. + * Added `create_blank_frame` method to create bland frames with suitable text. - [x] **[CI] Continuous Integration:** - * [x] Added new fake frame generated for fake `picamera` class with numpy. - * [x] Added new `create_bug` parameter to fake `picamera` class for emulating various artificial bugs. - * [x] Added float/int instance check on `time_delay` for camgear and pigear. - * [x] Added `EXIT_CODE` to new timeout implementation for pytests to upload codecov report when no timeout. - * [x] Added auxiliary classes to fake `picamera` for facilitating the emulation. - * [x] Added new CI tests for PiGear Class for testing on all platforms. - * [x] Added `shutdown()` function to gracefully terminate WebGear_RTC API. - * [x] Added new `coreutils` brew dependency. - * [x] Added handler for variable check on exit and codecov upload. - * [x] Added `is_running` flag to WebGear_RTC to exit safely. + * Added new fake frame generated for fake `picamera` class with numpy. + * Added new `create_bug` parameter to fake `picamera` class for emulating various artificial bugs. + * Added float/int instance check on `time_delay` for camgear and pigear. + * Added `EXIT_CODE` to new timeout implementation for pytests to upload codecov report when no timeout. + * Added auxiliary classes to fake `picamera` for facilitating the emulation. + * Added new CI tests for PiGear Class for testing on all platforms. + * Added `shutdown()` function to gracefully terminate WebGear_RTC API. + * Added new `coreutils` brew dependency. + * Added handler for variable check on exit and codecov upload. + * Added `is_running` flag to WebGear_RTC to exit safely. - [x] **Setup:** - * [x] New automated latest version retriever for packages: + * New automated latest version retriever for packages: + Implemented new `latest_version` method to automatically retrieve latest version for packages. + Added Some Dependencies. - * [x] Added `simplejpeg` package for all platforms. + * Added `simplejpeg` package for all platforms. ??? success "Updates/Improvements" - [x] Added exception for RunTimeErrors in NetGear CI tests. - [x] WriteGear: Critical file write access checking method: - * [x] Added new `check_WriteAccess` Helper method. - * [x] Implemented a new robust algorithm to check if given directory has write-access. - * [x] Removed old behavior which gives irregular results. + * Added new `check_WriteAccess` Helper method. + * Implemented a new robust algorithm to check if given directory has write-access. + * Removed old behavior which gives irregular results. - [x] Helper: Maintenance Updates - * [x] Added workaround for Python bug. - * [x] Added `safe_mkdir` to `check_WriteAccess` to automatically create non-existential parent folder in path. - * [x] Extended `check_WriteAccess` Patch to StreamGear. - * [x] Simplified `check_WriteAccess` to handle Windows envs easily. - * [x] Updated FFmpeg Static Download URL for WriteGear. - * [x] Implemented fallback option for auto-calculating bitrate from extracted audio sample-rate in `validate_audio` method. + * Added workaround for Python bug. + * Added `safe_mkdir` to `check_WriteAccess` to automatically create non-existential parent folder in path. + * Extended `check_WriteAccess` Patch to StreamGear. + * Simplified `check_WriteAccess` to handle Windows envs easily. + * Updated FFmpeg Static Download URL for WriteGear. + * Implemented fallback option for auto-calculating bitrate from extracted audio sample-rate in `validate_audio` method. - [x] Docs: General UI Updates - * [x] Updated Meta tags for og site and twitter cards. - * [x] Replaced Custom dark theme toggle with mkdocs-material's official Color palette toggle - * [x] Added example for external audio input and creating segmented MP4 video in WriteGear FAQ. - * [x] Added example for YouTube streaming with WriteGear. - * [x] Removed custom `dark-material.js` and `header.html` files from theme. - * [x] Added blogpost link for detailed information on Stabilizer Working. - * [x] Updated `mkdocs.yml` and `custom.css` configuration. - * [x] Remove old hack to resize clappr DASH player with css. - * [x] Updated Admonitions. - * [x] Improved docs contexts. - * [x] Updated CSS for version-selector-button. - * [x] Adjusted files to match new themes. - * [x] Updated welcome-bot message for typos. - * [x] Removed redundant FAQs from NetGear Docs. - * [x] Updated Assets Images. - * [x] Updated spacing. + * Updated Meta tags for og site and twitter cards. + * Replaced Custom dark theme toggle with mkdocs-material's official Color palette toggle + * Added example for external audio input and creating segmented MP4 video in WriteGear FAQ. + * Added example for YouTube streaming with WriteGear. + * Removed custom `dark-material.js` and `header.html` files from theme. + * Added blogpost link for detailed information on Stabilizer Working. + * Updated `mkdocs.yml` and `custom.css` configuration. + * Remove old hack to resize clappr DASH player with css. + * Updated Admonitions. + * Improved docs contexts. + * Updated CSS for version-selector-button. + * Adjusted files to match new themes. + * Updated welcome-bot message for typos. + * Removed redundant FAQs from NetGear Docs. + * Updated Assets Images. + * Updated spacing. - [x] CI: - * [x] Removed unused `github.ref` from yaml. - * [x] Updated OpenCV Bash Script for Linux envs. - * [x] Added `timeout-minutes` flag to github-actions workflow. - * [x] Added `timeout` flag to pytest. - * [x] Replaced Threaded Gears with OpenCV VideoCapture API. - * [x] Moved files and Removed redundant code. - * [x] Replaced grayscale frames with color frames for WebGear tests. - * [x] Updated pytest timeout value to 15mins. - * [x] Removed `aiortc` automated install on Windows platform within setup.py. - * [x] Added new timeout logic to continue to run on external timeout for GitHub Actions Workflows. - * [x] Removed unreliable old timeout solution from WebGear_RTC. - * [x] Removed `timeout_decorator` and `asyncio_timeout` dependencies for CI. - * [x] Removed WebGear_RTC API exception from codecov. - * [x] Implemented new fake `picamera` class to CI utils for emulating RPi Camera-Module Real-time capabilities. - * [x] Implemented new `get_RTCPeer_payload` method to receive WebGear_RTC peer payload. - * [x] Removed PiGear from Codecov exceptions. - * [x] Disable Frame Compression in few NetGear tests failing on frame matching. - * [x] Updated NetGear CI tests to support new attributes - * [x] Removed warnings and updated yaml + * Removed unused `github.ref` from yaml. + * Updated OpenCV Bash Script for Linux envs. + * Added `timeout-minutes` flag to github-actions workflow. + * Added `timeout` flag to pytest. + * Replaced Threaded Gears with OpenCV VideoCapture API. + * Moved files and Removed redundant code. + * Replaced grayscale frames with color frames for WebGear tests. + * Updated pytest timeout value to 15mins. + * Removed `aiortc` automated install on Windows platform within setup.py. + * Added new timeout logic to continue to run on external timeout for GitHub Actions Workflows. + * Removed unreliable old timeout solution from WebGear_RTC. + * Removed `timeout_decorator` and `asyncio_timeout` dependencies for CI. + * Removed WebGear_RTC API exception from codecov. + * Implemented new fake `picamera` class to CI utils for emulating RPi Camera-Module Real-time capabilities. + * Implemented new `get_RTCPeer_payload` method to receive WebGear_RTC peer payload. + * Removed PiGear from Codecov exceptions. + * Disable Frame Compression in few NetGear tests failing on frame matching. + * Updated NetGear CI tests to support new attributes + * Removed warnings and updated yaml + Added `pytest.ini` to address multiple warnings. + Updated azure workflow condition syntax. - * [x] Update `mike` settings for mkdocs versioning. - * [x] Updated codecov configurations. - * [x] Minor logging and docs updates. - * [x] Implemented pytest timeout for azure pipelines for macOS envs. - * [x] Added `aiortc` as external dependency in `appveyor.yml`. - * [x] Re-implemented WebGear_RTC improper offer-answer handshake in CI tests. - * [x] WebGear_RTC CI Updated with `VideoTransformTrack` to test stream play. - * [x] Implemented fake `AttributeError` for fake picamera class. - * [x] Updated PiGear CI tests to increment codecov. - * [x] Update Tests docs and other minor tweaks to increase overall coverage. - * [x] Enabled debugging and disabled exit 1 on error in azure pipeline. - * [x] Removed redundant benchmark tests. + * Update `mike` settings for mkdocs versioning. + * Updated codecov configurations. + * Minor logging and docs updates. + * Implemented pytest timeout for azure pipelines for macOS envs. + * Added `aiortc` as external dependency in `appveyor.yml`. + * Re-implemented WebGear_RTC improper offer-answer handshake in CI tests. + * WebGear_RTC CI Updated with `VideoTransformTrack` to test stream play. + * Implemented fake `AttributeError` for fake picamera class. + * Updated PiGear CI tests to increment codecov. + * Update Tests docs and other minor tweaks to increase overall coverage. + * Enabled debugging and disabled exit 1 on error in azure pipeline. + * Removed redundant benchmark tests. - [x] Helper: Added missing RSTP URL scheme to `is_valid_url` method. - [x] NetGear_Async: Added fix for uvloop only supporting python>=3.7 legacies. - [x] Extended WebGear's Video-Handler scope to `https`. - [x] CI: Remove all redundant 32-bit Tests from Appveyor: - * [x] Appveyor 32-bit Windows envs are actually running on 64-bit machines. - * [x] More information here: https://help.appveyor.com/discussions/questions/20637-is-it-possible-to-force-running-tests-on-both-32-bit-and-64-bit-windows + * Appveyor 32-bit Windows envs are actually running on 64-bit machines. + * More information here: https://help.appveyor.com/discussions/questions/20637-is-it-possible-to-force-running-tests-on-both-32-bit-and-64-bit-windows - [x] Setup: Removed `latest_version` behavior from some packages. - [x] NetGear_Async: Revised logic for handling uvloop for all platforms and legacies. - [x] Setup: Updated logic to install uvloop-"v0.14.0" for python-3.6 legacies. - [x] Removed any redundant code from webgear. - [x] StreamGear: - * [x] Replaced Ordinary dict with Ordered Dict to use `move_to_end` method. - * [x] Moved external audio input to output parameters dict. - * [x] Added additional imports. - * [x] Updated docs to reflect changes. + * Replaced Ordinary dict with Ordered Dict to use `move_to_end` method. + * Moved external audio input to output parameters dict. + * Added additional imports. + * Updated docs to reflect changes. - [x] Numerous Updates to Readme and `mkdocs.yml`. - [x] Updated font to `FONT_HERSHEY_SCRIPT_COMPLEX` and enabled logging in create_blank_frame. - [x] Separated channels for downloading and storing theme files for WebGear and WebGear_RTC APIs. - [x] Removed `logging` condition to always inform user in a event of FFmpeg binary download failure. - [x] WebGear_RTC: - * [x] Improved auto internal termination. - * [x] More Performance updates through `setCodecPreferences`. - * [x] Moved default Video RTC video launcher to `__offer`. + * Improved auto internal termination. + * More Performance updates through `setCodecPreferences`. + * Moved default Video RTC video launcher to `__offer`. - [x] NetGear_Async: Added timeout to client in CI tests. - [x] Reimplemented and updated `changelog.md`. - [x] Updated code comments. @@ -330,37 +330,37 @@ limitations under the License. - [x] NetGear_Async: Fixed `source` parameter missing `None` as default value. - [x] Fixed uvloops only supporting python>=3.7 in NetGear_Async. - [x] Helper: - * [x] Fixed Zombie processes in `check_output` method due a hidden bug in python. For reference: https://bugs.python.org/issue37380 - * [x] Fixed regex in `validate_video` method. + * Fixed Zombie processes in `check_output` method due a hidden bug in python. For reference: https://bugs.python.org/issue37380 + * Fixed regex in `validate_video` method. - [x] Docs: - * [x] Invalid `site_url` bug patched in mkdocs.yml - * [x] Remove redundant mike theme support and its files. - * [x] Fixed video not centered when DASH video in fullscreen mode with clappr. - * [x] Fixed Incompatible new mkdocs-docs theme. - * [x] Fixed missing hyperlinks. + * Invalid `site_url` bug patched in mkdocs.yml + * Remove redundant mike theme support and its files. + * Fixed video not centered when DASH video in fullscreen mode with clappr. + * Fixed Incompatible new mkdocs-docs theme. + * Fixed missing hyperlinks. - [x] CI: - * [x] Fixed NetGear Address bug - * [x] Fixed bugs related to termination in WebGear_RTC. - * [x] Fixed random CI test failures and code cleanup. - * [x] Fixed string formating bug in Helper.py. - * [x] Fixed F821 undefined name bugs in WebGear_RTC tests. - * [x] NetGear_Async Tests fixes. - * [x] Fixed F821 undefined name bugs. - * [x] Fixed typo bugs in `main.py`. - * [x] Fixed Relative import bug in PiGear. - * [x] Fixed regex bug in warning filter. - * [x] Fixed WebGear_RTC frozen threads on exit. - * [x] Fixed bugs in codecov bash uploader setting for azure pipelines. - * [x] Fixed False-positive `picamera` import due to improper sys.module settings. - * [x] Fixed Frozen Threads on exit in WebGear_RTC API. - * [x] Fixed deploy error in `VidGear Docs Deployer` workflow - * [x] Fixed low timeout bug. - * [x] Fixed bugs in PiGear tests. - * [x] Patched F821 undefined name bug. + * Fixed NetGear Address bug + * Fixed bugs related to termination in WebGear_RTC. + * Fixed random CI test failures and code cleanup. + * Fixed string formating bug in Helper.py. + * Fixed F821 undefined name bugs in WebGear_RTC tests. + * NetGear_Async Tests fixes. + * Fixed F821 undefined name bugs. + * Fixed typo bugs in `main.py`. + * Fixed Relative import bug in PiGear. + * Fixed regex bug in warning filter. + * Fixed WebGear_RTC frozen threads on exit. + * Fixed bugs in codecov bash uploader setting for azure pipelines. + * Fixed False-positive `picamera` import due to improper sys.module settings. + * Fixed Frozen Threads on exit in WebGear_RTC API. + * Fixed deploy error in `VidGear Docs Deployer` workflow + * Fixed low timeout bug. + * Fixed bugs in PiGear tests. + * Patched F821 undefined name bug. - [x] StreamGear: - * [x] Fixed StreamGear throwing `Picture size 0x0 is invalid` bug with external audio. - * [x] Fixed default input framerate value getting discarded in Real-time Frame Mode. - * [x] Fixed internal list-formatting bug. + * Fixed StreamGear throwing `Picture size 0x0 is invalid` bug with external audio. + * Fixed default input framerate value getting discarded in Real-time Frame Mode. + * Fixed internal list-formatting bug. - [x] Fixed E999 SyntaxError bug in `main.py`. - [x] Fixed Typo in bash script. - [x] Fixed WebGear freeze on reloading bug. @@ -389,20 +389,20 @@ limitations under the License. ??? tip "New Features" - [x] **CamGear API:** - * [x] Support for various Live-Video-Streaming services: + * Support for various Live-Video-Streaming services: + Added seamless support for live video streaming sites like Twitch, LiveStream, Dailymotion etc. + Implemented flexible framework around `streamlink` python library with easy control over parameters and quality. + Stream Mode can now automatically detects whether `source` belong to YouTube or elsewhere, and handles it with appropriate API. - * [x] Re-implemented YouTube URLs Handler: + * Re-implemented YouTube URLs Handler: + Re-implemented CamGear's YouTube URLs Handler completely from scratch. + New Robust Logic to flexibly handing video and video-audio streams. + Intelligent stream selector for selecting best possible stream compatible with OpenCV. + Added support for selecting stream qualities and parameters. + Implemented new `get_supported_quality` helper method for handling specified qualities + Fixed Live-Stream URLs not supported by OpenCV's Videocapture and its FFmpeg. - * [x] Added additional `STREAM_QUALITY` and `STREAM_PARAMS` attributes. + * Added additional `STREAM_QUALITY` and `STREAM_PARAMS` attributes. - [x] **ScreenGear API:** - * [x] Multiple Backends Support: + * Multiple Backends Support: + Added new multiple backend support with new [`pyscreenshot`](https://github.com/ponty/pyscreenshot) python library. + Made `pyscreenshot` the default API for ScreenGear, replaces `mss`. + Added new `backend` parameter for this feature while retaining previous behavior. @@ -413,90 +413,90 @@ limitations under the License. + Updated ScreenGear Docs. + Updated ScreenGear CI tests. - [X] **StreamGear API:** - * [x] Changed default behaviour to support complete video transcoding. - * [x] Added `-livestream` attribute to support live-streaming. - * [x] Added additional parameters for `-livestream` attribute functionality. - * [x] Updated StreamGear Tests. - * [x] Updated StreamGear docs. + * Changed default behaviour to support complete video transcoding. + * Added `-livestream` attribute to support live-streaming. + * Added additional parameters for `-livestream` attribute functionality. + * Updated StreamGear Tests. + * Updated StreamGear docs. - [x] **Stabilizer Class:** - * [x] New Robust Error Handling with Blank Frames: + * New Robust Error Handling with Blank Frames: + Elegantly handles all crashes due to Empty/Blank/Dark frames. + Stabilizer throws Warning with this new behavior instead of crashing. + Updated CI test for this feature. - [x] **Docs:** - * [x] Automated Docs Versioning: + * Automated Docs Versioning: + Implemented Docs versioning through `mike` API. + Separate new workflow steps to handle different versions. + Updated docs deploy worflow to support `release` and `dev` builds. + Added automatic version extraction from github events. + Added `version-select.js` and `version-select.css` files. - * [x] Toggleable Dark-White Docs Support: + * Toggleable Dark-White Docs Support: + Toggle-button to easily switch dark, white and preferred theme. + New Updated Assets for dark backgrounds + New css, js files/content to implement this behavior. + New material icons for button. + Updated scheme to `slate` in `mkdocs.yml`. - * [x] New Theme and assets: + * New Theme and assets: + New `purple` theme with `dark-purple` accent color. + New images assets with updated transparent background. + Support for both dark and white theme. + Increased `rebufferingGoal` for dash videos. + New updated custom 404 page for docs. - * [x] Issue and PR automated-bots changes + * Issue and PR automated-bots changes + New `need_info.yml` YAML Workflow. + New `needs-more-info.yml` Request-Info template. + Replaced Request-Info templates. + Improved PR and Issue welcome formatting. - * [x] Added custom HTML pages. - * [x] Added `show_root_heading` flag to disable headings in References. - * [x] Added new `inserAfter` function to version-select.js. - * [x] Adjusted hue for dark-theme for better contrast. - * [x] New usage examples and FAQs. - * [x] Added `gitmoji` for commits. + * Added custom HTML pages. + * Added `show_root_heading` flag to disable headings in References. + * Added new `inserAfter` function to version-select.js. + * Adjusted hue for dark-theme for better contrast. + * New usage examples and FAQs. + * Added `gitmoji` for commits. - [x] **Continuous Integration:** - * [x] Maintenance Updates: + * Maintenance Updates: + Added support for new `VIDGEAR_LOGFILE` environment variable in Travis CI. + Added missing CI tests. + Added logging for helper functions. - * [x] Azure-Pipeline workflow for MacOS envs + * Azure-Pipeline workflow for MacOS envs + Added Azure-Pipeline Workflow for testing MacOS environment. + Added codecov support. - * [x] GitHub Actions workflow for Linux envs + * GitHub Actions workflow for Linux envs + Added GitHub Action work-flow for testing Linux environment. - * [x] New YAML to implement GitHub Action workflow for python 3.6, 3.7, 3,8 & 3.9 matrices. - * [x] Added Upload coverage to Codecov GitHub Action workflow. - * [x] New codecov-bash uploader for Azure Pipelines. + * New YAML to implement GitHub Action workflow for python 3.6, 3.7, 3,8 & 3.9 matrices. + * Added Upload coverage to Codecov GitHub Action workflow. + * New codecov-bash uploader for Azure Pipelines. - [x] **Logging:** - * [x] Added file support + * Added file support + Added `VIDGEAR_LOGFILE` environment variable to manually add file/dir path. + Reworked `logger_handler()` Helper methods (in asyncio too). + Added new formatter and Filehandler for handling logger files. - * [x] Added `restore_levelnames` auxiliary method for restoring logging levelnames. + * Added `restore_levelnames` auxiliary method for restoring logging levelnames. - [x] Added auto version extraction from package `version.py` in setup.py. ??? success "Updates/Improvements" - [x] Added missing Lazy-pirate auto-reconnection support for Multi-Servers and Multi-Clients Mode in NetGear API. - [x] Added new FFmpeg test path to Bash-Script and updated README broken links. - [x] Asset Cleanup: - * [x] Removed all third-party javascripts from projects. - * [x] Linked all third-party javascript directly. - * [x] Cleaned up necessary code from CSS and JS files. - * [x] Removed any copyrighted material or links. + * Removed all third-party javascripts from projects. + * Linked all third-party javascript directly. + * Cleaned up necessary code from CSS and JS files. + * Removed any copyrighted material or links. - [x] Rewritten Docs from scratch: - * [x] Improved complete docs formatting. - * [x] Simplified language for easier understanding. - * [x] Fixed `mkdocstrings` showing root headings. - * [x] Included all APIs methods to `mkdocstrings` docs. - * [x] Removed unnecessary information from docs. - * [x] Corrected Spelling and typos. - * [x] Fixed context and grammar. - * [x] Removed `motivation.md`. - * [x] Renamed many terms. - * [x] Fixed hyper-links. - * [x] Reformatted missing or improper information. - * [x] Fixed context and spellings in Docs files. - * [x] Simplified language for easy understanding. - * [x] Updated image sizes for better visibility. + * Improved complete docs formatting. + * Simplified language for easier understanding. + * Fixed `mkdocstrings` showing root headings. + * Included all APIs methods to `mkdocstrings` docs. + * Removed unnecessary information from docs. + * Corrected Spelling and typos. + * Fixed context and grammar. + * Removed `motivation.md`. + * Renamed many terms. + * Fixed hyper-links. + * Reformatted missing or improper information. + * Fixed context and spellings in Docs files. + * Simplified language for easy understanding. + * Updated image sizes for better visibility. - [x] Bash Script: Updated to Latest OpenCV Binaries version and related changes - [x] Docs: Moved version-selector to header and changed default to alias. - [x] Docs: Updated `deploy_docs.yml` for releasing dev, stable, and release versions. @@ -560,23 +560,23 @@ limitations under the License. ??? tip "New Features" - [x] **StreamGear API:** - * [x] New API that automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats. - * [x] Implemented multi-platform , standalone, highly extensible and flexible wrapper around FFmpeg for generating chunked-encoded media segments of the media, and easily accessing almost all of its parameters. - * [x] API automatically transcodes videos/audio files & real-time frames into a sequence of multiple smaller chunks/segments and also creates a Manifest file. - * [x] Added initial support for [MPEG-DASH](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_. - * [x] Constructed default behavior in StreamGear, for auto-creating a Primary Stream of same resolution and framerate as source. - * [x] Added [TQDM](https://github.com/tqdm/tqdm) progress bar in non-debugged output for visual representation of internal processes. - * [x] Implemented several internal methods for preprocessing FFmpeg and internal parameters for producing streams. - * [x] Several standalone internal checks to ensure robust performance. - * [x] New `terminate()` function to terminate StremGear Safely. - * [x] New StreamGear Dual Modes of Operation: + * New API that automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats. + * Implemented multi-platform , standalone, highly extensible and flexible wrapper around FFmpeg for generating chunked-encoded media segments of the media, and easily accessing almost all of its parameters. + * API automatically transcodes videos/audio files & real-time frames into a sequence of multiple smaller chunks/segments and also creates a Manifest file. + * Added initial support for [MPEG-DASH](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_. + * Constructed default behavior in StreamGear, for auto-creating a Primary Stream of same resolution and framerate as source. + * Added [TQDM](https://github.com/tqdm/tqdm) progress bar in non-debugged output for visual representation of internal processes. + * Implemented several internal methods for preprocessing FFmpeg and internal parameters for producing streams. + * Several standalone internal checks to ensure robust performance. + * New `terminate()` function to terminate StremGear Safely. + * New StreamGear Dual Modes of Operation: + Implemented *Single-Source* and *Real-time Frames* like independent Transcoding Modes. + Linked `-video_source` attribute for activating these modes + **Single-Source Mode**, transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller segments for streaming + **Real-time Frames Mode**, directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller segments for streaming + Added separate functions, `stream()` for Real-time Frame Mode and `transcode_source()` for Single-Source Mode for easy transcoding. + Included auto-colorspace detection and RGB Mode like features _(extracted from WriteGear)_, into StreamGear. - * [x] New StreamGear Parameters: + * New StreamGear Parameters: + Developed several new parameters such as: + `output`: handles assets directory + `formats`: handles adaptive HTTP streaming format. @@ -592,7 +592,7 @@ limitations under the License. + `-gop` to manually specify GOP length. + `-ffmpeg_download_path` to handle custom FFmpeg download path on windows. + `-clear_prev_assets` to remove any previous copies of SteamGear Assets. - * [x] New StreamGear docs, MPEG-DASH demo, and recommended DASH players list: + * New StreamGear docs, MPEG-DASH demo, and recommended DASH players list: + Added new StreamGear docs, usage examples, parameters, references, new FAQs. + Added Several StreamGear usage examples w.r.t Mode of Operation. + Implemented [**Clappr**](https://github.com/clappr/clappr) based on [**Shaka-Player**](https://github.com/google/shaka-player), as Demo Player. @@ -603,65 +603,65 @@ limitations under the License. + Recommended tested Online, Command-line and GUI Adaptive Stream players. + Implemented separate FFmpeg installation doc for StreamGear API. + Reduced `rebufferingGoal` for faster response. - * [x] New StreamGear CI tests: + * New StreamGear CI tests: + Added IO and API initialization CI tests for its Modes. + Added various mode Streaming check CI tests. - [x] **NetGear_Async API:** - * [x] Added new `send_terminate_signal` internal method. - * [x] Added `WindowsSelectorEventLoopPolicy()` for windows 3.8+ envs. - * [x] Moved Client auto-termination to separate method. - * [x] Implemented graceful termination with `signal` API on UNIX machines. - * [x] Added new `timeout` attribute for controlling Timeout in Connections. - * [x] Added missing termination optimizer (`linger=0`) flag. - * [x] Several ZMQ Optimizer Flags added to boost performance. + * Added new `send_terminate_signal` internal method. + * Added `WindowsSelectorEventLoopPolicy()` for windows 3.8+ envs. + * Moved Client auto-termination to separate method. + * Implemented graceful termination with `signal` API on UNIX machines. + * Added new `timeout` attribute for controlling Timeout in Connections. + * Added missing termination optimizer (`linger=0`) flag. + * Several ZMQ Optimizer Flags added to boost performance. - [x] **WriteGear API:** - * [x] Added support for adding duplicate FFmpeg parameters to `output_params`: + * Added support for adding duplicate FFmpeg parameters to `output_params`: + Added new `-clones` attribute in `output_params` parameter for handing this behavior.. + Support to pass FFmpeg parameters as list, while maintaining the exact order it was specified. + Built support for `zmq.REQ/zmq.REP` and `zmq.PUB/zmq.SUB` patterns in this mode. + Added new CI tests debugging this behavior. + Updated docs accordingly. - * [x] Added support for Networks URLs in Compression Mode: + * Added support for Networks URLs in Compression Mode: + `output_filename` parameter supports Networks URLs in compression modes only + Added automated handling of non path/file Networks URLs as input. + Implemented new `is_valid_url` helper method to easily validate assigned URLs value. + Validates whether the given URL value has scheme/protocol supported by assigned/installed ffmpeg or not. + WriteGear will throw `ValueError` if `-output_filename` is not supported. + Added related CI tests and docs. - * [x] Added `disable_force_termination` attribute in WriteGear to disable force-termination. + * Added `disable_force_termination` attribute in WriteGear to disable force-termination. - [x] **NetGear API:** - * [x] Added option to completely disable Native Frame-Compression: + * Added option to completely disable Native Frame-Compression: + Checks if any Incorrect/Invalid value is assigned on `compression_format` attribute. + Completely disables Native Frame-Compression. + Updated docs accordingly. - [x] **CamGear API:** - * [x] Added new and robust regex for identifying YouTube URLs. - * [x] Moved `youtube_url_validator` to Helper. + * Added new and robust regex for identifying YouTube URLs. + * Moved `youtube_url_validator` to Helper. - [x] **New `helper.py` methods:** - * [x] Added `validate_video` function to validate video_source. - * [x] Added `extract_time` Extract time from give string value. - * [x] Added `get_video_bitrate` to calculate video birate from resolution, framerate, bits-per-pixels values. - * [x] Added `delete_safe` to safely delete files of given extension. - * [x] Added `validate_audio` to validate audio source. - * [x] Added new Helper CI tests. + * Added `validate_video` function to validate video_source. + * Added `extract_time` Extract time from give string value. + * Added `get_video_bitrate` to calculate video birate from resolution, framerate, bits-per-pixels values. + * Added `delete_safe` to safely delete files of given extension. + * Added `validate_audio` to validate audio source. + * Added new Helper CI tests. + Added new `check_valid_mpd` function to test MPD files validity. + Added `mpegdash` library to CI requirements. - [x] **Deployed New Docs Upgrades:** - * [x] Added new assets like _images, gifs, custom scripts, javascripts fonts etc._ for achieving better visual graphics in docs. - * [x] Added `clappr.min.js`, `dash-shaka-playback.js`, `clappr-level-selector.min.js` third-party javascripts locally. - * [x] Extended Overview docs Hyperlinks to include all major sub-pages _(such as Usage Examples, Reference, FAQs etc.)_. - * [x] Replaced GIF with interactive MPEG-DASH Video Example in Stabilizer Docs. - * [x] Added new `pymdownx.keys` to replace `[Ctrl+C]/[⌘+C]` formats. - * [x] Added new `custom.css` stylescripts variables for fluid animations in docs. - * [x] Overridden announce bar and added donation button. - * [x] Lossless WEBP compressed all PNG assets for faster loading. - * [x] Enabled lazy-loading for GIFS and Images for performance. - * [x] Reimplemented Admonitions contexts and added new ones. - * [x] Added StreamGear and its different modes Docs Assets. - * [x] Added patch for images & unicodes for PiP flavored markdown in `setup.py`. + * Added new assets like _images, gifs, custom scripts, javascripts fonts etc._ for achieving better visual graphics in docs. + * Added `clappr.min.js`, `dash-shaka-playback.js`, `clappr-level-selector.min.js` third-party javascripts locally. + * Extended Overview docs Hyperlinks to include all major sub-pages _(such as Usage Examples, Reference, FAQs etc.)_. + * Replaced GIF with interactive MPEG-DASH Video Example in Stabilizer Docs. + * Added new `pymdownx.keys` to replace `[Ctrl+C]/[⌘+C]` formats. + * Added new `custom.css` stylescripts variables for fluid animations in docs. + * Overridden announce bar and added donation button. + * Lossless WEBP compressed all PNG assets for faster loading. + * Enabled lazy-loading for GIFS and Images for performance. + * Reimplemented Admonitions contexts and added new ones. + * Added StreamGear and its different modes Docs Assets. + * Added patch for images & unicodes for PiP flavored markdown in `setup.py`. - [x] **Added `Request Info` and `Welcome` GitHub Apps to automate PR and issue workflow** - * [x] Added new `config.yml` for customizations. - * [x] Added various suitable configurations. + * Added new `config.yml` for customizations. + * Added various suitable configurations. - [x] Added new `-clones` attribute to handle FFmpeg parameter clones in StreamGear and WriteGear API. - [x] Added new Video-only and Audio-Only sources in bash script. - [x] Added new paths in bash script for storing StreamGear & WriteGear assets temporarily. @@ -776,13 +776,13 @@ limitations under the License. ??? tip "New Features" - [x] **NetGear API:** - * [x] Multiple Clients support: + * Multiple Clients support: + Implemented support for handling any number of Clients simultaneously with a single Server in this mode. + Added new `multiclient_mode` attribute for enabling this mode easily. + Built support for `zmq.REQ/zmq.REP` and `zmq.PUB/zmq.SUB` patterns in this mode. + Implemented ability to receive data from all Client(s) along with frames with `zmq.REQ/zmq.REP` pattern only. + Updated related CI tests - * [x] Support for robust Lazy Pirate pattern(auto-reconnection) in NetGear API for both server and client ends: + * Support for robust Lazy Pirate pattern(auto-reconnection) in NetGear API for both server and client ends: + Implemented a algorithm where NetGear rather than doing a blocking receive, will now: + Poll the socket and receive from it only when it's sure a reply has arrived. + Attempt to reconnect, if no reply has arrived within a timeout period. @@ -791,32 +791,32 @@ limitations under the License. + Added new `max_retries` and `request_timeout`(in seconds) for handling polling. + Added `DONTWAIT` flag for interruption-free data receiving. + Both Server and Client can now reconnect even after a premature termination. - * [x] Performance Updates: + * Performance Updates: + Added default Frame Compression support for Bidirectional frame transmission in Bidirectional mode. + Added support for `Reducer()` function in Helper.py to aid reducing frame-size on-the-go for more performance. + Added small delay in `recv()` function at client's end to reduce system load. + Reworked and Optimized NetGear termination, and also removed/changed redundant definitions and flags. - [x] **Docs: Migration to Mkdocs** - * [x] Implemented a beautiful, static documentation site based on [MkDocs](https://www.mkdocs.org/) which will then be hosted on GitHub Pages. - * [x] Crafted base mkdocs with third-party elegant & simplistic [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/) theme. - * [x] Implemented new `mkdocs.yml` for Mkdocs with relevant data. - * [x] Added new `docs` folder to handle markdown pages and its assets. - * [x] Added new Markdown pages(`.md`) to docs folder, which are carefully crafted documents - [x] based on previous Wiki's docs, and some completely new additions. - * [x] Added navigation under tabs for easily accessing each document. - * [x] New Assets: + * Implemented a beautiful, static documentation site based on [MkDocs](https://www.mkdocs.org/) which will then be hosted on GitHub Pages. + * Crafted base mkdocs with third-party elegant & simplistic [`mkdocs-material`](https://squidfunk.github.io/mkdocs-material/) theme. + * Implemented new `mkdocs.yml` for Mkdocs with relevant data. + * Added new `docs` folder to handle markdown pages and its assets. + * Added new Markdown pages(`.md`) to docs folder, which are carefully crafted documents - [x] based on previous Wiki's docs, and some completely new additions. + * Added navigation under tabs for easily accessing each document. + * New Assets: + Added new assets like _gifs, images, custom scripts, favicons, site.webmanifest etc._ for bringing standard and quality to docs visual design. + Designed brand new logo and banner for VidGear Documents. + Deployed all assets under separate [*Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License*](https://creativecommons.org/licenses/by-nc-sa/4.0/). - * [x] Added Required Plugins and Extensions: + * Added Required Plugins and Extensions: + Added support for all [pymarkdown-extensions](https://facelessuser.github.io/pymdown-extensions/). + Added support for some important `admonition`, `attr_list`, `codehilite`, `def_list`, `footnotes`, `meta`, and `toc` like Mkdocs extensions. + Enabled `search`, `minify` and `git-revision-date-localized` plugins support. + Added various VidGear's social links to yaml. + Added support for `en` _(English)_ language. - * [x] Auto-Build API Reference with `mkdocstrings:` + * Auto-Build API Reference with `mkdocstrings:` + Added support for [`mkdocstrings`](https://github.com/pawamoy/mkdocstrings) plugin for auto-building each VidGear's API references. + Added python handler for parsing python source-code to `mkdocstrings`. - * [x] Auto-Deploy Docs with GitHub Actions: + * Auto-Deploy Docs with GitHub Actions: + Implemented Automated Docs Deployment on gh-pages through GitHub Actions workflow. + Added new workflow yaml with minimal configuration for automated docs deployment. + Added all required python dependencies and environment for this workflow. @@ -885,38 +885,38 @@ limitations under the License. ??? tip "New Features" - [x] **WebGear API:** - * [x] Added a robust Live Video Server API that can transfer live video frames to any web browser on the network in real-time. - * [x] Implemented a flexible asyncio wrapper around [`starlette`](https://www.starlette.io/) ASGI Application Server. - * [x] Added seamless access to various starlette's Response classes, Routing tables, Static Files, Template engine(with Jinja2), etc. - * [x] Added a special internal access to VideoGear API and all its parameters. - * [x] Implemented a new Auto-Generation Work-flow to generate/download & thereby validate WebGear API data files from its GitHub server automatically. - * [x] Added on-the-go dictionary parameter in WebGear to tweak performance, Route Tables and other internal properties easily. - * [x] Added new simple & elegant default Bootstrap Cover Template for WebGear Server. - * [x] Added `__main__.py` to directly run WebGear Server through the terminal. - * [x] Added new gif and related docs for WebGear API. - * [x] Added and Updated various CI tests for this API. + * Added a robust Live Video Server API that can transfer live video frames to any web browser on the network in real-time. + * Implemented a flexible asyncio wrapper around [`starlette`](https://www.starlette.io/) ASGI Application Server. + * Added seamless access to various starlette's Response classes, Routing tables, Static Files, Template engine(with Jinja2), etc. + * Added a special internal access to VideoGear API and all its parameters. + * Implemented a new Auto-Generation Work-flow to generate/download & thereby validate WebGear API data files from its GitHub server automatically. + * Added on-the-go dictionary parameter in WebGear to tweak performance, Route Tables and other internal properties easily. + * Added new simple & elegant default Bootstrap Cover Template for WebGear Server. + * Added `__main__.py` to directly run WebGear Server through the terminal. + * Added new gif and related docs for WebGear API. + * Added and Updated various CI tests for this API. - [x] **NetGear_Async API:** - * [x] Designed NetGear_Async asynchronous network API built upon ZeroMQ's asyncio API. - * [x] Implemented support for state-of-the-art asyncio event loop [`uvloop`](https://github.com/MagicStack/uvloop) at its backend. - * [x] Achieved Unmatchable high-speed and lag-free video streaming over the network with minimal resource constraint. - * [x] Added exclusive internal wrapper around VideoGear API for this API. - * [x] Implemented complete server-client handling and options to use variable protocols/patterns for this API. - * [x] Implemented support for all four ZeroMQ messaging patterns: i.e `zmq.PAIR`, `zmq.REQ/zmq.REP`, `zmq.PUB/zmq.SUB`, and `zmq.PUSH/zmq.PULL`. - * [x] Implemented initial support for `tcp` and `ipc` protocols. - * [x] Added new Coverage CI tests for NetGear_Async Network Gear. - * [x] Added new Benchmark tests for benchmarking NetGear_Async against NetGear. + * Designed NetGear_Async asynchronous network API built upon ZeroMQ's asyncio API. + * Implemented support for state-of-the-art asyncio event loop [`uvloop`](https://github.com/MagicStack/uvloop) at its backend. + * Achieved Unmatchable high-speed and lag-free video streaming over the network with minimal resource constraint. + * Added exclusive internal wrapper around VideoGear API for this API. + * Implemented complete server-client handling and options to use variable protocols/patterns for this API. + * Implemented support for all four ZeroMQ messaging patterns: i.e `zmq.PAIR`, `zmq.REQ/zmq.REP`, `zmq.PUB/zmq.SUB`, and `zmq.PUSH/zmq.PULL`. + * Implemented initial support for `tcp` and `ipc` protocols. + * Added new Coverage CI tests for NetGear_Async Network Gear. + * Added new Benchmark tests for benchmarking NetGear_Async against NetGear. - [x] **Asynchronous Enhancements:** - * [x] Added `asyncio` package to for handling asynchronous APIs. - * [x] Moved WebGear API(webgear.py) to `asyncio` and created separate asyncio `helper.py` for it. - * [x] Various Performance tweaks for Asyncio APIs with concurrency within a single thread. - * [x] Moved `__main__.py` to asyncio for easier access to WebGear API through the terminal. - * [x] Updated `setup.py` with new dependencies and separated asyncio dependencies. + * Added `asyncio` package to for handling asynchronous APIs. + * Moved WebGear API(webgear.py) to `asyncio` and created separate asyncio `helper.py` for it. + * Various Performance tweaks for Asyncio APIs with concurrency within a single thread. + * Moved `__main__.py` to asyncio for easier access to WebGear API through the terminal. + * Updated `setup.py` with new dependencies and separated asyncio dependencies. - [x] **General Enhancements:** - * [x] Added new highly-precise Threaded FPS class for accurate benchmarking with `time.perf_counter` python module. - * [x] Added a new [Gitter](https://gitter.im/vidgear/community) community channel. - * [x] Added a new *Reducer* function to reduce the frame size on-the-go. - * [x] Add *Flake8* tests to Travis CI to find undefined names. (PR by @cclauss) - * [x] Added a new unified `logging handler` helper function for vidgear. + * Added new highly-precise Threaded FPS class for accurate benchmarking with `time.perf_counter` python module. + * Added a new [Gitter](https://gitter.im/vidgear/community) community channel. + * Added a new *Reducer* function to reduce the frame size on-the-go. + * Add *Flake8* tests to Travis CI to find undefined names. (PR by @cclauss) + * Added a new unified `logging handler` helper function for vidgear. ??? success "Updates/Improvements" - [x] Re-implemented and simplified logic for NetGear Async server-end. @@ -990,49 +990,49 @@ limitations under the License. ??? tip "New Features" - [x] **NetGear API:** - * [x] Added powerful ZMQ Authentication & Data Encryption features for NetGear API: + * Added powerful ZMQ Authentication & Data Encryption features for NetGear API: + Added exclusive `secure_mode` param for enabling it. + Added support for two most powerful `Stonehouse` & `Ironhouse` ZMQ security mechanisms. + Added smart auth-certificates/key generation and validation features. - * [x] Implemented Robust Multi-Servers support for NetGear API: + * Implemented Robust Multi-Servers support for NetGear API: + Enables Multiple Servers messaging support with a single client. + Added exclusive `multiserver_mode` param for enabling it. + Added support for `REQ/REP` & `PUB/SUB` patterns for this mode. + Added ability to send additional data of any datatype along with the frame in realtime in this mode. - * [x] Introducing exclusive Bidirectional Mode for bidirectional data transmission: + * Introducing exclusive Bidirectional Mode for bidirectional data transmission: + Added new `return_data` parameter to `recv()` function. + Added new `bidirectional_mode` attribute for enabling this mode. + Added support for `PAIR` & `REQ/REP` patterns for this mode + Added support for sending data of any python datatype. + Added support for `message` parameter for non-exclusive primary modes for this mode. - * [x] Implemented compression support with on-the-fly flexible frame encoding for the Server-end: + * Implemented compression support with on-the-fly flexible frame encoding for the Server-end: + Added initial support for `JPEG`, `PNG` & `BMP` encoding formats . + Added exclusive options attribute `compression_format` & `compression_param` to tweak this feature. + Client-end will now decode frame automatically based on the encoding as well as support decoding flags. - * [x] Added `force_terminate` attribute flag for handling force socket termination at the Server-end if there's latency in the network. - * [x] Implemented new *Publish/Subscribe(`zmq.PUB/zmq.SUB`)* pattern for seamless Live Streaming in NetGear API. + * Added `force_terminate` attribute flag for handling force socket termination at the Server-end if there's latency in the network. + * Implemented new *Publish/Subscribe(`zmq.PUB/zmq.SUB`)* pattern for seamless Live Streaming in NetGear API. - [x] **PiGear API:** - * [x] Added new threaded internal timing function for PiGear to handle any hardware failures/frozen threads. - * [x] PiGear will not exit safely with `SystemError` if Picamera ribbon cable is pulled out to save resources. - * [x] Added support for new user-defined `HWFAILURE_TIMEOUT` options attribute to alter timeout. + * Added new threaded internal timing function for PiGear to handle any hardware failures/frozen threads. + * PiGear will not exit safely with `SystemError` if Picamera ribbon cable is pulled out to save resources. + * Added support for new user-defined `HWFAILURE_TIMEOUT` options attribute to alter timeout. - [x] **VideoGear API:** - * [x] Added `framerate` global variable and removed redundant function. - * [x] Added `CROP_N_ZOOM` attribute in Videogear API for supporting Crop and Zoom stabilizer feature. + * Added `framerate` global variable and removed redundant function. + * Added `CROP_N_ZOOM` attribute in Videogear API for supporting Crop and Zoom stabilizer feature. - [x] **WriteGear API:** - * [x] Added new `execute_ffmpeg_cmd` function to pass a custom command to its FFmpeg pipeline. + * Added new `execute_ffmpeg_cmd` function to pass a custom command to its FFmpeg pipeline. - [x] **Stabilizer class:** - * [x] Added new Crop and Zoom feature. + * Added new Crop and Zoom feature. + Added `crop_n_zoom` param for enabling this feature. - * [x] Updated docs. + * Updated docs. - [x] **CI & Tests updates:** - * [x] Replaced python 3.5 matrices with latest python 3.8 matrices in Linux environment. - * [x] Added full support for **Codecov** in all CI environments. - * [x] Updated OpenCV to v4.2.0-pre(master branch). - * [x] Added various Netgear API tests. - * [x] Added initial Screengear API test. - * [x] More test RTSP feeds added with better error handling in CamGear network test. - * [x] Added tests for ZMQ authentication certificate generation. - * [x] Added badge and Minor doc updates. + * Replaced python 3.5 matrices with latest python 3.8 matrices in Linux environment. + * Added full support for **Codecov** in all CI environments. + * Updated OpenCV to v4.2.0-pre(master branch). + * Added various Netgear API tests. + * Added initial Screengear API test. + * More test RTSP feeds added with better error handling in CamGear network test. + * Added tests for ZMQ authentication certificate generation. + * Added badge and Minor doc updates. - [x] Added VidGear's official native support for MacOS environments. diff --git a/docs/contribution.md b/docs/contribution.md index 84d7c0612..88355fff2 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -54,7 +54,7 @@ There's no need to contribute for some typos. Just reach us on [Gitter ➶](http ### Found a bug? -If you encountered a bug, you can help us by submitting an 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). +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? diff --git a/docs/gears.md b/docs/gears.md index f9a3bf9b0..f3cb879c8 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -29,7 +29,7 @@ limitations under the License. VidGear is built on Standalone APIs - also known as **Gears**, each with some unique functionality. Each Gears is designed exclusively to handle/control/process different data-specific & device-specific video streams, network streams, and media encoders/decoders. -These Gears provide the user with an easy-to-use, extensible, exposed, and optimized parallel framework above many state-of-the-art libraries, while silently delivering robust error handling and unmatched real-time performance. +These Gears allows users to work with an inherently optimized, easy-to-use, extensible, and exposed API Framework over many state-of-the-art libraries, while silently delivering robust error handling and unmatched real-time performance. ## Gears Classification diff --git a/docs/gears/camgear/params.md b/docs/gears/camgear/params.md index ac6f2d0ae..bc0047b76 100644 --- a/docs/gears/camgear/params.md +++ b/docs/gears/camgear/params.md @@ -115,9 +115,9 @@ Its valid input can be one of the following: This parameter controls the Stream Mode, .i.e if enabled(`stream_mode=True`), the CamGear API will interpret the given `source` input as YouTube URL address. -!!! bug "Due to a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225) that causes video to freeze frequently in OpenCV, It is advised to always use [GStreamer backend _(`backend=cv2.CAP_GSTREAMER`)_](#backend) for any livestreams _(such as Twitch)_." +!!! bug "Due to a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225) that causes video to freeze frequently in OpenCV, It is advised to always use [GStreamer backend](#backend) for any livestreams _(such as Twitch)_." -!!! warning "CamGear automatically enforce GStreamer backend _(backend=`cv2.CAP_GSTREAMER`)_ for YouTube-livestreams!" +!!! warning "CamGear automatically enforce [GStreamer backend](#backend) for YouTube-livestreams!" !!! error "CamGear 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." @@ -160,7 +160,7 @@ CamGear(source=0, colorspace="COLOR_BGR2HSV") This parameter manually selects the backend for OpenCV's VideoCapture class _(only if specified)_. -!!! warning "To workaround a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225), CamGear automatically enforce GStreamer backend(`backend=cv2.CAP_GSTREAMER`) for YouTube-livestreams in [Stream Mode](#stream_mode). This behavior discards any `backend` parameter value for those streams." +!!! warning "To workaround a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225), CamGear automatically enforce GStreamer backend for YouTube-livestreams in [Stream Mode](#stream_mode). This behavior discards any `backend` parameter value for those streams." **Data-Type:** Integer diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index 9f6ff90c3..e15f54e07 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -68,7 +68,10 @@ stream.stop() CamGear API provides direct support for piping video streams from various popular streaming services like [Twitch](https://www.twitch.tv/), [Livestream](https://livestream.com/), [Dailymotion](https://www.dailymotion.com/live), and [many more ➶](https://streamlink.github.io/plugin_matrix.html#plugins). All you have to do is to provide the desired Video's URL to its `source` parameter, and enable the [`stream_mode`](../params/#stream_mode) parameter. The complete usage example is as follows: -!!! bug "To workaround a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225) that causes video to freeze frequently, You must always use [GStreamer backend _(`backend=cv2.CAP_GSTREAMER`)_](../params/#backend) for Livestreams _(such as Twitch URLs)_. Checkout [this FAQ ➶](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support." +!!! bug "Bug in OpenCV's FFmpeg" + To workaround a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225) that causes video to freeze frequently, You must always use [GStreamer backend](../params/#backend) for Livestreams _(such as Twitch URLs)_. + + **Checkout [this FAQ ➶](../../../help/camgear_faqs/#how-to-compile-opencv-with-gstreamer-support) for compiling OpenCV with GStreamer support.** ???+ info "Exclusive CamGear Attributes" CamGear also provides exclusive attributes: diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 95de21793..7b44e3f4d 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -204,4 +204,70 @@ cv2.destroyAllWindows() stream.stop() ``` -  \ No newline at end of file +  + +## Using PiGear with WriteGear API + +PiGear can be easily used with WriteGear API directly without any compatibility issues. The suitable example is as follows: + +```python +# import required libraries +from vidgear.gears import PiGear +from vidgear.gears import WriteGear +import cv2 + +# add various Picamera tweak parameters to dictionary +options = { + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, +} + +# define suitable (Codec,CRF,preset) FFmpeg parameters for writer +output_params = {"-vcodec": "libx264", "-crf": 0, "-preset": "fast"} + +# open pi video stream with defined parameters +stream = PiGear(resolution=(640, 480), framerate=60, logging=True, **options).start() + +# Define writer with defined parameters and suitable output filename for e.g. `Output.mp4` +writer = WriteGear(output_filename="Output.mp4", logging=True, **output_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + # lets convert frame to gray for this example + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + # write gray frame to writer + writer.write(gray) + + # 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 writer +writer.close() +``` + +  \ No newline at end of file diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index ff41d8b3e..947a6343b 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -35,7 +35,7 @@ StreamGear provides a standalone, highly extensible, and flexible wrapper around SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of fixed 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. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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 file _(such as MPD in-case of DASH)_ 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. SteamGear currently only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. @@ -55,6 +55,12 @@ SteamGear currently only supports [**MPEG-DASH**](https://www.encoding.com/mpeg- StreamGear primarily operates in following independent modes for transcoding: + +??? warning "Real-time Frames Mode is NOT Live-Streaming." + + You can enable live-streaming in Real-time Frames Mode by using using exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of stream_params dictionary parameter in WebGear_RTC API. Checkout [this usage example](../rtfm/usage/#bare-minimum-usage-with-live-streaming) for more information. + + - [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. - [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly transcodes video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. @@ -93,6 +99,8 @@ Watch StreamGear transcoded MPEG-DASH Stream: ## Recommended Players +!!! tip "Checkout out [this detailed blogpost](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works" + === "GUI Players" - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. - [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. @@ -108,6 +116,7 @@ Watch StreamGear transcoded MPEG-DASH Stream: - [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. - [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. - [x] **[MediaElementPlayer](https://github.com/mediaelement/mediaelement):** MediaElementPlayer is a complete HTML/CSS audio/video player. + - [x] **[Native MPEG-Dash + HLS Playback](https://chrome.google.com/webstore/detail/native-mpeg-dash-%20-hls-pl/cjfbmleiaobegagekpmlhmaadepdeedn?hl=en)(Chrome Extension):** Allow the browser to play HLS (m3u8) or MPEG-Dash (mpd) video urls 'natively' on chrome browsers.   diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index ba228a35e..c480b78a1 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -36,7 +36,7 @@ In this mode, StreamGear **DOES NOT** automatically maps video-source audio to g This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. -!!! warning +!!! 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 instantly result in **`RuntimeError`**! @@ -47,6 +47,11 @@ This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.g * Input framerate defaults to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined. +??? warning "Real-time Frames Mode is NOT Live-Streaming." + + You can enable live-streaming in Real-time Frames Mode by using using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of stream_params dictionary parameter in WebGear_RTC API. Checkout [this usage example](../usage/#bare-minimum-usage-with-live-streaming) for more information. + +   ## Usage Examples diff --git a/docs/gears/writegear/compression/advanced/cciw.md b/docs/gears/writegear/compression/advanced/cciw.md index 272deb4d1..825155315 100644 --- a/docs/gears/writegear/compression/advanced/cciw.md +++ b/docs/gears/writegear/compression/advanced/cciw.md @@ -75,7 +75,7 @@ execute_ffmpeg_cmd(ffmpeg_command) ## Usage Examples -!!! tip "Following usage examples is just an idea of what can be done with this powerful function. So just Tinker with various FFmpeg parameters/commands yourself and see it working. Also, if you're unable to run any terminal FFmpeg command, then [report an issue](../../../../../contribution/issue/)." +!!! abstract "Following usage examples is just an idea of what can be done with this powerful function. So just Tinker with various FFmpeg parameters/commands yourself and see it working. Also, if you're unable to run any terminal FFmpeg command, then [report an issue](../../../../../contribution/issue/)." ### Using WriteGear to separate Audio from Video diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index eb1a6408e..6abc3c622 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -441,9 +441,56 @@ In Compression Mode, WriteGear API allows us to exploit almost all FFmpeg suppor * Note down the Sound Card value using `arecord -L` command on the your Linux terminal. * It may be similar to this `plughw:CARD=CAMERA,DEV=0` -??? tips - - The useful audio input options for ALSA input are `-ar` (_audio sample rate_) and `-ac` (_audio channels_). Specifying audio sampling rate/frequency will force the audio card to record the audio at that specified rate. Usually the default value is `"44100"` (Hz) but `"48000"`(Hz) works, so chose wisely. Specifying audio channels will force the audio card to record the audio as mono, stereo or even 2.1, and 5.1(_if supported by your audio card_). Usually the default value is `"1"` (mono) for Mic input and `"2"` (stereo) for Line-In input. Kindly go through [FFmpeg docs](https://ffmpeg.org/ffmpeg.html) for more of such options. +??? tip "Tip for Windows" + + - [x] **Enable sound card(if disabled):** First enable your Stereo Mix by opening the "Sound" window and select the "Recording" tab, then right click on the window and select "Show Disabled Devices" to toggle the Stereo Mix device visibility. **Follow this [post ➶](https://forums.tomshardware.com/threads/no-sound-through-stereo-mix-realtek-hd-audio.1716182/) for more details.** + + - [x] **Locate Sound Card:** Then, You can locate your soundcard on windows using ffmpeg's [`directshow`](https://trac.ffmpeg.org/wiki/DirectShow) backend: + + ```sh + ffmpeg -list_devices true -f dshow -i dummy + ``` + + which will result in something similar output as following: + + ```sh + c:\> ffmpeg -list_devices true -f dshow -i dummy + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [dshow @ 03ACF580] DirectShow video devices + [dshow @ 03ACF580] "Integrated Camera" + [dshow @ 03ACF580] "screen-capture-recorder" + [dshow @ 03ACF580] DirectShow audio devices + [dshow @ 03ACF580] "Microphone (Realtek High Definition Audio)" + [dshow @ 03ACF580] "virtual-audio-capturer" + dummy: Immediate exit requested + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in WriteGear as follows: + + ```python + # change with your webcam soundcard, plus add additional required FFmpeg parameters for your writer + output_params = { + "-i": "audio=Microphone (Realtek High Definition Audio)", + "-thread_queue_size": "512", + "-f": "dshow", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + + # Define writer with defined parameters and suitable output filename for e.g. `Output.mp4 + writer = WriteGear(output_filename="Output.mp4", logging=True, **output_params) + ``` + + !!! 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" In this example code, we will merge the audio from a Audio Source _(for e.g. Webcam inbuilt mic)_ to the frames of a Video Source _(for e.g external webcam)_, and save this data as a compressed video file, all in real time: diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index 3f454a627..7d136b8c3 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -59,7 +59,7 @@ limitations under the License. - [x] **Follow [this tutorial ➶](https://medium.com/@galaktyk01/how-to-build-opencv-with-gstreamer-b11668fa09c)** -=== "FOn MAC OSes" +=== "On MAC OSes" - [x] **Follow [this tutorial ➶](https://www.learnopencv.com/install-opencv-4-on-macos/) but make sure to brew install GStreamer as follows:** diff --git a/docs/help/general_faqs.md b/docs/help/general_faqs.md index 6f6d475b5..b7983af92 100644 --- a/docs/help/general_faqs.md +++ b/docs/help/general_faqs.md @@ -34,7 +34,7 @@ limitations under the License. - There's also the official [**OpenCV Tutorials** ➶](https://docs.opencv.org/master/d6/d00/tutorial_py_root.html), provided by the OpenCV folks themselves. -Finally, once done, see [Switching from OpenCV ➶](../../switch_from_cv/) and go through our [Gears ➶](../../gears/#gears-what-are-these) to learn how VidGear works. If you run into any trouble or have any questions, then see [getting help ➶](../get_help) +Finally, once done, see [Switching from OpenCV ➶](../../switch_from_cv/) and go through our [Gears ➶](../../gears/#gears-what-are-these) to learn how VidGear APIs works. If you run into any trouble or have any questions, then see [getting help ➶](../get_help)   diff --git a/docs/index.md b/docs/index.md index 858ea84ef..58a496952 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,9 +28,9 @@ limitations under the License.   -> VidGear is a High-Performance **Video-Processing** Framework for building complex real-time media applications in python :fire: +> VidGear is a cross-platform High-Performance **Video-Processing** Framework for building complex real-time media applications in python :fire: -VidGear provides an easy-to-use, highly extensible, **[Multi-Threaded](bonus/TQM/#threaded-queue-mode) + [Asyncio](https://docs.python.org/3/library/asyncio.html) Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. +VidGear provides an easy-to-use, highly extensible, **[Multi-Threaded](bonus/TQM/#threaded-queue-mode) + [Asyncio](https://docs.python.org/3/library/asyncio.html) API Framework** on top of many state-of-the-art specialized libraries like *[OpenCV][opencv], [FFmpeg][ffmpeg], [ZeroMQ][zmq], [picamera][picamera], [starlette][starlette], [streamlink][streamlink], [pafy][pafy], [pyscreenshot][pyscreenshot], [aiortc][aiortc] and [python-mss][mss]* at its backend, and enable us to flexibly exploit their internal parameters and methods, while silently delivering robust error-handling and real-time performance ⚡️. > _"Write Less and Accomplish More"_ — VidGear's Motto diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 99b69e3ed..acbd96f51 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -37,7 +37,7 @@ Must require OpenCV(3.0+) python binaries installed for all core functions. You You can also follow online tutorials for building & installing OpenCV on [Windows](https://www.learnopencv.com/install-opencv3-on-windows/), [Linux](https://www.pyimagesearch.com/2018/05/28/ubuntu-18-04-how-to-install-opencv/) and [Raspberry Pi](https://www.pyimagesearch.com/2018/09/26/install-opencv-4-on-your-raspberry-pi/) machines manually from its source. ```sh - pip install -U opencv-python +pip install opencv-python ``` ### FFmpeg @@ -56,7 +56,7 @@ Must Required if you're using Raspberry Pi Camera Modules with its [PiGear](../. !!! 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 +pip install picamera ``` ### Aiortc @@ -81,7 +81,7 @@ Must Required only if you're using the [WebGear_RTC API](../../gears/webgear_rtc Finally, proceed installing `aiortc` via pip. ```sh - pip install aiortc +pip install aiortc ``` ### Uvloop @@ -92,7 +92,7 @@ Must required only if you're using the [NetGear_Async](../../gears/netgear_async !!! warning "Python-3.6 legacies support [**dropped in version `>=1.15.0`**](https://github.com/MagicStack/uvloop/releases/tag/v0.15.0). Kindly install previous `0.14.0` version instead." ```sh - pip install uvloop +pip install uvloop ```   @@ -108,45 +108,45 @@ Installation is as simple as: A quick solution may be to preface every Python command with `python -m` like this: ```sh - python -m pip install vidgear + python -m pip install vidgear - # or with asyncio support - python -m pip install vidgear[asyncio] + # or with asyncio support + python -m pip install vidgear[asyncio] ``` If you don't have the privileges to the directory you're installing package. Then use `--user` flag, that makes pip install packages in your home directory instead: ``` sh - python -m pip install --user vidgear + python -m pip install --user vidgear - # or with asyncio support - python -m pip install --user vidgear[asyncio] + # or with asyncio support + python -m pip install --user vidgear[asyncio] ``` ```sh - # Install stable release - pip install vidgear +# Install stable release +pip install vidgear - # Or Install stable release with Asyncio support - pip install vidgear[asyncio] +# Or Install stable release with Asyncio support +pip install vidgear[asyncio] ``` **And if you prefer to install VidGear directly from the repository:** ```sh - pip install git+git://github.com/abhiTronix/vidgear@master#egg=vidgear +pip install git+git://github.com/abhiTronix/vidgear@master#egg=vidgear - # or with asyncio support - pip install git+git://github.com/abhiTronix/vidgear@master#egg=vidgear[asyncio] +# or with asyncio support +pip install git+git://github.com/abhiTronix/vidgear@master#egg=vidgear[asyncio] ``` **Or you can also download its wheel (`.whl`) package from our repository's [releases](https://github.com/abhiTronix/vidgear/releases) section, and thereby can be installed as follows:** ```sh - pip install vidgear-0.2.2-py3-none-any.whl +pip install vidgear-0.2.2-py3-none-any.whl - # or with asyncio support - pip install vidgear-0.2.2-py3-none-any.whl[asyncio] +# or with asyncio support +pip install vidgear-0.2.2-py3-none-any.whl[asyncio] ```   diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index bce2bc7c4..b10aefecc 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -42,7 +42,7 @@ Switching OpenCV with VidGear APIs is usually a fairly painless process, and wil VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art features on top of it like: -- [x] [Accerlated Multi-Threaded](../bonus/TQM/#c-accelerates-frame-processing) Performance. +- [x] Accelerated [Multi-Threaded](../bonus/TQM/#c-accelerates-frame-processing) Performance. - [x] Real-time Stabilization. - [x] Inherit support for multiple sources. - [x] Screen-casting, Live network-streaming, [plus way much more ➶](../gears) diff --git a/mkdocs.yml b/mkdocs.yml index 093034ba9..6c46839d7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -215,7 +215,8 @@ nav: - Real-time Frames Mode: - Overview: gears/streamgear/rtfm/overview.md - Usage Examples: gears/streamgear/rtfm/usage.md - - FFmpeg Installation: gears/streamgear/ffmpeg_install.md + - Extras: + - FFmpeg Installation: gears/streamgear/ffmpeg_install.md - Parameters: gears/streamgear/params.md - References: bonus/reference/streamgear.md - FAQs: help/streamgear_faqs.md @@ -283,7 +284,6 @@ nav: - Getting Help: help/get_help.md - Frequently Asked Questions: - General FAQs: help/general_faqs.md - - Stabilizer Class FAQs: help/stabilizer_faqs.md - CamGear FAQs: help/camgear_faqs.md - PiGear FAQs: help/pigear_faqs.md - VideoGear FAQs: help/videogear_faqs.md @@ -294,3 +294,4 @@ nav: - WebGear FAQs: help/webgear_faqs.md - WebGear_RTC FAQs: help/webgear_rtc_faqs.md - NetGear_Async FAQs: help/netgear_async_faqs.md + - Stabilizer Class FAQs: help/stabilizer_faqs.md From 5020ec91aa35b9394ceab7e194b8091c0fc14fdf Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 21 Jun 2021 10:22:58 +0530 Subject: [PATCH 044/112] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Setup:=20Dropped?= =?UTF-8?q?=20patch=20for=20`simplejpeg`=20latest=20`v1.6.1`=20version.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/setup.py b/setup.py index b2782ffa4..c5b6e6690 100644 --- a/setup.py +++ b/setup.py @@ -98,11 +98,7 @@ def latest_version(package_name): "streamlink{}".format(latest_version("streamlink")), "requests{}".format(latest_version("requests")), "pyzmq{}".format(latest_version("pyzmq")), - "simplejpeg{}".format( - latest_version("simplejpeg") - if sys.version_info[:2] >= (3, 7) - else "==1.5.0" - ), # dropped support for 3.6.x legacies + "simplejpeg{}".format(latest_version("simplejpeg")), "colorlog", "colorama", "tqdm", From e8c9f0fc90d2294fab01b2d4a8d0c682321c522d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 22 Jun 2021 08:17:40 +0530 Subject: [PATCH 045/112] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20=20Docs:=20Fixed?= =?UTF-8?q?=20typos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gears/webgear/usage.md | 2 +- docs/gears/webgear_rtc/advanced.md | 2 +- docs/gears/webgear_rtc/usage.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/gears/webgear/usage.md b/docs/gears/webgear/usage.md index 91b0960ae..922efe350 100644 --- a/docs/gears/webgear/usage.md +++ b/docs/gears/webgear/usage.md @@ -111,7 +111,7 @@ uvicorn.run(web(), host="localhost", port=8000) web.shutdown() ``` -which can be accessed on any browser on the network at http://localhost:8000/. +which can be accessed on any browser on your machine at http://localhost:8000/. ### Running from Terminal diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index e36038092..a8dda61d7 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -57,7 +57,7 @@ uvicorn.run(web(), host="0.0.0.0", port=8000) web.shutdown() ``` -**And that's all, Now you can see output at [`http://localhost:8000/`](http://localhost:8000/) address.** +**And that's all, Now you can see output at [`http://localhost:8000/`](http://localhost:8000/) address on your local machine.**   diff --git a/docs/gears/webgear_rtc/usage.md b/docs/gears/webgear_rtc/usage.md index e576512eb..04c15950e 100644 --- a/docs/gears/webgear_rtc/usage.md +++ b/docs/gears/webgear_rtc/usage.md @@ -96,7 +96,7 @@ uvicorn.run(web(), host="localhost", port=8000) web.shutdown() ``` -which can be accessed on any browser on the network at http://localhost:8000/. +which can be accessed on any browser on your machine at http://localhost:8000/. ### Running from Terminal From 85e90aae534db12210caa4374c63197a6d4fcccc Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 27 Jun 2021 08:16:07 +0530 Subject: [PATCH 046/112] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20NetGear:=20Fixe?= =?UTF-8?q?d=20Bidirectional=20Video-Frame=20Transfer=20broken=20with=20fr?= =?UTF-8?q?ame=20compression=20(Fixed=20#226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐛 Fixed `return_data` interfering with return JSON-data in receive mode. - 🎨 Fixed logic and added comments. - ♻️ Refactored Code and reduced redundancy. --- vidgear/gears/netgear.py | 120 +++++++++++++-------------------------- 1 file changed, 41 insertions(+), 79 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index ecb6a3e4d..403c503e1 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1025,15 +1025,14 @@ def __recv_handler(self): track=self.__msg_track, ) + # handle data transfer in synchronous modes. if self.__pattern < 2: - if self.__bi_mode or self.__multiclient_mode: - + # check if we are returning `ndarray` frames if not (self.__return_data is None) and isinstance( self.__return_data, np.ndarray ): - - # handle return data + # handle return data for compression return_data = self.__return_data[:] # check whether exit_flag is False @@ -1053,41 +1052,29 @@ def __recv_handler(self): fastdct=self.__jpeg_compression_fastdct, ) - if self.__bi_mode: - return_dict = dict( - return_type=(type(return_data).__name__), - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - } - if self.__jpeg_compression - else False, - array_dtype=str(return_data.dtype) - if not (self.__jpeg_compression) - else "", - array_shape=return_data.shape - if not (self.__jpeg_compression) - else "", - data=None, - ) - else: - return_dict = dict( - port=self.__port, - return_type=(type(return_data).__name__), + return_dict = ( + dict() if self.__bi_mode else dict(port=self.__port) + ) + + return_dict.update( + dict( + return_type=(type(self.__return_data).__name__), compression={ "dct": self.__jpeg_compression_fastdct, "ups": self.__jpeg_compression_fastupsample, } if self.__jpeg_compression else False, - array_dtype=str(return_data.dtype) + array_dtype=str(self.__return_data.dtype) if not (self.__jpeg_compression) else "", - array_shape=return_data.shape + array_shape=self.__return_data.shape if not (self.__jpeg_compression) else "", data=None, ) + ) + # send the json dict self.__msg_socket.send_json( return_dict, self.__msg_flag | self.__zmq.SNDMORE @@ -1100,17 +1087,15 @@ def __recv_handler(self): track=self.__msg_track, ) else: - if self.__bi_mode: - return_dict = dict( - return_type=(type(self.__return_data).__name__), - data=self.__return_data, - ) - else: - return_dict = dict( - port=self.__port, + return_dict = ( + dict() if self.__bi_mode else dict(port=self.__port) + ) + return_dict.update( + dict( return_type=(type(self.__return_data).__name__), data=self.__return_data, ) + ) self.__msg_socket.send_json(return_dict, self.__msg_flag) else: # send confirmation message to server @@ -1118,7 +1103,8 @@ def __recv_handler(self): "Data received on device: {} !".format(self.__id) ) else: - if self.__return_data and self.__logging: + # else raise warning + if self.__return_data: logger.warning("`return_data` is disabled for this pattern!") # check if encoding was enabled @@ -1248,26 +1234,12 @@ def send(self, frame, message=None): fastdct=self.__jpeg_compression_fastdct, ) - # check if multiserver_mode is activated - if self.__multiserver_mode: - # prepare the exclusive json dict and assign values with unique port - msg_dict = dict( - terminate_flag=exit_flag, - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - } - if self.__jpeg_compression - else False, - port=self.__port, - pattern=str(self.__pattern), - message=message, - dtype=str(frame.dtype) if not (self.__jpeg_compression) else "", - shape=frame.shape if not (self.__jpeg_compression) else "", - ) - else: - # otherwise prepare normal json dict and assign values - msg_dict = dict( + # check if multiserver_mode is activated and assign values with unique port + msg_dict = dict(port=self.__port) if self.__multiserver_mode else dict() + + # prepare the exclusive json dict + msg_dict.update( + dict( terminate_flag=exit_flag, compression={ "dct": self.__jpeg_compression_fastdct, @@ -1280,6 +1252,7 @@ def send(self, frame, message=None): dtype=str(frame.dtype) if not (self.__jpeg_compression) else "", shape=frame.shape if not (self.__jpeg_compression) else "", ) + ) # send the json dict self.__msg_socket.send_json(msg_dict, self.__msg_flag | self.__zmq.SNDMORE) @@ -1324,7 +1297,6 @@ def send(self, frame, message=None): # Create new connection self.__msg_socket = self.__msg_context.socket(self.__msg_pattern) - if isinstance(self.__connection_address, list): for _connection in self.__connection_address: self.__msg_socket.connect(_connection) @@ -1343,9 +1315,8 @@ def send(self, frame, message=None): else: # connect normally self.__msg_socket.connect(self.__connection_address) - self.__poll.register(self.__msg_socket, self.__zmq.POLLIN) - + # return None for mean-time return None # save the unique port addresses @@ -1483,6 +1454,7 @@ def close(self): except self.__ZMQError: pass finally: + # exit return if self.__multiserver_mode: @@ -1495,29 +1467,19 @@ def close(self): try: if self.__multiclient_mode: - if self.__port_buffer: - for _ in self.__port_buffer: - self.__msg_socket.send_json(term_dict) - - # check for confirmation if available within half timeout - if self.__pattern < 2: - if self.__logging: - logger.debug("Terminating. Please wait...") - if self.__msg_socket.poll( - self.__request_timeout // 5, self.__zmq.POLLIN - ): - self.__msg_socket.recv() + for _ in self.__port_buffer: + self.__msg_socket.send_json(term_dict) else: self.__msg_socket.send_json(term_dict) - # check for confirmation if available within half timeout - if self.__pattern < 2: - if self.__logging: - logger.debug("Terminating. Please wait...") - if self.__msg_socket.poll( - self.__request_timeout // 5, self.__zmq.POLLIN - ): - self.__msg_socket.recv() + # check for confirmation if available within 1/5 timeout + if self.__pattern < 2: + if self.__logging: + logger.debug("Terminating. Please wait...") + if self.__msg_socket.poll( + self.__request_timeout // 5, self.__zmq.POLLIN + ): + self.__msg_socket.recv() except Exception as e: if not isinstance(e, self.__ZMQError): logger.exception(str(e)) From 69b76aee061044e81cd4738f7c019c4ff6d26531 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 28 Jun 2021 10:01:39 +0530 Subject: [PATCH 047/112] =?UTF-8?q?=F0=9F=90=9B=20Setup:=20Fixed=20`latest?= =?UTF-8?q?=5Fversion`=20returning=20incorrect=20version=20for=20some=20py?= =?UTF-8?q?pi=20packages.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c5b6e6690..37124e464 100644 --- a/setup.py +++ b/setup.py @@ -59,8 +59,8 @@ def latest_version(package_name): try: response = urllib.request.urlopen(urllib.request.Request(url), timeout=1) data = json.load(response) - versions = data["releases"].keys() - versions = sorted(versions) + versions = list(data["releases"].keys()) + versions.sort(key=LooseVersion) return ">={}".format(versions[-1]) except: pass From 938ce9e97a93b7e92586445410f2ef543ba82ebf Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 29 Jun 2021 08:10:00 +0530 Subject: [PATCH 048/112] =?UTF-8?q?=F0=9F=90=9B=20WebGear=5FRTC:=20Fixed?= =?UTF-8?q?=20event=20loop=20closing=20prematurely=20while=20reloading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Internally disabled suspending event loop while reloading. - Updated CI tests. --- vidgear/gears/asyncio/webgear_rtc.py | 11 ++++++---- .../asyncio_tests/test_webgear_rtc.py | 22 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 0a257b32c..efdd055c8 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -599,6 +599,8 @@ async def __reset_connections(self, request): """ Resets all connections and recreates VideoServer timestamps """ + # get additional parameter + parameter = await request.json() # check if Live Broadcasting is enabled if ( self.__relay is None @@ -606,10 +608,11 @@ async def __reset_connections(self, request): and (self.__default_rtc_server.is_running) ): logger.critical("Resetting Server") - # collects peer RTC connections - coros = [pc.close() for pc in self.__pcs] - await asyncio.gather(*coros) - self.__pcs.clear() + # close old peer connections + if parameter != 0: # disabled for testing + coros = [pc.close() for pc in self.__pcs] + await asyncio.gather(*coros) + self.__pcs.clear() await self.__default_rtc_server.reset() return PlainTextResponse("OK") else: 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 b03b928fb..63093b19f 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -390,7 +390,7 @@ def test_webpage_reload(options): # simulate webpage reload response_rtc_reload = client.post( "/close_connection", - data="1", + data="0", ) # close offer run(offer_pc.close()) @@ -515,7 +515,6 @@ def test_webgear_rtc_routes(): pytest.fail(str(e)) -@pytest.mark.xfail(raises=RuntimeError) def test_webgear_rtc_routes_validity(): # add various tweaks for testing only options = { @@ -524,9 +523,16 @@ def test_webgear_rtc_routes_validity(): } # initialize WebGear_RTC app web = WebGear_RTC(source=return_testvideo_path(), logging=True) - # modify route - web.routes.clear() - # test - client = TestClient(web(), raise_server_exceptions=True) - # close - web.shutdown() + try: + # modify route + web.routes.clear() + # test + client = TestClient(web(), raise_server_exceptions=True) + except Exception as e: + if isinstance(e, RuntimeError): + pytest.xfail(str(e)) + else: + pytest.fail(str(e)) + finally: + # close + web.shutdown() From 36d2b53b73282524299f6ed29cbc198f3565588b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 1 Jul 2021 07:53:00 +0530 Subject: [PATCH 049/112] :bug: WebGear_RTC: Fixed event loop switching to incompatible one on windows in CI tests. --- .../streamer_tests/asyncio_tests/test_webgear_rtc.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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 63093b19f..c13dec024 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -21,6 +21,7 @@ # import the necessary packages import os import cv2 +import sys import pytest import asyncio import platform @@ -52,6 +53,14 @@ logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) +# 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()) + def return_testvideo_path(): """ From 9a5a1e062abff1c03d7c320c43bcc059eb18166b Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 4 Jul 2021 10:23:21 +0530 Subject: [PATCH 050/112] :bug: WebGear_RTC: Fixed more asyncio bugs in CI Tests for windows enviroments. --- .../asyncio_tests/test_webgear_rtc.py | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) 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 c13dec024..eb57c5916 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -53,6 +53,7 @@ logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) +# handles event loop # Setup and assign event loop policy if platform.system() == "Windows": # On Windows, VidGear requires the ``WindowsSelectorEventLoop``, and this is @@ -60,6 +61,8 @@ # 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()) +# Retrieve event loop and assign it +loop = asyncio.get_event_loop() def return_testvideo_path(): @@ -73,7 +76,10 @@ def return_testvideo_path(): def run(coro): - return asyncio.get_event_loop().run_until_complete(coro) + logger.debug( + "Using `{}` event loop for this process.".format(loop.__class__.__name__) + ) + return loop.run_until_complete(coro) class VideoTransformTrack(MediaStreamTrack): @@ -277,12 +283,7 @@ def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 + assert offer_pc.iceConnectionState == "checking" run(offer_pc.close()) web.shutdown() except Exception as e: @@ -337,12 +338,7 @@ def test_webgear_rtc_options(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 + assert offer_pc.iceConnectionState == "checking" run(offer_pc.close()) web.shutdown() except Exception as e: @@ -365,7 +361,6 @@ def test_webgear_rtc_options(options): ] -@pytest.mark.skipif((platform.system() == "Windows"), reason="Random Failures!") @pytest.mark.parametrize("options", test_data) def test_webpage_reload(options): """ @@ -389,13 +384,7 @@ def test_webpage_reload(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - + assert offer_pc.iceConnectionState == "checking" # simulate webpage reload response_rtc_reload = client.post( "/close_connection", @@ -419,13 +408,7 @@ def test_webpage_reload(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - + assert offer_pc.iceConnectionState == "checking" # shutdown run(offer_pc.close()) except Exception as e: @@ -512,12 +495,8 @@ def test_webgear_rtc_routes(): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 + assert offer_pc.iceConnectionState == "checking" + # shutdown run(offer_pc.close()) web.shutdown() except Exception as e: From cfddddc9c50a62f984ccc2c7e869b215a9cfd87a Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 4 Jul 2021 10:52:06 +0530 Subject: [PATCH 051/112] :bug: CI: Fixed Event loop is closed bug --- .../tests/streamer_tests/asyncio_tests/test_webgear_rtc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 eb57c5916..944efb834 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -61,8 +61,6 @@ # 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()) -# Retrieve event loop and assign it -loop = asyncio.get_event_loop() def return_testvideo_path(): @@ -76,6 +74,8 @@ def return_testvideo_path(): def run(coro): + # Retrieve event loop and assign it + loop = asyncio.get_event_loop() logger.debug( "Using `{}` event loop for this process.".format(loop.__class__.__name__) ) From da9f90997d60322b4a3dd0c0baf5b1a0a6ecc70f Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 4 Jul 2021 19:56:59 +0530 Subject: [PATCH 052/112] :bug: CI: More asyncio event loop fixes --- .../streamer_tests/asyncio_tests/test_webgear_rtc.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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 944efb834..2b72fca19 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -53,6 +53,7 @@ logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) + # handles event loop # Setup and assign event loop policy if platform.system() == "Windows": @@ -60,6 +61,7 @@ # 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): + logger.info("Setting Event loop policy for windows.") asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) @@ -75,6 +77,12 @@ def return_testvideo_path(): def run(coro): # Retrieve event loop and assign it + if platform.system() == "Windows": + if sys.version_info[:2] >= (3, 8) and not isinstance( + asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy + ): + logger.info("Resetting Event loop policy for windows.") + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) loop = asyncio.get_event_loop() logger.debug( "Using `{}` event loop for this process.".format(loop.__class__.__name__) From 68d63c2a1c92c6775f0a8f520129b55b52a98f56 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 4 Jul 2021 22:12:41 +0530 Subject: [PATCH 053/112] :white_check_mark: CI: Restored old webgear_rtc test behavior --- .../asyncio_tests/test_webgear_rtc.py | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) 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 2b72fca19..7d21db908 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -291,7 +291,12 @@ def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - assert offer_pc.iceConnectionState == "checking" + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 run(offer_pc.close()) web.shutdown() except Exception as e: @@ -346,7 +351,12 @@ def test_webgear_rtc_options(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - assert offer_pc.iceConnectionState == "checking" + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 run(offer_pc.close()) web.shutdown() except Exception as e: @@ -392,7 +402,12 @@ def test_webpage_reload(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - assert offer_pc.iceConnectionState == "checking" + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 # simulate webpage reload response_rtc_reload = client.post( "/close_connection", @@ -416,7 +431,12 @@ def test_webpage_reload(options): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - assert offer_pc.iceConnectionState == "checking" + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 # shutdown run(offer_pc.close()) except Exception as e: @@ -503,7 +523,12 @@ def test_webgear_rtc_routes(): params = response_rtc_answer.json() answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) run(offer_pc.setRemoteDescription(answer)) - assert offer_pc.iceConnectionState == "checking" + response_rtc_offer = client.get( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 # shutdown run(offer_pc.close()) web.shutdown() From 18db4a0ca207dbb8f5cdac7b3efa44641980b30f Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 5 Jul 2021 06:37:44 +0530 Subject: [PATCH 054/112] :green_heart: CI: Replaced buggy starlette TestClient with async-asgi-testclient - Removed `run()` method and replaced with pure asyncio implementation. - Replaced `startlette.TestClient` with async_asgi_testclient. - Added new async-asgi-testclient CI dependency. - Updated CI tests. - Rollbacked previous changes. --- .github/workflows/ci_linux.yml | 2 +- appveyor.yml | 2 +- azure-pipelines.yml | 4 +- vidgear/gears/asyncio/webgear_rtc.py | 2 +- .../asyncio_tests/test_webgear_rtc.py | 328 ++++++++---------- 5 files changed, 153 insertions(+), 185 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index a82b8b636..d0052d72d 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -51,7 +51,7 @@ jobs: pip install -U pip wheel numpy pip install -U .[asyncio] pip uninstall opencv-python -y - pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash paramiko + pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash paramiko async-asgi-testclient if: success() - name: run prepare_dataset_script run: bash scripts/bash/prepare_dataset.sh diff --git a/appveyor.yml b/appveyor.yml index 4bf60c172..f125375e5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "python -m pip install --upgrade pip wheel" - - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko" + - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko async-asgi-testclient" - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev/mpegdash-0.3.0.dev0-py3-none-any.whl" - cmd: chmod +x scripts/bash/prepare_dataset.sh - cmd: bash scripts/bash/prepare_dataset.sh diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f3ba7a24f..c8350cdfb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,8 +55,8 @@ steps: - script: | python -m pip install --upgrade pip wheel - pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash paramiko - pip install --upgrade pytest pytest-asyncio pytest-cov pytest-azurepipelines + pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash paramiko async-asgi-testclient + pip install --upgrade pytest pytest-asyncio pytest-cov pytest-azurepipelines displayName: 'Install pip dependencies' - script: | diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index efdd055c8..2f48b57eb 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -609,7 +609,7 @@ async def __reset_connections(self, request): ): logger.critical("Resetting Server") # close old peer connections - if parameter != 0: # disabled for testing + if parameter != 0: # disable if specified explicitly coros = [pc.close() for pc in self.__pcs] await asyncio.gather(*coros) self.__pcs.clear() 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 7d21db908..5dcb7aea9 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -21,8 +21,8 @@ # import the necessary packages import os import cv2 -import sys import pytest +import sys import asyncio import platform import logging as log @@ -33,7 +33,7 @@ from starlette.responses import PlainTextResponse from starlette.middleware import Middleware from starlette.middleware.cors import CORSMiddleware -from starlette.testclient import TestClient +from async_asgi_testclient import TestClient from aiortc import ( MediaStreamTrack, RTCPeerConnection, @@ -75,21 +75,6 @@ def return_testvideo_path(): return os.path.abspath(path) -def run(coro): - # Retrieve event loop and assign it - if platform.system() == "Windows": - if sys.version_info[:2] >= (3, 8) and not isinstance( - asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy - ): - logger.info("Resetting Event loop policy for windows.") - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - loop = asyncio.get_event_loop() - logger.debug( - "Using `{}` event loop for this process.".format(loop.__class__.__name__) - ) - return loop.run_until_complete(coro) - - class VideoTransformTrack(MediaStreamTrack): """ A video stream track that transforms frames from an another track. @@ -106,53 +91,24 @@ async def recv(self): return frame -def track_states(pc): - states = { - "connectionState": [pc.connectionState], - "iceConnectionState": [pc.iceConnectionState], - "iceGatheringState": [pc.iceGatheringState], - "signalingState": [pc.signalingState], - } - - @pc.on("connectionstatechange") - def connectionstatechange(): - states["connectionState"].append(pc.connectionState) - - @pc.on("iceconnectionstatechange") - def iceconnectionstatechange(): - states["iceConnectionState"].append(pc.iceConnectionState) - - @pc.on("icegatheringstatechange") - def icegatheringstatechange(): - states["iceGatheringState"].append(pc.iceGatheringState) - - @pc.on("signalingstatechange") - def signalingstatechange(): - states["signalingState"].append(pc.signalingState) - - return states - - -def get_RTCPeer_payload(): +async def get_RTCPeer_payload(): pc = RTCPeerConnection( RTCConfiguration(iceServers=[RTCIceServer("stun:stun.l.google.com:19302")]) ) - track_states(pc) - @pc.on("track") - def on_track(track): + async def on_track(track): logger.debug("Receiving %s" % track.kind) if track.kind == "video": pc.addTrack(VideoTransformTrack(track)) @track.on("ended") - def on_ended(): + async def on_ended(): logger.info("Track %s ended", track.kind) pc.addTransceiver("video", direction="recvonly") - offer = run(pc.createOffer()) - run(pc.setLocalDescription(offer)) + offer = await pc.createOffer() + await pc.setLocalDescription(offer) new_offer = pc.localDescription payload = {"sdp": new_offer.sdp, "type": new_offer.type} return (pc, json.dumps(payload, separators=(",", ":"))) @@ -264,8 +220,9 @@ def __init__(self, source=None): ] +@pytest.mark.asyncio @pytest.mark.parametrize("source, stabilize, colorspace, time_delay", test_data) -def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): +async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ Test for various WebGear_RTC API parameters """ @@ -277,27 +234,27 @@ def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): time_delay=time_delay, logging=True, ) - client = TestClient(web(), raise_server_exceptions=True) - response = client.get("/") - assert response.status_code == 200 - response_404 = client.get("/test") - assert response_404.status_code == 404 - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - run(offer_pc.close()) + async with TestClient(web()) as client: + response = await client.get("/") + assert response.status_code == 200 + response_404 = await client.get("/test") + assert response_404.status_code == 404 + (offer_pc, data) = await get_RTCPeer_payload() + response_rtc_answer = await client.post( + "/offer", + data=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( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + await offer_pc.close() web.shutdown() except Exception as e: pytest.fail(str(e)) @@ -328,36 +285,39 @@ def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): ] +@pytest.mark.skipif((platform.system() == "Windows"), reason="Random Failures!") +@pytest.mark.asyncio @pytest.mark.parametrize("options", test_data) -def test_webgear_rtc_options(options): +async def test_webgear_rtc_options(options): """ Test for various WebGear_RTC API internal options """ + web = None try: web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) - client = TestClient(web(), raise_server_exceptions=True) - response = client.get("/") - assert response.status_code == 200 - if ( - not "enable_live_broadcast" in options - or options["enable_live_broadcast"] == False - ): - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - run(offer_pc.close()) + async with TestClient(web()) as client: + response = await client.get("/") + assert response.status_code == 200 + if ( + not "enable_live_broadcast" in options + or options["enable_live_broadcast"] == False + ): + (offer_pc, data) = await get_RTCPeer_payload() + response_rtc_answer = await client.post( + "/offer", + data=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( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + await offer_pc.close() web.shutdown() except Exception as e: if isinstance(e, AssertionError): @@ -379,8 +339,10 @@ def test_webgear_rtc_options(options): ] +@pytest.mark.skipif((platform.system() == "Windows"), reason="Random Failures!") +@pytest.mark.asyncio @pytest.mark.parametrize("options", test_data) -def test_webpage_reload(options): +async def test_webpage_reload(options): """ Test for testing WebGear_RTC API against Webpage reload disruptions @@ -388,57 +350,57 @@ def test_webpage_reload(options): web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: # run webgear_rtc - client = TestClient(web(), raise_server_exceptions=True) - response = client.get("/") - assert response.status_code == 200 - - # create offer and receive - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - # simulate webpage reload - response_rtc_reload = client.post( - "/close_connection", - data="0", - ) - # close offer - run(offer_pc.close()) - offer_pc = None - data = None - # verify response - logger.debug(response_rtc_reload.text) - assert response_rtc_reload.text == "OK", "Test Failed!" - - # recreate offer and continue receive - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - # shutdown - run(offer_pc.close()) + async with TestClient(web()) as client: + response = await client.get("/") + assert response.status_code == 200 + + # create offer and receive + (offer_pc, data) = await get_RTCPeer_payload() + response_rtc_answer = await client.post( + "/offer", + data=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( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + # simulate webpage reload + response_rtc_reload = await client.post( + "/close_connection", + data="0", + ) + # close offer + await offer_pc.close() + offer_pc = None + data = None + # verify response + logger.debug(response_rtc_reload.text) + assert response_rtc_reload.text == "OK", "Test Failed!" + + # recreate offer and continue receive + (offer_pc, data) = await get_RTCPeer_payload() + response_rtc_answer = await client.post( + "/offer", + data=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( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + # shutdown + await offer_pc.close() except Exception as e: if "enable_live_broadcast" in options and isinstance(e, AssertionError): pytest.xfail("Test Passed") @@ -457,15 +419,17 @@ def test_webpage_reload(options): ] +@pytest.mark.asyncio @pytest.mark.xfail(raises=ValueError) @pytest.mark.parametrize("server, result", test_data_class) -def test_webgear_rtc_custom_server_generator(server, result): +async def test_webgear_rtc_custom_server_generator(server, result): """ Test for WebGear_RTC API's custom source """ web = WebGear_RTC(logging=True) web.config["server"] = server - client = TestClient(web(), raise_server_exceptions=True) + async with TestClient(web()) as client: + pass web.shutdown() @@ -476,24 +440,26 @@ def test_webgear_rtc_custom_server_generator(server, result): ] +@pytest.mark.asyncio @pytest.mark.parametrize("middleware, result", test_data_class) -def test_webgear_rtc_custom_middleware(middleware, result): +async def test_webgear_rtc_custom_middleware(middleware, result): """ Test for WebGear_RTC API's custom middleware """ try: web = WebGear_RTC(source=return_testvideo_path(), logging=True) web.middleware = middleware - client = TestClient(web(), raise_server_exceptions=True) - response = client.get("/") - assert response.status_code == 200 + async with TestClient(web()) as client: + response = await client.get("/") + assert response.status_code == 200 web.shutdown() except Exception as e: if result: pytest.fail(str(e)) -def test_webgear_rtc_routes(): +@pytest.mark.asyncio +async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes """ @@ -509,34 +475,35 @@ def test_webgear_rtc_routes(): web.routes.append(Route("/hello", endpoint=hello_webpage)) # test - client = TestClient(web(), raise_server_exceptions=True) - response = client.get("/") - assert response.status_code == 200 - response_hello = client.get("/hello") - assert response_hello.status_code == 200 - (offer_pc, data) = get_RTCPeer_payload() - response_rtc_answer = client.post( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - params = response_rtc_answer.json() - answer = RTCSessionDescription(sdp=params["sdp"], type=params["type"]) - run(offer_pc.setRemoteDescription(answer)) - response_rtc_offer = client.get( - "/offer", - data=data, - headers={"Content-Type": "application/json"}, - ) - assert response_rtc_offer.status_code == 200 - # shutdown - run(offer_pc.close()) + async with TestClient(web()) as client: + response = await client.get("/") + assert response.status_code == 200 + response_hello = await client.get("/hello") + assert response_hello.status_code == 200 + (offer_pc, data) = await get_RTCPeer_payload() + response_rtc_answer = await client.post( + "/offer", + data=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( + "/offer", + data=data, + headers={"Content-Type": "application/json"}, + ) + assert response_rtc_offer.status_code == 200 + # shutdown + await offer_pc.close() web.shutdown() except Exception as e: pytest.fail(str(e)) -def test_webgear_rtc_routes_validity(): +@pytest.mark.asyncio +async def test_webgear_rtc_routes_validity(): # add various tweaks for testing only options = { "enable_infinite_frames": False, @@ -548,7 +515,8 @@ def test_webgear_rtc_routes_validity(): # modify route web.routes.clear() # test - client = TestClient(web(), raise_server_exceptions=True) + async with TestClient(web()) as client: + pass except Exception as e: if isinstance(e, RuntimeError): pytest.xfail(str(e)) From 54765957214a3fb02168c9a98994f51ff3bb4bfd Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 5 Jul 2021 21:11:04 +0530 Subject: [PATCH 055/112] :construction_worker: CI: Event Policy Loop patcher added for WebGear_RTC. --- .../asyncio_tests/test_webgear_rtc.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) 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 5dcb7aea9..9be29e7cf 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -75,6 +75,23 @@ def return_testvideo_path(): return os.path.abspath(path) +def patch_eventlooppolicy(): + """ + Fixes event loop policy on newer python versions + """ + # Retrieve event loop and assign it + if platform.system() == "Windows": + if sys.version_info[:2] >= (3, 8) and not isinstance( + asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy + ): + logger.info("Resetting Event loop policy for windows.") + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + loop = asyncio.get_event_loop() + logger.debug( + "Using `{}` event loop for this process.".format(loop.__class__.__name__) + ) + + class VideoTransformTrack(MediaStreamTrack): """ A video stream track that transforms frames from an another track. @@ -226,6 +243,7 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ Test for various WebGear_RTC API parameters """ + patch_eventloop() try: web = WebGear_RTC( source=source, @@ -285,13 +303,13 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): ] -@pytest.mark.skipif((platform.system() == "Windows"), reason="Random Failures!") @pytest.mark.asyncio @pytest.mark.parametrize("options", test_data) async def test_webgear_rtc_options(options): """ Test for various WebGear_RTC API internal options """ + patch_eventloop() web = None try: web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) @@ -347,6 +365,7 @@ async def test_webpage_reload(options): Test for testing WebGear_RTC API against Webpage reload disruptions """ + patch_eventloop() web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: # run webgear_rtc @@ -426,6 +445,7 @@ async def test_webgear_rtc_custom_server_generator(server, result): """ Test for WebGear_RTC API's custom source """ + patch_eventloop() web = WebGear_RTC(logging=True) web.config["server"] = server async with TestClient(web()) as client: @@ -446,6 +466,7 @@ async def test_webgear_rtc_custom_middleware(middleware, result): """ Test for WebGear_RTC API's custom middleware """ + patch_eventloop() try: web = WebGear_RTC(source=return_testvideo_path(), logging=True) web.middleware = middleware @@ -463,6 +484,7 @@ async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes """ + patch_eventloop() try: # add various performance tweaks as usual options = { @@ -505,6 +527,7 @@ async def test_webgear_rtc_routes(): @pytest.mark.asyncio async def test_webgear_rtc_routes_validity(): # add various tweaks for testing only + patch_eventloop() options = { "enable_infinite_frames": False, "enable_live_broadcast": True, From 8428fcc1b2fa621d6835e350dab7e48c72b1e9b8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 5 Jul 2021 21:44:34 +0530 Subject: [PATCH 056/112] :pencil2: CI: Fixed typo. --- .../asyncio_tests/test_webgear_rtc.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 9be29e7cf..11e66a184 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -243,7 +243,7 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ Test for various WebGear_RTC API parameters """ - patch_eventloop() + patch_eventlooppolicy() try: web = WebGear_RTC( source=source, @@ -309,7 +309,7 @@ async def test_webgear_rtc_options(options): """ Test for various WebGear_RTC API internal options """ - patch_eventloop() + patch_eventlooppolicy() web = None try: web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) @@ -365,7 +365,7 @@ async def test_webpage_reload(options): Test for testing WebGear_RTC API against Webpage reload disruptions """ - patch_eventloop() + patch_eventlooppolicy() web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: # run webgear_rtc @@ -445,7 +445,7 @@ async def test_webgear_rtc_custom_server_generator(server, result): """ Test for WebGear_RTC API's custom source """ - patch_eventloop() + patch_eventlooppolicy() web = WebGear_RTC(logging=True) web.config["server"] = server async with TestClient(web()) as client: @@ -466,7 +466,7 @@ async def test_webgear_rtc_custom_middleware(middleware, result): """ Test for WebGear_RTC API's custom middleware """ - patch_eventloop() + patch_eventlooppolicy() try: web = WebGear_RTC(source=return_testvideo_path(), logging=True) web.middleware = middleware @@ -484,7 +484,7 @@ async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes """ - patch_eventloop() + patch_eventlooppolicy() try: # add various performance tweaks as usual options = { @@ -527,7 +527,7 @@ async def test_webgear_rtc_routes(): @pytest.mark.asyncio async def test_webgear_rtc_routes_validity(): # add various tweaks for testing only - patch_eventloop() + patch_eventlooppolicy() options = { "enable_infinite_frames": False, "enable_live_broadcast": True, From 1b410978fb45899b7814f7560bf1d28ac95d0bff Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 6 Jul 2021 09:42:11 +0530 Subject: [PATCH 057/112] =?UTF-8?q?=E2=9C=A8=20NetGear:=20Enabled=20colors?= =?UTF-8?q?pace=20conversion=20support=20with=20frame=20compression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implemented colorspace coversion on-the-fly with JPEG frame compression. - Updated `jpeg_compression` dict parameter to support colorspace string values. - Added all supported colorspace values by underline `simplejpeg` library. - Enable "BGR" colorspace by default. - 🐛 Fixed color-subsampling interfering with colorspace. - :warning: Enforced server colorspace on client(s). --- vidgear/gears/netgear.py | 82 ++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 403c503e1..165fea04f 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -210,6 +210,7 @@ def __init__( self.__jpeg_compression_quality = 90 # 90% quality self.__jpeg_compression_fastdct = True # fastest DCT on by default self.__jpeg_compression_fastupsample = False # fastupsample off by default + self.__jpeg_compression_colorspace = "BGR" # use BGR colorspace by default # defines frame compression on return data self.__ex_compression_params = None @@ -331,9 +332,28 @@ def __init__( ) ) - elif key == "jpeg_compression" and isinstance(value, bool): - # enable frame-compression encoding value - self.__jpeg_compression = value + elif key == "jpeg_compression" and isinstance(value, (bool, str)): + if isinstance(value, str) and value.strip().upper() in [ + "RGB", + "BGR", + "RGBX", + "BGRX", + "XBGR", + "XRGB", + "GRAY", + "RGBA", + "BGRA", + "ABGR", + "ARGB", + "CMYK", + ]: + # set encoding colorspace + self.__jpeg_compression_colorspace = value.strip().upper() + # enable frame-compression encoding value + self.__jpeg_compression = True + else: + # enable frame-compression encoding value + self.__jpeg_compression = value elif key == "jpeg_compression_quality" and isinstance(value, (int, float)): # set valid jpeg quality if value >= 10 and value <= 100: @@ -694,7 +714,8 @@ def __init__( ) if self.__jpeg_compression: logger.debug( - "JPEG Frame-Compression is activated for this connection with Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + "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 @@ -903,7 +924,8 @@ def __init__( ) if self.__jpeg_compression: logger.debug( - "JPEG Frame-Compression is activated for this connection with Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + "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 @@ -1044,13 +1066,21 @@ def __recv_handler(self): # handle jpeg-compression encoding if self.__jpeg_compression: - return_data = simplejpeg.encode_jpeg( - return_data, - quality=self.__jpeg_compression_quality, - colorspace="BGR", - colorsubsampling="422", - fastdct=self.__jpeg_compression_fastdct, - ) + if self.__jpeg_compression_colorspace == "GRAY": + return_data = simplejpeg.encode_jpeg( + return_data, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + fastdct=self.__jpeg_compression_fastdct, + ) + else: + return_data = simplejpeg.encode_jpeg( + return_data, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + colorsubsampling="422", + fastdct=self.__jpeg_compression_fastdct, + ) return_dict = ( dict() if self.__bi_mode else dict(port=self.__port) @@ -1062,6 +1092,7 @@ def __recv_handler(self): compression={ "dct": self.__jpeg_compression_fastdct, "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, } if self.__jpeg_compression else False, @@ -1112,7 +1143,7 @@ def __recv_handler(self): # decode JPEG frame frame = simplejpeg.decode_jpeg( msg_data, - colorspace="BGR", + colorspace=msg_json["compression"]["colorspace"], fastdct=self.__jpeg_compression_fastdct or msg_json["compression"]["dct"], fastupsample=self.__jpeg_compression_fastupsample @@ -1226,13 +1257,21 @@ def send(self, frame, message=None): # handle JPEG compression encoding if self.__jpeg_compression: - frame = simplejpeg.encode_jpeg( - frame, - quality=self.__jpeg_compression_quality, - colorspace="BGR", - colorsubsampling="422", - fastdct=self.__jpeg_compression_fastdct, - ) + if self.__jpeg_compression_colorspace == "GRAY": + frame = simplejpeg.encode_jpeg( + frame, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + fastdct=self.__jpeg_compression_fastdct, + ) + else: + frame = simplejpeg.encode_jpeg( + frame, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + colorsubsampling="422", + fastdct=self.__jpeg_compression_fastdct, + ) # check if multiserver_mode is activated and assign values with unique port msg_dict = dict(port=self.__port) if self.__multiserver_mode else dict() @@ -1244,6 +1283,7 @@ def send(self, frame, message=None): compression={ "dct": self.__jpeg_compression_fastdct, "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, } if self.__jpeg_compression else False, @@ -1337,7 +1377,7 @@ def send(self, frame, message=None): # decode JPEG frame recvd_data = simplejpeg.decode_jpeg( recv_array, - colorspace="BGR", + colorspace=recv_json["compression"]["colorspace"], fastdct=self.__jpeg_compression_fastdct or recv_json["compression"]["dct"], fastupsample=self.__jpeg_compression_fastupsample From 30693f758c38dec348595675a65df1129d131191 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 12 Jul 2021 11:19:40 +0530 Subject: [PATCH 058/112] :boom: NetGear: Patched external bug and new updates - :alien: Patched external `simplejpeg` bug. Issue: https://gitlab.com/jfolz/simplejpeg/-/issues/11 - :memo: Added Example for changing incoming frames colorspace with NetGear's Frame Compression. - :wrench: Updated to new extra `analytics` parameter in Material Mkdocs. - :memo: Updated Frame Compression parameters in NetGear docs. - :bug: Fixed `main.py` shutdown. --- docs/gears/netgear/advanced/compression.md | 143 +++++++- docs/gears/netgear/params.md | 6 +- mkdocs.yml | 6 +- vidgear/gears/asyncio/__main__.py | 391 ++++++++++----------- vidgear/gears/netgear.py | 6 + 5 files changed, 344 insertions(+), 208 deletions(-) diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index 1c3244703..394509507 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -53,12 +53,29 @@ Frame Compression is enabled by default in NetGear, and can be easily controlled For implementing Frame Compression, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter to leverage performance with Frame Compression: -* `jpeg_compression` _(bool)_: This attribute can be used to activate(if True)/deactivate(if False) Frame Compression. Its default value is also `True`, and its usage is as follows: +* `jpeg_compression` _(bool/str)_: This internal attribute is used to activate/deactivate JPEG Frame Compression as well as to specify incoming frames colorspace with compression. Its usage is as follows: + + - [x] **For activating JPEG Frame Compression _(Boolean)_:** + + !!! alert "In this case, colorspace will default to `BGR`." + + !!! note "You can set `jpeg_compression` value to `False` at Server end to completely disable Frame Compression." + + ```python + # enable jpeg encoding + options = {"jpeg_compression": True} + ``` + + - [x] **For specifying Input frames colorspace _(String)_:** + + !!! alert "In this case, JPEG Frame Compression is activated automatically." + + !!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" - ```python - # disable jpeg encoding - options = {"jpeg_compression": False} - ``` + ```python + # Specify incoming frames are `grayscale` + options = {"jpeg_compression": "GRAY"} + ``` * ### Performance Attributes :zap: @@ -160,7 +177,7 @@ from vidgear.gears import NetGear import cv2 # define NetGear Client with `receive_mode = True` and defined parameter -client = NetGear(receive_mode=True, pattern=1, logging=True, **options) +client = NetGear(receive_mode=True, pattern=1, logging=True) # loop over while True: @@ -194,6 +211,120 @@ client.close()   +### Bare-Minimum Usage with Variable Colorspace + +Frame Compression also supports specify incoming frames colorspace with compression. In following bare-minimum code, we will be sending [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) frames from Server to Client: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! example "This example works in conjunction with [Source ColorSpace manipulation for VideoCapture Gears ➶](../../../../../bonus/colorspace_manipulation/#source-colorspace-manipulation)" + +!!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" + +#### Server End + +Open your favorite terminal and execute the following python code: + +!!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import NetGear +import cv2 + +# open any valid video stream(for e.g `test.mp4` file) and change its colorspace to `GRAY` +stream = VideoGear(source="test.mp4", colorspace="COLOR_BGR2GRAY").start() + +# activate jpeg encoding and specify other related parameters +options = { + "jpeg_compression": "GRAY", # grayscale + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + +# Define NetGear Server with defined parameters +server = NetGear(pattern=1, logging=True, **options) + +# loop over until KeyBoard Interrupted +while True: + + try: + # read grayscale frames from stream + frame = stream.read() + + # check for frame if None-type + if frame is None: + break + + # {do something with the frame here} + + # send grayscale frame to server + server.send(frame) + + except KeyboardInterrupt: + break + +# safely close video stream +stream.stop() + +# safely close server +server.close() +``` + +  + +#### Client 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!" + +!!! note "If compression is enabled at Server, then Client will automatically enforce Frame Compression with its performance attributes." + +!!! info "Client's end also automatically enforces Server's colorspace, there's no need to define it again." + +```python +# import required libraries +from vidgear.gears import NetGear +import cv2 + +# define NetGear Client with `receive_mode = True` and defined parameter +client = NetGear(receive_mode=True, pattern=1, logging=True) + +# loop over +while True: + + # receive grayscale 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 Grayscale 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() +``` + +  + +  + ### Using Frame Compression with Variable Parameters diff --git a/docs/gears/netgear/params.md b/docs/gears/netgear/params.md index f52d70629..892e63dda 100644 --- a/docs/gears/netgear/params.md +++ b/docs/gears/netgear/params.md @@ -159,11 +159,11 @@ This parameter provides the flexibility to alter various NetGear API's internal * **`overwrite_cert`** (_boolean_) : In Secure Mode, This internal attribute decides whether to overwrite existing Public+Secret Keypair/Certificates or not, ==at the Server-end only==. More information can be found [here ➶](../advanced/secure_mode/#supported-attributes) - * **`jpeg_compression`**(_bool_): This internal attribute can be used to activate(if True)/deactivate(if False) Frame Compression. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression`**(_bool/str_): This internal attribute is used to activate(if `True`)/deactivate(if `False`) JPEG Frame Compression as well as to specify incoming frames colorspace with compression. By default colorspace is `BGR` and compression is enabled(`True`). More information can be found [here ➶](../advanced/compression/#supported-attributes) - * **`jpeg_compression_quality`**(_int/float_): This internal attribute controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression_quality`**(_int/float_): This internal attribute controls the JPEG quantization factor in JPEG Frame Compression. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. More information can be found [here ➶](../advanced/compression/#supported-attributes) - * **`jpeg_compression_fastdct`**(_bool_): This internal attributee if True, use fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) + * **`jpeg_compression_fastdct`**(_bool_): This internal attributee if True, use fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality in JPEG Frame Compression. Its default value is also `True`. More information can be found [here ➶](../advanced/compression/#supported-attributes) * **`jpeg_compression_fastupsample`**(_bool_): This internal attribute if True, use fastest color upsampling method. Its default value is `False`. More information can be found [here ➶](../advanced/compression/#supported-attributes) diff --git a/mkdocs.yml b/mkdocs.yml index 6c46839d7..47d081183 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,9 +24,6 @@ repo_name: abhiTronix/vidgear repo_url: https://github.com/abhiTronix/vidgear edit_uri: "" -# Google analytics -google_analytics: ['UA-131929464-1', 'abhitronix.github.io'] - # Copyright copyright: Copyright © 2019 - 2021 Abhishek Thakur(@abhiTronix) @@ -99,6 +96,9 @@ extra: manifest: site.webmanifest version: provider: mike + analytics: # Google analytics + provider: google + property: UA-131929464-1 extra_css: diff --git a/vidgear/gears/asyncio/__main__.py b/vidgear/gears/asyncio/__main__.py index 7f4d8ffa8..6a6283f12 100644 --- a/vidgear/gears/asyncio/__main__.py +++ b/vidgear/gears/asyncio/__main__.py @@ -1,196 +1,195 @@ -""" -=============================================== -vidgear library source-code is deployed under the Apache 2.0 License: - -Copyright (c) 2019-2020 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. -=============================================== -""" - -if __name__ == "__main__": - # import libs - import yaml - import argparse - - try: - import uvicorn - except ImportError: - raise ImportError( - "[VidGear:ERROR] :: Failed to detect correct uvicorn executables, install it with `pip3 install uvicorn` command." - ) - - # define argument parser and parse command line arguments - usage = """python -m vidgear.gears.asyncio [-h] [-m MODE] [-s SOURCE] [-ep ENABLEPICAMERA] [-S STABILIZE] - [-cn CAMERA_NUM] [-yt stream_mode] [-b BACKEND] [-cs COLORSPACE] - [-r RESOLUTION] [-f FRAMERATE] [-td TIME_DELAY] - [-ip IPADDRESS] [-pt PORT] [-l LOGGING] [-op OPTIONS]""" - - ap = argparse.ArgumentParser( - usage=usage, - description="Runs WebGear/WebGear_RTC Video Server through terminal.", - ) - ap.add_argument( - "-m", - "--mode", - type=str, - default="mjpeg", - choices=["mjpeg", "webrtc"], - help='Whether to use "MJPEG" or "WebRTC" mode for streaming.', - ) - # VideoGear API specific params - ap.add_argument( - "-s", - "--source", - default=0, - type=str, - help="Path to input source for CamGear API.", - ) - ap.add_argument( - "-ep", - "--enablePiCamera", - type=bool, - default=False, - help="Sets the flag to access PiGear(if True) or otherwise CamGear API respectively.", - ) - ap.add_argument( - "-S", - "--stabilize", - type=bool, - default=False, - help="Enables/disables real-time video stabilization.", - ) - ap.add_argument( - "-cn", - "--camera_num", - default=0, - help="Sets the camera module index that will be used by PiGear API.", - ) - ap.add_argument( - "-yt", - "--stream_mode", - default=False, - type=bool, - help="Enables YouTube Mode in CamGear API.", - ) - ap.add_argument( - "-b", - "--backend", - default=0, - type=int, - help="Sets the backend of the video source in CamGear API.", - ) - ap.add_argument( - "-cs", - "--colorspace", - type=str, - help="Sets the colorspace of the output video stream.", - ) - ap.add_argument( - "-r", - "--resolution", - default=(640, 480), - help="Sets the resolution (width,height) for camera module in PiGear API.", - ) - ap.add_argument( - "-f", - "--framerate", - default=30, - type=int, - help="Sets the framerate for camera module in PiGear API.", - ) - ap.add_argument( - "-td", - "--time_delay", - default=0, - help="Sets the time delay(in seconds) before start reading the frames.", - ) - # define WebGear exclusive params - ap.add_argument( - "-ip", - "--ipaddress", - type=str, - default="0.0.0.0", - help="Uvicorn binds the socket to this ipaddress.", - ) - ap.add_argument( - "-pt", - "--port", - type=int, - default=8000, - help="Uvicorn binds the socket to this port.", - ) - # define common params - ap.add_argument( - "-l", - "--logging", - type=bool, - default=False, - help="Enables/disables error logging, essential for debugging.", - ) - ap.add_argument( - "-op", - "--options", - type=str, - help="Sets the parameters supported by APIs(whichever being accessed) to the input videostream, \ - But make sure to wrap your dict value in single or double quotes.", - ) - args = vars(ap.parse_args()) - - options = {} - # handle `options` params - if not (args["options"] is None): - options = yaml.safe_load(args["options"]) - - if args["mode"] == "mjpeg": - from .webgear import WebGear - - # initialize WebGear object - web = WebGear( - enablePiCamera=args["enablePiCamera"], - stabilize=args["stabilize"], - source=args["source"], - camera_num=args["camera_num"], - stream_mode=args["stream_mode"], - backend=args["backend"], - colorspace=args["colorspace"], - resolution=args["resolution"], - framerate=args["framerate"], - logging=args["logging"], - time_delay=args["time_delay"], - **options - ) - else: - from .webgear_rtc import WebGear_RTC - - # initialize WebGear object - web = WebGear_RTC( - enablePiCamera=args["enablePiCamera"], - stabilize=args["stabilize"], - source=args["source"], - camera_num=args["camera_num"], - stream_mode=args["stream_mode"], - backend=args["backend"], - colorspace=args["colorspace"], - resolution=args["resolution"], - framerate=args["framerate"], - logging=args["logging"], - time_delay=args["time_delay"], - **options - ) - # run this object on Uvicorn server - uvicorn.run(web(), host=args["ipaddress"], port=args["port"]) - - if args["mode"] == "mjpeg": - # close app safely - web.shutdown() +""" +=============================================== +vidgear library source-code is deployed under the Apache 2.0 License: + +Copyright (c) 2019-2020 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. +=============================================== +""" + +if __name__ == "__main__": + # import libs + import yaml + import argparse + + try: + import uvicorn + except ImportError: + raise ImportError( + "[VidGear:ERROR] :: Failed to detect correct uvicorn executables, install it with `pip3 install uvicorn` command." + ) + + # define argument parser and parse command line arguments + usage = """python -m vidgear.gears.asyncio [-h] [-m MODE] [-s SOURCE] [-ep ENABLEPICAMERA] [-S STABILIZE] + [-cn CAMERA_NUM] [-yt stream_mode] [-b BACKEND] [-cs COLORSPACE] + [-r RESOLUTION] [-f FRAMERATE] [-td TIME_DELAY] + [-ip IPADDRESS] [-pt PORT] [-l LOGGING] [-op OPTIONS]""" + + ap = argparse.ArgumentParser( + usage=usage, + description="Runs WebGear/WebGear_RTC Video Server through terminal.", + ) + ap.add_argument( + "-m", + "--mode", + type=str, + default="mjpeg", + choices=["mjpeg", "webrtc"], + help='Whether to use "MJPEG" or "WebRTC" mode for streaming.', + ) + # VideoGear API specific params + ap.add_argument( + "-s", + "--source", + default=0, + type=str, + help="Path to input source for CamGear API.", + ) + ap.add_argument( + "-ep", + "--enablePiCamera", + type=bool, + default=False, + help="Sets the flag to access PiGear(if True) or otherwise CamGear API respectively.", + ) + ap.add_argument( + "-S", + "--stabilize", + type=bool, + default=False, + help="Enables/disables real-time video stabilization.", + ) + ap.add_argument( + "-cn", + "--camera_num", + default=0, + help="Sets the camera module index that will be used by PiGear API.", + ) + ap.add_argument( + "-yt", + "--stream_mode", + default=False, + type=bool, + help="Enables YouTube Mode in CamGear API.", + ) + ap.add_argument( + "-b", + "--backend", + default=0, + type=int, + help="Sets the backend of the video source in CamGear API.", + ) + ap.add_argument( + "-cs", + "--colorspace", + type=str, + help="Sets the colorspace of the output video stream.", + ) + ap.add_argument( + "-r", + "--resolution", + default=(640, 480), + help="Sets the resolution (width,height) for camera module in PiGear API.", + ) + ap.add_argument( + "-f", + "--framerate", + default=30, + type=int, + help="Sets the framerate for camera module in PiGear API.", + ) + ap.add_argument( + "-td", + "--time_delay", + default=0, + help="Sets the time delay(in seconds) before start reading the frames.", + ) + # define WebGear exclusive params + ap.add_argument( + "-ip", + "--ipaddress", + type=str, + default="0.0.0.0", + help="Uvicorn binds the socket to this ipaddress.", + ) + ap.add_argument( + "-pt", + "--port", + type=int, + default=8000, + help="Uvicorn binds the socket to this port.", + ) + # define common params + ap.add_argument( + "-l", + "--logging", + type=bool, + default=False, + help="Enables/disables error logging, essential for debugging.", + ) + ap.add_argument( + "-op", + "--options", + type=str, + help="Sets the parameters supported by APIs(whichever being accessed) to the input videostream, \ + But make sure to wrap your dict value in single or double quotes.", + ) + args = vars(ap.parse_args()) + + options = {} + # handle `options` params + if not (args["options"] is None): + options = yaml.safe_load(args["options"]) + + if args["mode"] == "mjpeg": + from .webgear import WebGear + + # initialize WebGear object + web = WebGear( + enablePiCamera=args["enablePiCamera"], + stabilize=args["stabilize"], + source=args["source"], + camera_num=args["camera_num"], + stream_mode=args["stream_mode"], + backend=args["backend"], + colorspace=args["colorspace"], + resolution=args["resolution"], + framerate=args["framerate"], + logging=args["logging"], + time_delay=args["time_delay"], + **options + ) + else: + from .webgear_rtc import WebGear_RTC + + # initialize WebGear object + web = WebGear_RTC( + enablePiCamera=args["enablePiCamera"], + stabilize=args["stabilize"], + source=args["source"], + camera_num=args["camera_num"], + stream_mode=args["stream_mode"], + backend=args["backend"], + colorspace=args["colorspace"], + resolution=args["resolution"], + framerate=args["framerate"], + logging=args["logging"], + time_delay=args["time_delay"], + **options + ) + # run this object on Uvicorn server + uvicorn.run(web(), host=args["ipaddress"], port=args["port"]) + + # close app safely + web.shutdown() diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 165fea04f..c515992cb 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1067,6 +1067,9 @@ def __recv_handler(self): # handle jpeg-compression encoding if self.__jpeg_compression: if self.__jpeg_compression_colorspace == "GRAY": + if return_data.ndim == 2: + # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 + return_data = return_data[:, :, np.newaxis] return_data = simplejpeg.encode_jpeg( return_data, quality=self.__jpeg_compression_quality, @@ -1258,6 +1261,9 @@ def send(self, frame, message=None): # handle JPEG compression encoding if self.__jpeg_compression: if self.__jpeg_compression_colorspace == "GRAY": + if frame.ndim == 2: + # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 + frame = frame[:, :, np.newaxis] frame = simplejpeg.encode_jpeg( frame, quality=self.__jpeg_compression_quality, From e4a61874a16288b9269a081b9fd725dff8182f13 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Thu, 15 Jul 2021 15:28:08 +0530 Subject: [PATCH 059/112] :art: NetGear: Bug fixes and updated CI tests - :construction_worker: Updated existing CI tests to cover new frame compression functionality. - :bug: Fixed bug that cause server end frame dimensions differ from client's end when frame compression enabled. - :zap: Added `np.squeeze` to drop grayscale frame's 3rd dimension on recieving end. - :zap: Replaced `np.newaxis` with `np.expand_dims`. - :fire: Removed redundant code from CI tests. --- vidgear/gears/netgear.py | 12 ++- vidgear/tests/network_tests/test_netgear.py | 95 ++++++++++++++------- 2 files changed, 77 insertions(+), 30 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index c515992cb..56d911b54 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -1159,6 +1159,9 @@ def __recv_handler(self): raise RuntimeError( "[NetGear:ERROR] :: Received compressed JPEG frame decoding failed" ) + if msg_json["compression"]["colorspace"] == "GRAY" and frame.ndim == 3: + # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 + frame = np.squeeze(frame, axis=2) else: # recover and reshape frame from buffer frame_buffer = np.frombuffer(msg_data, dtype=msg_json["dtype"]) @@ -1263,7 +1266,7 @@ def send(self, frame, message=None): if self.__jpeg_compression_colorspace == "GRAY": if frame.ndim == 2: # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 - frame = frame[:, :, np.newaxis] + frame = np.expand_dims(frame, axis=2) frame = simplejpeg.encode_jpeg( frame, quality=self.__jpeg_compression_quality, @@ -1399,6 +1402,13 @@ def send(self, frame, message=None): self.__ex_compression_params, ) ) + + if ( + recv_json["compression"]["colorspace"] == "GRAY" + and recvd_data.ndim == 3 + ): + # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 + recvd_data = np.squeeze(recvd_data, axis=2) else: recvd_data = np.frombuffer( recv_array, dtype=recv_json["array_dtype"] diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 7294036b6..6f22eccec 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -172,28 +172,30 @@ def test_patterns(pattern): @pytest.mark.parametrize( - "options_client", + "options_server", [ - {"compression_format": None, "compression_param": cv2.IMREAD_UNCHANGED}, { - "compression_format": ".jpg", - "compression_param": [cv2.IMWRITE_JPEG_QUALITY, 80], + "jpeg_compression": "invalid", + "jpeg_compression_quality": 5, + }, + { + "jpeg_compression": " gray ", + "jpeg_compression_quality": 50, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, + }, + { + "jpeg_compression": True, + "jpeg_compression_quality": 55.55, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, }, ], ) -def test_compression(options_client): +def test_compression(options_server): """ Testing NetGear's real-time frame compression capabilities """ - options = { - "compression_format": ".jpg", - "compression_param": [ - cv2.IMWRITE_JPEG_QUALITY, - 20, - cv2.IMWRITE_JPEG_OPTIMIZE, - True, - ], - } # JPEG compression # initialize stream = None server = None @@ -201,9 +203,17 @@ def test_compression(options_client): try: # open streams options_gear = {"THREAD_TIMEOUT": 60} - stream = VideoGear(source=return_testvideo_path(), **options_gear).start() - client = NetGear(pattern=0, receive_mode=True, logging=True, **options_client) - server = NetGear(pattern=0, logging=True, **options) + colorspace = ( + "COLOR_BGR2GRAY" + if isinstance(options_server["jpeg_compression"], str) + and options_server["jpeg_compression"].strip().upper() == "GRAY" + else None + ) + stream = VideoGear( + source=return_testvideo_path(), colorspace=colorspace, **options_gear + ).start() + client = NetGear(pattern=0, receive_mode=True, logging=True) + server = NetGear(pattern=0, logging=True, **options_server) # send over network while True: frame_server = stream.read() @@ -211,6 +221,13 @@ def test_compression(options_client): break server.send(frame_server) frame_client = client.recv() + if ( + isinstance(options_server["jpeg_compression"], str) + and options_server["jpeg_compression"].strip().upper() == "GRAY" + ): + assert ( + frame_server.ndim == frame_client.ndim + ), "Grayscale frame Test Failed!" except Exception as e: if isinstance(e, (ZMQError, ValueError, RuntimeError, queue.Empty)): logger.exception(str(e)) @@ -287,26 +304,40 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer @pytest.mark.parametrize( "pattern, target_data, options", [ - (0, [1, "string", ["list"]], {"bidirectional_mode": True}), + ( + 0, + [1, "string", ["list"]], + { + "bidirectional_mode": True, + "jpeg_compression": ["invalid"], + }, + ), ( 1, (np.random.random(size=(480, 640, 3)) * 255).astype(np.uint8), { "bidirectional_mode": True, - "jpeg_compression_quality": 55.0, - "jpeg_compression_fastdct": True, - "jpeg_compression_fastupsample": True, + "jpeg_compression": False, + "jpeg_compression_quality": 55, + "jpeg_compression_fastdct": False, + "jpeg_compression_fastupsample": False, }, ), ( - 2, + 1, { - "jpeg_compression": False, 1: "apple", 2: "cat", - "jpeg_compression_quality": 5, }, - {"bidirectional_mode": True}, + {"bidirectional_mode": True, "jpeg_compression": "GRAY"}, + ), + ( + 2, + (np.random.random(size=(480, 640, 3)) * 255).astype(np.uint8), + { + "bidirectional_mode": True, + "jpeg_compression": True, + }, ), ], ) @@ -322,7 +353,15 @@ def test_bidirectional_mode(pattern, target_data, options): logger.debug("Given Input Data: {}".format(target_data)) # open stream options_gear = {"THREAD_TIMEOUT": 60} - stream = VideoGear(source=return_testvideo_path(), **options_gear).start() + colorspace = ( + "COLOR_BGR2GRAY" + if isinstance(options["jpeg_compression"], str) + and options["jpeg_compression"].strip().upper() == "GRAY" + else None + ) + stream = VideoGear( + source=return_testvideo_path(), colorspace=colorspace, **options_gear + ).start() # define params client = NetGear(pattern=pattern, receive_mode=True, **options) server = NetGear(pattern=pattern, **options) @@ -340,8 +379,6 @@ def test_bidirectional_mode(pattern, target_data, options): # logger.debug data received at client-end and server-end logger.debug("Data received at Server-end: {}".format(frame_client)) logger.debug("Data received at Client-end: {}".format(client_data)) - if "jpeg_compression" in options and options["jpeg_compression"] == False: - assert np.array_equal(client_data, frame_client) else: # sent frame and data from server to client server.send(frame_server, message=target_data) @@ -350,7 +387,7 @@ def test_bidirectional_mode(pattern, target_data, options): # server receives the data and cycle continues client_data = server.send(frame_server, message=target_data) # check if received frame exactly matches input frame - if "jpeg_compression" in options and options["jpeg_compression"] == False: + if not options["jpeg_compression"] in [True, "GRAY", ["invalid"]]: assert np.array_equal(frame_server, frame_client) # logger.debug data received at client-end and server-end logger.debug("Data received at Server-end: {}".format(server_data)) From 68f32e66a1a4a9ad32c473bb71ef34cb714579d9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 16 Jul 2021 07:44:39 +0530 Subject: [PATCH 060/112] :art: NetGear: More updates - :zap: Replaced `random` module with `secrets` while generating system ID. - :children_crossing: Moved `pyzmq` import globally. - :construction_worker: Improved code coverage. --- vidgear/gears/netgear.py | 66 +++++++++------------ vidgear/tests/network_tests/test_netgear.py | 10 ++-- 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 56d911b54..70f9e269c 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -21,14 +21,18 @@ import os import cv2 +import zmq import time +import string +import secrets import simplejpeg + import numpy as np -import random import logging as log + +from zmq.error import ZMQError from threading import Thread from collections import deque - from .helper import ( logger_handler, generate_auth_certificates, @@ -120,22 +124,6 @@ def __init__( logging (bool): enables/disables logging. options (dict): provides the flexibility to alter various NetGear internal properties. """ - - try: - # import PyZMQ library - import zmq - from zmq.error import ZMQError - - # assign values to global variable for further use - self.__zmq = zmq - self.__ZMQError = ZMQError - - except ImportError as error: - # raise error - raise ImportError( - "[NetGear:ERROR] :: pyzmq python library not installed. Kindly install it with `pip install pyzmq` command." - ) - # enable logging if specified self.__logging = True if logging else False @@ -218,8 +206,10 @@ def __init__( # define receiver return data handler self.__return_data = None - # generate random system id - self.__id = "".join(random.choice("0123456789ABCDEF") for i in range(5)) + # generate 8-digit random system id + self.__id = "".join( + secrets.choice(string.ascii_uppercase + string.digits) for i in range(8) + ) # define termination flag self.__terminate = False @@ -966,9 +956,9 @@ def __recv_handler(self): if self.__pattern < 2: socks = dict(self.__poll.poll(self.__request_timeout * 3)) - if socks.get(self.__msg_socket) == self.__zmq.POLLIN: + if socks.get(self.__msg_socket) == zmq.POLLIN: msg_json = self.__msg_socket.recv_json( - flags=self.__msg_flag | self.__zmq.DONTWAIT + flags=self.__msg_flag | zmq.DONTWAIT ) else: logger.critical("No response from Server(s), Reconnecting again...") @@ -998,7 +988,7 @@ def __recv_handler(self): logger.exception(str(e)) self.__terminate = True raise RuntimeError("API failed to restart the Client-end!") - self.__poll.register(self.__msg_socket, self.__zmq.POLLIN) + self.__poll.register(self.__msg_socket, zmq.POLLIN) continue else: @@ -1042,7 +1032,7 @@ def __recv_handler(self): continue msg_data = self.__msg_socket.recv( - flags=self.__msg_flag | self.__zmq.DONTWAIT, + flags=self.__msg_flag | zmq.DONTWAIT, copy=self.__msg_copy, track=self.__msg_track, ) @@ -1111,7 +1101,7 @@ def __recv_handler(self): # send the json dict self.__msg_socket.send_json( - return_dict, self.__msg_flag | self.__zmq.SNDMORE + return_dict, self.__msg_flag | zmq.SNDMORE ) # send the array with correct flags self.__msg_socket.send( @@ -1304,7 +1294,7 @@ def send(self, frame, message=None): ) # send the json dict - self.__msg_socket.send_json(msg_dict, self.__msg_flag | self.__zmq.SNDMORE) + self.__msg_socket.send_json(msg_dict, self.__msg_flag | zmq.SNDMORE) # send the frame array with correct flags self.__msg_socket.send( frame, flags=self.__msg_flag, copy=self.__msg_copy, track=self.__msg_track @@ -1319,13 +1309,13 @@ def send(self, frame, message=None): recvd_data = None socks = dict(self.__poll.poll(self.__request_timeout)) - if socks.get(self.__msg_socket) == self.__zmq.POLLIN: + if socks.get(self.__msg_socket) == zmq.POLLIN: # handle return data recv_json = self.__msg_socket.recv_json(flags=self.__msg_flag) else: logger.critical("No response from Client, Reconnecting again...") # Socket is confused. Close and remove it. - self.__msg_socket.setsockopt(self.__zmq.LINGER, 0) + self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() self.__poll.unregister(self.__msg_socket) self.__max_retries -= 1 @@ -1364,7 +1354,7 @@ def send(self, frame, message=None): else: # connect normally self.__msg_socket.connect(self.__connection_address) - self.__poll.register(self.__msg_socket, self.__zmq.POLLIN) + self.__poll.register(self.__msg_socket, zmq.POLLIN) # return None for mean-time return None @@ -1424,12 +1414,12 @@ def send(self, frame, message=None): else: # otherwise log normally socks = dict(self.__poll.poll(self.__request_timeout)) - if socks.get(self.__msg_socket) == self.__zmq.POLLIN: + if socks.get(self.__msg_socket) == zmq.POLLIN: recv_confirmation = self.__msg_socket.recv() else: logger.critical("No response from Client, Reconnecting again...") # Socket is confused. Close and remove it. - self.__msg_socket.setsockopt(self.__zmq.LINGER, 0) + self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() self.__poll.unregister(self.__msg_socket) self.__max_retries -= 1 @@ -1457,7 +1447,7 @@ def send(self, frame, message=None): else: # connect normally self.__msg_socket.connect(self.__connection_address) - self.__poll.register(self.__msg_socket, self.__zmq.POLLIN) + self.__poll.register(self.__msg_socket, zmq.POLLIN) return None @@ -1505,9 +1495,9 @@ def close(self): ): try: # properly close the socket - self.__msg_socket.setsockopt(self.__zmq.LINGER, 0) + self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() - except self.__ZMQError: + except ZMQError: pass finally: # exit @@ -1532,16 +1522,14 @@ def close(self): if self.__pattern < 2: if self.__logging: logger.debug("Terminating. Please wait...") - if self.__msg_socket.poll( - self.__request_timeout // 5, self.__zmq.POLLIN - ): + if self.__msg_socket.poll(self.__request_timeout // 5, zmq.POLLIN): self.__msg_socket.recv() except Exception as e: - if not isinstance(e, self.__ZMQError): + if not isinstance(e, ZMQError): logger.exception(str(e)) finally: # properly close the socket - self.__msg_socket.setsockopt(self.__zmq.LINGER, 0) + self.__msg_socket.setsockopt(zmq.LINGER, 0) self.__msg_socket.close() if self.__logging: logger.debug("Terminated Successfully!") diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 6f22eccec..10f755e98 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -314,7 +314,10 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer ), ( 1, - (np.random.random(size=(480, 640, 3)) * 255).astype(np.uint8), + { + 1: "apple", + 2: "cat", + }, { "bidirectional_mode": True, "jpeg_compression": False, @@ -325,10 +328,7 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer ), ( 1, - { - 1: "apple", - 2: "cat", - }, + (np.random.random(size=(480, 640, 3)) * 255).astype(np.uint8), {"bidirectional_mode": True, "jpeg_compression": "GRAY"}, ), ( From 4f853d7d866ea82d350445831aca5d0e6510762e Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 16 Jul 2021 08:51:01 +0530 Subject: [PATCH 061/112] :bug: NetGear: Fixed import bugs. --- vidgear/gears/netgear.py | 46 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 70f9e269c..1a0284a49 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -30,6 +30,9 @@ import numpy as np import logging as log +from zmq import ssh +from zmq import auth +from zmq.auth.thread import ThreadAuthenticator from zmq.error import ZMQError from threading import Thread from collections import deque @@ -383,10 +386,6 @@ def __init__( # Handle Secure mode if self.__secure_mode: - # import required libs - import zmq.auth - from zmq.auth.thread import ThreadAuthenticator - # activate and log if overwriting is enabled if overwrite_cert: if not receive_mode: @@ -472,11 +471,8 @@ def __init__( ) # import packages - import zmq.ssh import importlib - # assign globally - self.__zmq_ssh = zmq.ssh self.__paramiko_present = ( True if bool(importlib.util.find_spec("paramiko")) else False ) @@ -574,20 +570,20 @@ def __init__( # activate secure_mode threaded authenticator if self.__secure_mode > 0: # start an authenticator for this context - auth = ThreadAuthenticator(self.__msg_context) - auth.start() - auth.allow(str(address)) # allow current address + z_auth = ThreadAuthenticator(self.__msg_context) + z_auth.start() + 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 - auth.configure_curve( + 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 - auth.configure_curve( - domain="*", location=zmq.auth.CURVE_ALLOW_ANY + z_auth.configure_curve( + domain="*", location=auth.CURVE_ALLOW_ANY ) # define thread-safe messaging socket @@ -603,7 +599,7 @@ def __init__( server_secret_file = os.path.join( self.__auth_secretkeys_dir, "server.key_secret" ) - server_public, server_secret = zmq.auth.load_certificate( + server_public, server_secret = auth.load_certificate( server_secret_file ) # load all CURVE keys @@ -774,20 +770,20 @@ def __init__( # activate secure_mode threaded authenticator if self.__secure_mode > 0: # start an authenticator for this context - auth = ThreadAuthenticator(self.__msg_context) - auth.start() - auth.allow(str(address)) # allow current address + z_auth = ThreadAuthenticator(self.__msg_context) + z_auth.start() + 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 - auth.configure_curve( + 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 - auth.configure_curve( - domain="*", location=zmq.auth.CURVE_ALLOW_ANY + z_auth.configure_curve( + domain="*", location=auth.CURVE_ALLOW_ANY ) # define thread-safe messaging socket @@ -808,7 +804,7 @@ def __init__( client_secret_file = os.path.join( self.__auth_secretkeys_dir, "client.key_secret" ) - client_public, client_secret = zmq.auth.load_certificate( + client_public, client_secret = auth.load_certificate( client_secret_file ) # load all CURVE keys @@ -818,7 +814,7 @@ def __init__( server_public_file = os.path.join( self.__auth_publickeys_dir, "server.key" ) - server_public, _ = zmq.auth.load_certificate(server_public_file) + server_public, _ = auth.load_certificate(server_public_file) # inject public key to make a CURVE connection. self.__msg_socket.curve_serverkey = server_public @@ -833,7 +829,7 @@ def __init__( # handle SSH tuneling if enabled if self.__ssh_tunnel_mode: # establish tunnel connection - self.__zmq_ssh.tunnel_connection( + ssh.tunnel_connection( self.__msg_socket, protocol + "://" + str(address) + ":" + str(port), self.__ssh_tunnel_mode, @@ -1343,7 +1339,7 @@ def send(self, frame, message=None): # handle SSH tunneling if enabled if self.__ssh_tunnel_mode: # establish tunnel connection - self.__zmq_ssh.tunnel_connection( + ssh.tunnel_connection( self.__msg_socket, self.__connection_address, self.__ssh_tunnel_mode, @@ -1436,7 +1432,7 @@ def send(self, frame, message=None): # handle SSH tunneling if enabled if self.__ssh_tunnel_mode: # establish tunnel connection - self.__zmq_ssh.tunnel_connection( + ssh.tunnel_connection( self.__msg_socket, self.__connection_address, self.__ssh_tunnel_mode, From 6fbe204423d2ad9824efc52ab0a6393360ede238 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 16 Jul 2021 10:52:19 +0530 Subject: [PATCH 062/112] :construction_worker: CI: Updated macOS VM Image to `latest` in azure devops. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c8350cdfb..8f904c42e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,7 @@ pr: - testing pool: - vmImage: 'macOS-10.14' + vmImage: 'macOS-latest' strategy: matrix: From 4b8946d92abc3bb4a53902ef2f5a30603867c843 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 17 Jul 2021 08:22:28 +0530 Subject: [PATCH 063/112] :construction_worker: CI: Added new tests and improved code-coverage --- vidgear/tests/network_tests/test_netgear.py | 69 ++++++++++++++++----- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 10f755e98..5071b20de 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -20,6 +20,7 @@ # import the necessary packages import os +import platform import queue import cv2 import numpy as np @@ -153,7 +154,7 @@ def test_patterns(pattern): assert not (frame_server is None) # send frame over network server.send(frame_server) - frame_client = client.recv() + frame_client = client.recv(return_data=[1, 2, 3] if pattern == 2 else None) # check if received frame exactly matches input frame assert np.array_equal(frame_server, frame_client) except Exception as e: @@ -246,7 +247,14 @@ def test_compression(options_server): test_data_class = [ (0, 1, tempfile.gettempdir(), True), (0, 1, ["invalid"], True), - (1, 1, "unknown://invalid.com/", False), + ( + 1, + 2, + os.path.abspath(os.sep) + if platform.system() == "Linux" + else "unknown://invalid.com/", + False, + ), ] @@ -350,24 +358,29 @@ def test_bidirectional_mode(pattern, target_data, options): server = None client = None try: - logger.debug("Given Input Data: {}".format(target_data)) + logger.debug( + "Given Input Data: {}".format( + target_data if not isinstance(target_data, np.ndarray) else "IMAGE" + ) + ) # open stream options_gear = {"THREAD_TIMEOUT": 60} + # change colorspace colorspace = ( "COLOR_BGR2GRAY" if isinstance(options["jpeg_compression"], str) and options["jpeg_compression"].strip().upper() == "GRAY" else None ) + if colorspace == "COLOR_BGR2GRAY" and isinstance(target_data, np.ndarray): + target_data = cv2.cvtColor(target_data, cv2.COLOR_BGR2GRAY) + stream = VideoGear( source=return_testvideo_path(), colorspace=colorspace, **options_gear ).start() # define params - client = NetGear(pattern=pattern, receive_mode=True, **options) - server = NetGear(pattern=pattern, **options) - # get frame from stream - frame_server = stream.read() - assert not (frame_server is None) + client = NetGear(pattern=pattern, receive_mode=True, logging=True, **options) + server = NetGear(pattern=pattern, logging=True, **options) # check if target data is numpy ndarray if isinstance(target_data, np.ndarray): # sent frame and data from server to client @@ -375,11 +388,13 @@ def test_bidirectional_mode(pattern, target_data, options): # client receives the data and frame and send its data server_data, frame_client = client.recv(return_data=target_data) # server receives the data and cycle continues - client_data = server.send(target_data, message=target_data) - # logger.debug data received at client-end and server-end - logger.debug("Data received at Server-end: {}".format(frame_client)) - logger.debug("Data received at Client-end: {}".format(client_data)) + client_data = server.send(target_data) + # test if recieved successfully + assert not (client_data is None), "Test Failed!" else: + # get frame from stream + frame_server = stream.read() + assert not (frame_server is None) # sent frame and data from server to client server.send(frame_server, message=target_data) # client receives the data and frame and send its data @@ -609,13 +624,13 @@ def test_multiclient_mode(pattern): "ssh_tunnel_pwd": "xyz", "ssh_tunnel_keyfile": "ok.txt", }, - {"max_retries": 2, "request_timeout": 2, "multiclient_mode": True}, - {"max_retries": 2, "request_timeout": 2, "multiserver_mode": True}, + {"max_retries": 2, "request_timeout": 4, "multiclient_mode": True}, + {"max_retries": 2, "request_timeout": -1, "multiserver_mode": True}, ], ) def test_client_reliablity(options): """ - Testing validation function of WebGear API + Testing validation function of NetGear API """ client = None frame_client = None @@ -671,7 +686,7 @@ def test_client_reliablity(options): ) def test_server_reliablity(options): """ - Testing validation function of WebGear API + Testing validation function of NetGear API """ server = None stream = None @@ -706,3 +721,25 @@ def test_server_reliablity(options): stream.release() if not (server is None): server.close() + + +@pytest.mark.parametrize( + "server_ports, client_ports, options", + [ + (0, 5555, {"multiserver_mode": True}), + (5555, 0, {"multiclient_mode": True}), + ], +) +@pytest.mark.xfail(raises=ValueError) +def test_ports(server_ports, client_ports, options): + """ + Test made to fail on wrong port values + """ + if server_ports: + server = NetGear(pattern=1, port=server_ports, logging=True, **options) + server.close() + else: + client = NetGear( + port=client_ports, pattern=1, receive_mode=True, logging=True, **options + ) + client.close() From 5dfed5e8277e03d408cb84c6592f3366bc3f5daf Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 17 Jul 2021 18:39:40 +0530 Subject: [PATCH 064/112] :bug: WebGear_RTC: More fixes to eventloop. --- .../asyncio_tests/test_webgear_rtc.py | 44 ++++--------------- 1 file changed, 9 insertions(+), 35 deletions(-) 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 11e66a184..d36b0b23f 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -54,15 +54,12 @@ logger.setLevel(log.DEBUG) -# handles 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): - logger.info("Setting Event loop policy for windows.") - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) +@pytest.fixture +def event_loop(): + """Create an instance of the default event loop for each test case.""" + loop = asyncio.SelectorEventLoop() + yield loop + loop.close() def return_testvideo_path(): @@ -75,23 +72,6 @@ def return_testvideo_path(): return os.path.abspath(path) -def patch_eventlooppolicy(): - """ - Fixes event loop policy on newer python versions - """ - # Retrieve event loop and assign it - if platform.system() == "Windows": - if sys.version_info[:2] >= (3, 8) and not isinstance( - asyncio.get_event_loop_policy(), asyncio.WindowsSelectorEventLoopPolicy - ): - logger.info("Resetting Event loop policy for windows.") - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - loop = asyncio.get_event_loop() - logger.debug( - "Using `{}` event loop for this process.".format(loop.__class__.__name__) - ) - - class VideoTransformTrack(MediaStreamTrack): """ A video stream track that transforms frames from an another track. @@ -144,7 +124,6 @@ class Custom_RTCServer(VideoStreamTrack): """ def __init__(self, source=None): - # don't forget this line! super().__init__() @@ -189,7 +168,6 @@ class Invalid_Custom_RTCServer_1(VideoStreamTrack): """ def __init__(self, source=None): - # don't forget this line! super().__init__() @@ -243,7 +221,6 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): """ Test for various WebGear_RTC API parameters """ - patch_eventlooppolicy() try: web = WebGear_RTC( source=source, @@ -309,7 +286,6 @@ async def test_webgear_rtc_options(options): """ Test for various WebGear_RTC API internal options """ - patch_eventlooppolicy() web = None try: web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) @@ -365,7 +341,6 @@ async def test_webpage_reload(options): Test for testing WebGear_RTC API against Webpage reload disruptions """ - patch_eventlooppolicy() web = WebGear_RTC(source=return_testvideo_path(), logging=True, **options) try: # run webgear_rtc @@ -445,7 +420,6 @@ async def test_webgear_rtc_custom_server_generator(server, result): """ Test for WebGear_RTC API's custom source """ - patch_eventlooppolicy() web = WebGear_RTC(logging=True) web.config["server"] = server async with TestClient(web()) as client: @@ -466,7 +440,6 @@ async def test_webgear_rtc_custom_middleware(middleware, result): """ Test for WebGear_RTC API's custom middleware """ - patch_eventlooppolicy() try: web = WebGear_RTC(source=return_testvideo_path(), logging=True) web.middleware = middleware @@ -484,7 +457,6 @@ async def test_webgear_rtc_routes(): """ Test for WebGear_RTC API's custom routes """ - patch_eventlooppolicy() try: # add various performance tweaks as usual options = { @@ -526,8 +498,10 @@ async def test_webgear_rtc_routes(): @pytest.mark.asyncio async def test_webgear_rtc_routes_validity(): + """ + Test WebGear_RTC Routes + """ # add various tweaks for testing only - patch_eventlooppolicy() options = { "enable_infinite_frames": False, "enable_live_broadcast": True, From 71c693c1ffbc61472dacac4957a1120a96f2a314 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 18 Jul 2021 09:18:21 +0530 Subject: [PATCH 065/112] :memo: Docs: Updated context and some minor tweaks. --- README.md | 32 +++--- docs/contribution/PR.md | 16 +-- docs/gears.md | 2 +- .../netgear/advanced/bidirectional_mode.md | 2 +- docs/gears/netgear/advanced/compression.md | 2 +- docs/gears/netgear/advanced/ssh_tunnel.md | 9 +- docs/help.md | 6 +- docs/help/stabilizer_faqs.md | 2 +- docs/index.md | 28 +++--- docs/installation/pip_install.md | 98 +++++++++++-------- docs/installation/source_install.md | 52 +++++----- docs/switch_from_cv.md | 8 +- mkdocs.yml | 4 +- 13 files changed, 144 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 250111d90..7754a8c8d 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ limitations under the License. [Releases][release]   |   [Gears][gears]   |   [Documentation][docs]   |   [Installation][installation]   |   [License](#license) -[![Build Status][github-cli]][github-flow] [![Codecov branch][codecov]][code] [![Build Status][appveyor]][app] +[![Build Status][github-cli]][github-flow] [![Build Status][appveyor]][app] [![Azure DevOps builds (branch)][azure-badge]][azure-pipeline] -[![Azure DevOps builds (branch)][azure-badge]][azure-pipeline] [![PyPi version][pypi-badge]][pypi] [![Glitter chat][gitter-bagde]][gitter] +[![PyPi version][pypi-badge]][pypi] [![Codecov branch][codecov]][code] [![Glitter chat][gitter-bagde]][gitter] [![Code Style][black-badge]][black]
@@ -67,10 +67,10 @@ The following **functional block diagram** clearly depicts the generalized funct * [**WebGear**](#webgear) * [**WebGear_RTC**](#webgear_rtc) * [**NetGear_Async**](#netgear_async) -* [**Community Channel**](#community-channel) -* [**Contributions & Support**](#contributions--support) - * [**Support**](#support) +* [**Contributions & Community Support**](#contributions--community-support) + * [**Community Support**](community-support) * [**Contributors**](#contributors) +* [**Donations**](#donations) * [**Citation**](#citation) * [**Copyright**](#copyright) @@ -631,19 +631,17 @@ Whereas supported protocol are: `tcp` and `ipc`.   -# Contributions & Support +# Contributions & Community Support -Contributions are welcome. We'd love to have your contributions to VidGear to fix bugs or to implement new features! +> Contributions are welcome. We'd love to have your contributions to fix bugs or to implement new features! Please see our **[Contribution Guidelines](contributing.md)** for more details. -### Support +### Community Support -PiGear - -Donations help keep VidGear's Development alive. Giving a little means a lot, even the smallest contribution can make a huge difference. +We ask contributors to join the Gitter community channel for quick discussions: -[![ko-fi][kofi-badge]][kofi] +[![Gitter](https://badges.gitter.im/vidgear/community.svg)](https://gitter.im/vidgear/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) ### Contributors @@ -657,9 +655,15 @@ Donations help keep VidGear's Development alive. Giving a little means a lot, ev   -# Community Channel +# Donations + +PiGear + +> VidGear is free and open source and will always remain so. :heart: -If you've come up with some new idea, or looking for the fastest way troubleshoot your problems, then *join our [Gitter community channel ➶][gitter]* +It is (like all open source software) a labour of love and something I am doing with my own free time. If you would like to say thanks, please feel free to make a donation: + +[![ko-fi][kofi-badge]][kofi]   diff --git a/docs/contribution/PR.md b/docs/contribution/PR.md index cf1d73138..19b4c3e75 100644 --- a/docs/contribution/PR.md +++ b/docs/contribution/PR.md @@ -184,22 +184,22 @@ All Pull Request(s) must be tested, formatted & linted against our library stand Testing VidGear requires additional test dependencies and dataset, which can be handled manually as follows: -* **Install additional python libraries:** +- [x] **Install additional python libraries:** You can easily install these dependencies via pip: - ??? warning "Note for Windows" - The [`mpegdash`](https://github.com/sangwonl/python-mpegdash) library has not yet been updated and bugs on windows machines. Kindly instead try the forked [DEV-version of `mpegdash`](https://github.com/abhiTronix/python-mpegdash) as follows: + ??? info "MPEGDASH for Windows" + The [`mpegdash`](https://github.com/sangwonl/python-mpegdash) library has not yet been updated and bugs on windows machines. Therefore install the forked [DEV-version of `mpegdash`](https://github.com/abhiTronix/python-mpegdash) as follows: ```sh python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev/mpegdash-0.3.0.dev0-py3-none-any.whl ``` ```sh - pip install --upgrade six, flake8, black, pytest, pytest-asyncio, mpegdash + pip install --upgrade six flake8 black pytest pytest-asyncio mpegdash paramiko async-asgi-testclient ``` -* **Download Tests Dataset:** +- [x] **Download Tests Dataset:** To perform tests, you also need to download additional dataset *(to your temp dir)* by running [`prepare_dataset.sh`](https://github.com/abhiTronix/vidgear/blob/master/scripts/bash/prepare_dataset.sh) bash script as follows: @@ -223,13 +223,13 @@ All tests can be run with [`pytest`](https://docs.pytest.org/en/stable/)(*in Vid For formatting and linting, following libraries are used: -* **Flake8:** You must run [`flake8`](https://flake8.pycqa.org/en/latest/manpage.html) linting for checking the code base against the coding style (PEP8), programming errors and other cyclomatic complexity: +- [x] **Flake8:** You must run [`flake8`](https://flake8.pycqa.org/en/latest/manpage.html) linting for checking the code base against the coding style (PEP8), programming errors and other cyclomatic complexity: ```sh - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 {source_file_or_directory} --count --select=E9,F63,F7,F82 --show-source --statistics ``` -* **Black:** Vidgear follows [`black`](https://github.com/psf/black) formatting to make code review faster by producing the smallest diffs possible. You must run it with sensible defaults as follows: +- [x] **Black:** Vidgear follows [`black`](https://github.com/psf/black) formatting to make code review faster by producing the smallest diffs possible. You must run it with sensible defaults as follows: ```sh black {source_file_or_directory} diff --git a/docs/gears.md b/docs/gears.md index f3cb879c8..40c3a0e38 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -29,7 +29,7 @@ limitations under the License. VidGear is built on Standalone APIs - also known as **Gears**, each with some unique functionality. Each Gears is designed exclusively to handle/control/process different data-specific & device-specific video streams, network streams, and media encoders/decoders. -These Gears allows users to work with an inherently optimized, easy-to-use, extensible, and exposed API Framework over many state-of-the-art libraries, while silently delivering robust error handling and unmatched real-time performance. +Gears allows users to work with an inherently optimized, easy-to-use, extensible, and exposed API Framework on top of many state-of-the-art libraries, while silently delivering robust error handling and unmatched real-time performance. ## Gears Classification diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index 59464c4b8..4ded72358 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -365,7 +365,7 @@ In this example we are going to implement a bare-minimum example, where we will !!! tip "This feature is great for building applications like Real-Time Video Chat." -!!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#reducer) method for reducing frame-size on-the-go for additional performance." +!!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#vidgear.gears.helper.reducer--reducer) method for reducing frame-size on-the-go for additional performance." !!! warning "Remember, Sending large HQ video-frames may required more network bandwidth and packet size which may lead to video latency!" diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index 394509507..c3b4f3421 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -462,7 +462,7 @@ In this example we are going to implement a bare-minimum example, where we will !!! note "This Dual Frame Compression feature also available for [Multi-Clients](../../advanced/multi_client/) Mode." -!!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#reducer) Helper method for reducing frame-size on-the-go for additional performance." +!!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#vidgear.gears.helper.reducer--reducer) Helper method for reducing frame-size on-the-go for additional performance." !!! success "Remember to define Frame Compression's [performance attributes](#performance-attributes) both on Server and Client ends in Dual Frame Compression to boost performance bidirectionally!" diff --git a/docs/gears/netgear/advanced/ssh_tunnel.md b/docs/gears/netgear/advanced/ssh_tunnel.md index 20a53419e..2f13be1b1 100644 --- a/docs/gears/netgear/advanced/ssh_tunnel.md +++ b/docs/gears/netgear/advanced/ssh_tunnel.md @@ -20,10 +20,11 @@ limitations under the License. # SSH Tunneling Mode for NetGear API -
- NetGear's SSH Tunneling Mode -
NetGear's SSH Tunneling Mode
-
+

+ NetGear's SSH Tunneling Mode +
NetGear's Bidirectional Mode
+

+ ## Overview diff --git a/docs/help.md b/docs/help.md index 08426b2bd..d7914ea36 100644 --- a/docs/help.md +++ b/docs/help.md @@ -73,9 +73,11 @@ Let others know how you are using VidGear and why you like it!   -## Help Author +## Helping Author -Donations help keep VidGear's Development alive and motivate me _(author)_. Giving a little means a lot, even the smallest contribution can make a huge difference. You can financially support through ko-fi 🤝: +> Donations help keep VidGear's development alive and motivate me _(as author)_. :heart: + +It is (like all open source software) a labour of love and something I am doing with my own free time. If you would like to say thanks, please feel free to make a donation through ko-fi: diff --git a/docs/help/stabilizer_faqs.md b/docs/help/stabilizer_faqs.md index 5a33629c1..3855575dc 100644 --- a/docs/help/stabilizer_faqs.md +++ b/docs/help/stabilizer_faqs.md @@ -30,7 +30,7 @@ limitations under the License. ## How much latency you would typically expect with Stabilizer Class? -**Answer:** The stabilizer will be Slower for High-Quality videos-frames. Try reducing frames size _(Use [`reducer()`](../../bonus/reference/helper/#reducer) method)_ before feeding them for reducing latency. Also, see [`smoothing_radius`](../../gears/stabilizer/params/#smoothing_radius) parameter of Stabilizer class that handles the quality of stabilization at the expense of latency and sudden panning. The larger its value, the less will be panning, more will be latency, and vice-versa. +**Answer:** The stabilizer will be Slower for High-Quality videos-frames. Try reducing frames size _(Use [`reducer()`](../../bonus/reference/helper/#vidgear.gears.helper.reducer--reducer) method)_ before feeding them for reducing latency. Also, see [`smoothing_radius`](../../gears/stabilizer/params/#smoothing_radius) parameter of Stabilizer class that handles the quality of stabilization at the expense of latency and sudden panning. The larger its value, the less will be panning, more will be latency, and vice-versa.   diff --git a/docs/index.md b/docs/index.md index 58a496952..ad60c63a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,13 +40,17 @@ VidGear focuses on simplicity, and thereby lets programmers and software develop ## Getting Started -- [x] If this is your first time using VidGear, head straight to the [Installation ➶](installation.md) to install VidGear. +!!! tip "In case you're run into any problems, consult the [Help](help/get_help) section." -- [x] Once you have VidGear installed, **Checkout its Function-Specific [Gears ➶](gears.md)** +- [x] If this is your first time using VidGear, head straight to the [**Installation**](installation.md) to install VidGear. + +- [x] Once you have VidGear installed, Checkout its **[Function-Specific Gears](gears.md)**. + +- [x] Also, if you're already familar with [**OpenCV**][opencv] library, then see **[Switching from OpenCV Library](switch_from_cv.md)**. + +!!! alert "If you're just getting started with OpenCV-Python, then see [here ➶](../help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects)" -- [x] Also, if you're already familar with [OpenCV][opencv] library, then see [Switching from OpenCV Library ➶](switch_from_cv.md) -- [x] Or, if you're just getting started with OpenCV with Python, then see [here ➶](help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects)   @@ -63,7 +67,7 @@ These Gears can be classified as follows: * [CamGear](gears/camgear/overview/): Multi-Threaded API targeting various IP-USB-Cameras/Network-Streams/Streaming-Sites-URLs. * [PiGear](gears/pigear/overview/): Multi-Threaded API targeting various Raspberry-Pi Camera Modules. * [ScreenGear](gears/screengear/overview/): Multi-Threaded API targeting ultra-fast Screencasting. -* [VideoGear](gears/videogear/overview/): Common Video-Capture API with internal [Video Stabilizer](gears/stabilizer/overview/) wrapper. +* [VideoGear](gears/videogear/overview/): Common Video-Capture API with internal [_Video Stabilizer_](gears/stabilizer/overview/) wrapper. #### VideoWriter Gears @@ -92,29 +96,29 @@ These Gears can be classified as follows: > Contributions are welcome, and greatly appreciated! -Please see our [Contribution Guidelines ➶](contribution.md) for more details. +Please see our [**Contribution Guidelines**](contribution.md) for more details.   ## Community Channel -If you've come up with some new idea, or looking for the fastest way troubleshoot your problems. Please checkout our [Gitter community channel ➶][gitter] +If you've come up with some new idea, or looking for the fastest way troubleshoot your problems. Please checkout our [**Gitter community channel ➶**][gitter]   ## Become a Stargazer -You can be a [Stargazer :star2:][stargazer] by starring us on Github, it helps us a lot and you're making it easier for others to find & trust this library. Thanks! +You can be a [**Stargazer :star2:**][stargazer] by starring us on Github, it helps us a lot and you're making it easier for others to find & trust this library. Thanks!   -## Support Us +## Donations -> VidGear relies on your support :heart: +> VidGear is free and open source and will always remain so. :heart: -Donations help keep VidGear's Open Source Development alive. No amount is too little, even the smallest contributions can make a huge difference. +It is (like all open source software) a labour of love and something I am doing with my own free time. If you would like to say thanks, please feel free to make a donation: - +   diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index acbd96f51..88ef67db1 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -21,85 +21,97 @@ limitations under the License. # Install using pip -> _Best option for quickly getting stable VidGear installed._ +> _Best option for easily getting stable VidGear installed._ ## Prerequisites -When installing VidGear with pip, you need to check manually if following dependencies are installed: +When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), you need to check manually if following dependencies are installed: -### OpenCV -Must require OpenCV(3.0+) python binaries installed for all core functions. You easily install it directly via [pip](https://pip.pypa.io/en/stable/installing/): +### Core Prerequisites -??? tip "OpenCV installation from source" +* #### OpenCV - You can also follow online tutorials for building & installing OpenCV on [Windows](https://www.learnopencv.com/install-opencv3-on-windows/), [Linux](https://www.pyimagesearch.com/2018/05/28/ubuntu-18-04-how-to-install-opencv/) and [Raspberry Pi](https://www.pyimagesearch.com/2018/09/26/install-opencv-4-on-your-raspberry-pi/) machines manually from its source. + Must require OpenCV(3.0+) python binaries installed for all core functions. You easily install it directly via [pip](https://pypi.org/project/opencv-python/): -```sh -pip install opencv-python -``` + ??? tip "OpenCV installation from source" -### FFmpeg + You can also follow online tutorials for building & installing OpenCV on [Windows](https://www.learnopencv.com/install-opencv3-on-windows/), [Linux](https://www.pyimagesearch.com/2018/05/28/ubuntu-18-04-how-to-install-opencv/), [MacOS](https://www.pyimagesearch.com/2018/08/17/install-opencv-4-on-macos/) and [Raspberry Pi](https://www.pyimagesearch.com/2018/09/26/install-opencv-4-on-your-raspberry-pi/) machines manually from its source. -Must require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) and [**WriteGear's Compression Mode**](../../gears/writegear/compression/overview/). + :warning: Make sure not to install both *pip* and *source* version together. Otherwise installation will fail to work! -!!! tip "FFmpeg Installation" + ??? info "Other OpenCV binaries" - Follow this dedicated [**FFmpeg Installation doc**](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. + 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). -### Picamera -Must Required if you're using Raspberry Pi Camera Modules with its [PiGear](../../gears/pigear/overview/) API. You can easily install it via pip: + ```sh + pip install opencv-python + ``` +### API Specific Prerequisites -!!! 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." +* #### FFmpeg -```sh -pip install picamera -``` + Require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). -### Aiortc + !!! tip "FFmpeg Installation" -Must Required only if you're using the [WebGear_RTC API](../../gears/webgear_rtc/overview/). You can easily install it via pip: + Follow this dedicated [**FFmpeg Installation doc**](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. -??? error "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: +* #### Picamera - !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." + Required only if you're using Raspberry Pi Camera Modules with its [**PiGear**](../../gears/pigear/overview/) API. You can easily install it via pip: - - Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). - - Alternative link to Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019). - - Offline installer: [vs_buildtools.exe](https://aka.ms/vs/16/release/vs_buildtools.exe) - Afterwards, Select: Workloads → Desktop development with C++, then for Individual Components, select only: + !!! 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." - - [x] Windows 10 SDK - - [x] C++ x64/x86 build tools + ```sh + pip install picamera + ``` - Finally, proceed installing `aiortc` via pip. +* #### Aiortc -```sh -pip install aiortc -``` + Required only if you're using the [**WebGear_RTC**](../../gears/webgear_rtc/overview/) API. You can easily install it via pip: -### Uvloop + ??? error "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: -Must 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: + !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." -!!! error "uvloop is **[NOT yet supported on Windows Machines](https://github.com/MagicStack/uvloop/issues/14).**" -!!! warning "Python-3.6 legacies support [**dropped in version `>=1.15.0`**](https://github.com/MagicStack/uvloop/releases/tag/v0.15.0). Kindly install previous `0.14.0` version instead." + - Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). + - Alternative link to Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019). + - Offline installer: [vs_buildtools.exe](https://aka.ms/vs/16/release/vs_buildtools.exe) -```sh -pip install uvloop -``` + Afterwards, Select: Workloads → Desktop development with C++, then for Individual Components, select only: + + - [x] Windows 10 SDK + - [x] C++ x64/x86 build tools + + Finally, proceed installing `aiortc` via pip. + + ```sh + pip install aiortc + ``` + +* #### 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: + + !!! error "uvloop is **[NOT yet supported on Windows Machines](https://github.com/MagicStack/uvloop/issues/14).**" + !!! warning "Python-3.6 legacies support [**dropped in version `>=1.15.0`**](https://github.com/MagicStack/uvloop/releases/tag/v0.15.0). Kindly install previous `0.14.0` version instead." + + ```sh + pip install uvloop + ```   ## Installation -Installation is as simple as: +**Installation is as simple as:** ??? warning "Windows Installation" diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index 0418ffd79..49a8598bf 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -26,54 +26,58 @@ limitations under the License. ## Prerequisites -When installing VidGear from source, FFmpeg and Aiortc is the only dependency you need to install manually: +When installing VidGear from source, FFmpeg and Aiortc are the only two API specific dependencies you need to install manually: !!! question "What about rest of the dependencies?" - Any other python dependencies will be automatically installed based on your OS specifications. + Any other python dependencies _(Core/API specific)_ will be automatically installed based on your OS specifications. -### FFmpeg -Must require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) and [**WriteGear's Compression Mode**](../../gears/writegear/compression/overview/). +### API Specific Prerequisites -!!! tip "FFmpeg Installation" +* #### FFmpeg - Follow this dedicated [**FFmpeg Installation doc**](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. + Require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). + !!! tip "FFmpeg Installation" -### Aiortc + Follow this dedicated [**FFmpeg Installation doc**](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. -Must Required only if you're using the [WebGear_RTC API](../../gears/webgear_rtc/overview/). You can easily install it via pip: -??? error "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: +* #### Aiortc - !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." + Required only if you're using the [**WebGear_RTC**](../../gears/webgear_rtc/overview/) API. You can easily install it via pip: - - Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). - - Alternative link to Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019). - - Offline installer: [vs_buildtools.exe](https://aka.ms/vs/16/release/vs_buildtools.exe) + ??? error "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: - Afterwards, Select: Workloads → Desktop development with C++, then for Individual Components, select only: + !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." - - [x] Windows 10 SDK - - [x] C++ x64/x86 build tools + - Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). + - Alternative link to Microsoft [Build Tools for Visual Studio](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2019). + - Offline installer: [vs_buildtools.exe](https://aka.ms/vs/16/release/vs_buildtools.exe) - Finally, proceed installing `aiortc` via pip. + Afterwards, Select: Workloads → Desktop development with C++, then for Individual Components, select only: -```sh - pip install aiortc -``` + - [x] Windows 10 SDK + - [x] C++ x64/x86 build tools + + Finally, proceed installing `aiortc` via pip. + ```sh + pip install aiortc + ```   ## Installation -If you want to just install and try out the checkout the latest beta [`testing`](https://github.com/abhiTronix/vidgear/tree/testing) branch , you can do so with the following command. This can be useful if you want to provide feedback for a new feature or want to confirm if a bug you have encountered is fixed in the `testing` branch. +**If you want to just install and try out the checkout the latest beta [`testing`](https://github.com/abhiTronix/vidgear/tree/testing) branch , you can do so with the following command:** + +!!! info "This can be useful if you want to provide feedback for a new feature or want to confirm if a bug you have encountered is fixed in the `testing` branch." -!!! warning "DO NOT clone or install `development` branch, as it is not tested with CI environments and is possibly very unstable or unusable." +!!! warning "DO NOT clone or install `development` branch unless advised, as it is not tested with CI environments and possibly very unstable or unusable." ??? tip "Windows Installation" diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index b10aefecc..5483e4e8e 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -40,14 +40,14 @@ Switching OpenCV with VidGear APIs is usually a fairly painless process, and wil !!! info "Learn more about OpenCV [here ➶](https://software.intel.com/content/www/us/en/develop/articles/what-is-opencv.html)" -VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art features on top of it like: +VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art functionalities such as: - [x] Accelerated [Multi-Threaded](../bonus/TQM/#c-accelerates-frame-processing) Performance. - [x] Real-time Stabilization. - [x] Inherit support for multiple sources. - [x] Screen-casting, Live network-streaming, [plus way much more ➶](../gears) -Vidgear offers all this while maintaining the same standard OpenCV-Python _(Python API for OpenCV)_ coding syntax for all of its APIs, thereby making it even easier to implement Complex OpenCV applications in fewer lines of python code. +Vidgear offers all this at once while maintaining the same standard OpenCV-Python _(Python API for OpenCV)_ coding syntax for all of its APIs, thereby making it even easier to implement complex OpenCV applications in way fewer lines and without changing your python code much.   @@ -150,7 +150,7 @@ Let's breakdown a few noteworthy difference in both syntaxes: | Terminating | `#!python stream.release()` | `#!python stream.stop()` | -!!! success "Now, checkout other [VideoCapture Gears ➶](../gears/#a-videocapture-gears)" +!!! success "Now checkout other [VideoCapture Gears ➶](../gears/#a-videocapture-gears)"   @@ -274,6 +274,6 @@ Let's breakdown a few noteworthy difference in both syntaxes: | Writing frames | `#!python writer.write(frame)` | `#!python writer.write(frame)` | | Terminating | `#!python writer.release()` | `#!python writer.close()` | -!!! success "Now, checkout more examples of WriteGear API _(with FFmpeg backend)_ [here ➶](../gears/writegear/compression/usage/)" +!!! success "Now checkout more about WriteGear API [here ➶](../gears/writegear/introduction/)"   \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 47d081183..ed671148a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,8 +56,8 @@ theme: icon: material/weather-night name: Switch to light mode font: - text: Source Sans Pro - code: IBM Plex + text: Muli + code: Fira Code icon: logo: logo logo: assets/images/logo.svg From e667d73ca5fc90a148117f97aabfa04949741958 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 18 Jul 2021 09:21:05 +0530 Subject: [PATCH 066/112] :pencil2: Docs: Fixed minor typo. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7754a8c8d..f944246bb 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The following **functional block diagram** clearly depicts the generalized funct * [**WebGear_RTC**](#webgear_rtc) * [**NetGear_Async**](#netgear_async) * [**Contributions & Community Support**](#contributions--community-support) - * [**Community Support**](community-support) + * [**Community Support**](#community-support) * [**Contributors**](#contributors) * [**Donations**](#donations) * [**Citation**](#citation) From 007131a20d20e5ea0e43f3e81456078420599dfb Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 19 Jul 2021 10:11:21 +0530 Subject: [PATCH 067/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20WebGear:=20New=20J?= =?UTF-8?q?PEG=20Frame=20compression=20with=20`simplejpeg`=20(Fixes=20#202?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💥 Removed format specific OpenCV decoding and encoding support for WebGear. - 💥 Dropped support for `frame_jpeg_quality`, `frame_jpeg_optimize`, `frame_jpeg_progressive` attributes from WebGear. - ⚡️ Implemented JPEG compression algorithm for 4-5% performance boost at cost of minor loss in quality. - ✨ Utilized `encode_jpeg` and `decode_jpeg` methods to implement turbo-JPEG transcoding with `simplejpeg`. - ✨ Added new options to control JPEG frames quality, enable fastest dct, fast upsampling to boost performance. - 🎨 Added new `jpeg_compression`, `jpeg_compression_quality`, `jpeg_compression_fastdct`, `jpeg_compression_fastupsample` attributes. - ⚡️ Enabled fast dct by default with JPEG frames at 90%. - ⚡️ Incremented default frame reduction to 25%. - 💄 Docs: Changed fonts => text: `Muli` & code: `Fira Code` --- vidgear/gears/asyncio/webgear.py | 113 ++++++++++++++++++------------- 1 file changed, 66 insertions(+), 47 deletions(-) diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index 4d5fd17cc..c294f6a8d 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -24,6 +24,8 @@ import sys import asyncio import inspect +import simplejpeg +import numpy as np import logging as log from collections import deque from starlette.routing import Mount, Route @@ -94,10 +96,12 @@ def __init__( """ # initialize global params - self.__jpeg_quality = 90 # 90% quality - self.__jpeg_optimize = 0 # optimization off - self.__jpeg_progressive = 0 # jpeg will be baseline instead - self.__frame_size_reduction = 20 # 20% reduction + # define frame-compression handler + self.__jpeg_compression_quality = 90 # 90% quality + self.__jpeg_compression_fastdct = True # fastest DCT on by default + self.__jpeg_compression_fastupsample = False # fastupsample off by default + self.__jpeg_compression_colorspace = "BGR" # use BGR colorspace by default + self.__frame_size_reduction = 25 # use 25% reduction self.__logging = logging custom_data_location = "" # path to save data-files to custom location @@ -110,37 +114,66 @@ def __init__( # assign values to global variables if specified and valid if options: - if "frame_size_reduction" in options: - value = options["frame_size_reduction"] - if isinstance(value, (int, float)) and value >= 0 and value <= 90: - self.__frame_size_reduction = value + if "jpeg_compression_colorspace" in options: + value = options["jpeg_compression_colorspace"] + if isinstance(value, str) and value.strip().upper() in [ + "RGB", + "BGR", + "RGBX", + "BGRX", + "XBGR", + "XRGB", + "GRAY", + "RGBA", + "BGRA", + "ABGR", + "ARGB", + "CMYK", + ]: + # set encoding colorspace + self.__jpeg_compression_colorspace = value.strip().upper() else: - logger.warning("Skipped invalid `frame_size_reduction` value!") - del options["frame_size_reduction"] # clean + logger.warning( + "Skipped invalid `jpeg_compression_colorspace` value!" + ) + del options["jpeg_compression_colorspace"] # clean - if "frame_jpeg_quality" in options: - value = options["frame_jpeg_quality"] - if isinstance(value, (int, float)) and value >= 10 and value <= 95: - self.__jpeg_quality = int(value) + if "jpeg_compression_quality" in options: + value = options["jpeg_compression_quality"] + # set valid jpeg quality + if isinstance(value, (int, float)) and value >= 10 and value <= 100: + self.__jpeg_compression_quality = int(value) else: - logger.warning("Skipped invalid `frame_jpeg_quality` value!") - del options["frame_jpeg_quality"] # clean + logger.warning("Skipped invalid `jpeg_compression_quality` value!") + del options["jpeg_compression_quality"] # clean - if "frame_jpeg_optimize" in options: - value = options["frame_jpeg_optimize"] + if "jpeg_compression_fastdct" in options: + value = options["jpeg_compression_fastdct"] + # enable jpeg fastdct if isinstance(value, bool): - self.__jpeg_optimize = int(value) + self.__jpeg_compression_fastdct = value else: - logger.warning("Skipped invalid `frame_jpeg_optimize` value!") - del options["frame_jpeg_optimize"] # clean + logger.warning("Skipped invalid `jpeg_compression_fastdct` value!") + del options["jpeg_compression_fastdct"] # clean - if "frame_jpeg_progressive" in options: - value = options["frame_jpeg_progressive"] + if "jpeg_compression_fastupsample" in options: + value = options["jpeg_compression_fastupsample"] + # enable jpeg fastupsample if isinstance(value, bool): - self.__jpeg_progressive = int(value) + self.__jpeg_compression_fastupsample = value else: - logger.warning("Skipped invalid `frame_jpeg_progressive` value!") - del options["frame_jpeg_progressive"] # clean + logger.warning( + "Skipped invalid `jpeg_compression_fastupsample` value!" + ) + del options["jpeg_compression_fastupsample"] # clean + + if "frame_size_reduction" in options: + value = options["frame_size_reduction"] + if isinstance(value, (int, float)) and value >= 0 and value <= 90: + self.__frame_size_reduction = value + else: + logger.warning("Skipped invalid `frame_size_reduction` value!") + del options["frame_size_reduction"] # clean if "custom_data_location" in options: value = options["custom_data_location"] @@ -201,12 +234,11 @@ def __init__( ) ) logger.debug( - "Setting params:: Size Reduction:{}%, JPEG quality:{}%, JPEG optimizations:{}, JPEG progressive:{}{}.".format( - self.__frame_size_reduction, - self.__jpeg_quality, - bool(self.__jpeg_optimize), - bool(self.__jpeg_progressive), - " and emulating infinite frames" if self.__enable_inf else "", + "Enabling JPEG Frame-Compression 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", ) ) @@ -337,31 +369,18 @@ async def __producer(self): # reducer frames size if specified if self.__frame_size_reduction: frame = await reducer(frame, percentage=self.__frame_size_reduction) - # handle JPEG encoding - encodedImage = cv2.imencode( - ".jpg", - frame, - [ - cv2.IMWRITE_JPEG_QUALITY, - self.__jpeg_quality, - cv2.IMWRITE_JPEG_PROGRESSIVE, - self.__jpeg_progressive, - cv2.IMWRITE_JPEG_OPTIMIZE, - self.__jpeg_optimize, - ], - )[1].tobytes() + # yield frame in byte format yield ( b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n" ) - await asyncio.sleep(0.00001) + #await asyncio.sleep(0.00000001) async def __video(self, scope): """ Return a async video streaming response. """ assert scope["type"] in ["http", "https"] - await asyncio.sleep(0.00001) return StreamingResponse( self.config["generator"](), media_type="multipart/x-mixed-replace; boundary=frame", From 191c424dc1cdb5bbc376e2e9c6df47cd7379ae9e Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 19 Jul 2021 10:25:46 +0530 Subject: [PATCH 068/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20WebGear:=20Added?= =?UTF-8?q?=20additional=20colorspace=20support=20with=20JPEG=20Frame=20Co?= =?UTF-8?q?mpression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Implemented automated grayscale colorspace frames handling. - 🐛 Helper: Fixed bug with `create_blank_frame` that throws error with gray frames. - ⚡️ Helper: Implemented automatic output channel correction inside `create_blank_frame` function. - 🎨 Helper Extended automatic output channel correction support to asyncio package. --- vidgear/gears/asyncio/helper.py | 9 +++++++++ vidgear/gears/asyncio/webgear.py | 22 +++++++++++++++++++++- vidgear/gears/helper.py | 9 +++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index ad5cd75a1..2bdef47c6 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -168,6 +168,15 @@ def create_blank_frame(frame=None, text="", logging=False): cv2.putText( blank_frame, text, (textX, textY), font, fontScale, (125, 125, 125), 6 ) + + # correct channels + if frame.ndim == 2: + blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2GRAY) + elif frame.ndim == 3 and frame.shape[-1] == 4: + blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2BGRA) + else: + pass + # return frame return blank_frame diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index c294f6a8d..0067e403c 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -370,11 +370,31 @@ async def __producer(self): if self.__frame_size_reduction: frame = await reducer(frame, percentage=self.__frame_size_reduction) + # handle JPEG encoding + if self.__jpeg_compression_colorspace == "GRAY": + if frame.ndim == 2: + # patch for https://gitlab.com/jfolz/simplejpeg/-/issues/11 + frame = np.expand_dims(frame, axis=2) + encodedImage = simplejpeg.encode_jpeg( + frame, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + fastdct=self.__jpeg_compression_fastdct, + ) + else: + encodedImage = simplejpeg.encode_jpeg( + frame, + quality=self.__jpeg_compression_quality, + colorspace=self.__jpeg_compression_colorspace, + colorsubsampling="422", + fastdct=self.__jpeg_compression_fastdct, + ) + # yield frame in byte format yield ( b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n" ) - #await asyncio.sleep(0.00000001) + # await asyncio.sleep(0.00000001) async def __video(self, scope): """ diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index b560e30f9..3e12a67ec 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -445,6 +445,15 @@ def create_blank_frame(frame=None, text="", logging=False): cv2.putText( blank_frame, text, (textX, textY), font, fontScale, (125, 125, 125), 6 ) + + # correct channels + if frame.ndim == 2: + blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2GRAY) + elif frame.ndim == 3 and frame.shape[-1] == 4: + blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2BGRA) + else: + pass + # return frame return blank_frame From 0cc45749fbfc9cfc4058d63bd0d43e53fc8c22f7 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 19 Jul 2021 20:32:17 +0530 Subject: [PATCH 069/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Helper:=20Automate?= =?UTF-8?q?d=20interpolation=20selection=20for=20gears?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Implemented `retrieve_best_interpolation` method to automatically select best available interpolation within OpenCV. - ✨ Added support for this method in WebGear, WebGear_RTC and Stabilizer Classes/APIs. - 🔇 Added `logging` parameter to capPropId function to forcefully discard any error(if required). - 👷 Added new CI tests for this feature. - CI: - 💚 Fixed eventloop bugs in Helper CI tests. - 💚 Enabled Helper tests for python 3.8+ legacies. --- vidgear/gears/asyncio/helper.py | 50 +++++++++++++- vidgear/gears/asyncio/webgear.py | 20 +++++- vidgear/gears/asyncio/webgear_rtc.py | 14 +++- vidgear/gears/helper.py | 35 ++++++++-- vidgear/gears/stabilizer.py | 14 ++-- .../asyncio_tests/test_helper.py | 69 ++++++++++++++----- vidgear/tests/test_helper.py | 40 +++++++++-- 7 files changed, 199 insertions(+), 43 deletions(-) diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index 2bdef47c6..a3619b324 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -134,6 +134,46 @@ def mkdir_safe(dir, logging=False): logger.debug("Directory already exists at `{}`".format(dir)) +def capPropId(property, logging=True): + """ + ### capPropId + + Retrieves the OpenCV property's Integer(Actual) value from string. + + Parameters: + property (string): inputs OpenCV property as string. + logging (bool): enables logging for its operations + + **Returns:** Resultant integer value. + """ + integer_value = 0 + 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)) + return None + return integer_value + + +def retrieve_best_interpolation(interpolations): + """ + ### retrieve_best_interpolation + Retrieves best interpolation for resizing + + Parameters: + interpolations (list): list of interpolations as string. + **Returns:** Resultant integer value of found interpolation. + """ + if isinstance(interpolations, list): + for intp in interpolations: + interpolation = capPropId(intp, logging=False) + if not (interpolation is None): + return interpolation + return None + + def create_blank_frame(frame=None, text="", logging=False): """ ### create_blank_frame @@ -181,7 +221,7 @@ def create_blank_frame(frame=None, text="", logging=False): return blank_frame -async def reducer(frame=None, percentage=0): +async def reducer(frame=None, percentage=0, interpolation=cv2.INTER_LANCZOS4): """ ### reducer @@ -190,6 +230,7 @@ async def reducer(frame=None, percentage=0): Parameters: frame (numpy.ndarray): inputs numpy array(frame). percentage (int/float): inputs size-reduction percentage. + interpolation (int): Change resize interpolation. **Returns:** A reduced numpy ndarray array. """ @@ -203,6 +244,11 @@ async def reducer(frame=None, percentage=0): "[Helper:ERROR] :: Given frame-size reduction percentage is invalid, Kindly refer docs." ) + if not (isinstance(interpolation, int)): + raise ValueError( + "[Helper:ERROR] :: Given interpolation is invalid, Kindly refer docs." + ) + # grab the frame size (height, width) = frame.shape[:2] @@ -213,7 +259,7 @@ async def reducer(frame=None, percentage=0): dimensions = (int(reduction), int(height * ratio)) # return the resized frame - return cv2.resize(frame, dimensions, interpolation=cv2.INTER_LANCZOS4) + return cv2.resize(frame, dimensions, interpolation=interpolation) def generate_webdata(path, c_name="webgear", overwrite_default=False, logging=False): diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index 0067e403c..119e2f6f1 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -35,7 +35,13 @@ from starlette.applications import Starlette from starlette.middleware import Middleware -from .helper import reducer, logger_handler, generate_webdata, create_blank_frame +from .helper import ( + reducer, + logger_handler, + generate_webdata, + create_blank_frame, + retrieve_best_interpolation, +) from ..videogear import VideoGear # define logger @@ -101,8 +107,12 @@ def __init__( self.__jpeg_compression_fastdct = True # fastest DCT on by default self.__jpeg_compression_fastupsample = False # fastupsample off by default self.__jpeg_compression_colorspace = "BGR" # use BGR colorspace by default - self.__frame_size_reduction = 25 # use 25% reduction self.__logging = logging + self.__frame_size_reduction = 25 # use 25% reduction + # retrieve interpolation for reduction + self.__interpolation = retrieve_best_interpolation( + ["INTER_LINEAR_EXACT", "INTER_LINEAR", "INTER_AREA"] + ) custom_data_location = "" # path to save data-files to custom location data_path = "" # path to WebGear data-files @@ -368,7 +378,11 @@ async def __producer(self): # reducer frames size if specified if self.__frame_size_reduction: - frame = await reducer(frame, percentage=self.__frame_size_reduction) + frame = await reducer( + frame, + percentage=self.__frame_size_reduction, + interpolation=self.__interpolation, + ) # handle JPEG encoding if self.__jpeg_compression_colorspace == "GRAY": diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 2f48b57eb..295d8e9b4 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -50,6 +50,7 @@ logger_handler, generate_webdata, create_blank_frame, + retrieve_best_interpolation, ) from ..videogear import VideoGear @@ -111,10 +112,15 @@ def __init__( # initialize global params self.__logging = logging self.__enable_inf = False # continue frames even when video ends. - self.__frame_size_reduction = 20 # 20% reduction self.is_launched = False # check if launched already self.is_running = False # check if running + self.__frame_size_reduction = 20 # 20% reduction + # retrieve interpolation for reduction + self.__interpolation = retrieve_best_interpolation( + ["INTER_LINEAR_EXACT", "INTER_LINEAR", "INTER_AREA"] + ) + if options: if "frame_size_reduction" in options: value = options["frame_size_reduction"] @@ -233,7 +239,11 @@ async def recv(self): # reducer frames size if specified if self.__frame_size_reduction: - f_stream = await reducer(f_stream, percentage=self.__frame_size_reduction) + f_stream = await reducer( + f_stream, + percentage=self.__frame_size_reduction, + interpolation=self.__interpolation, + ) # construct `av.frame.Frame` from `numpy.nd.array` frame = VideoFrame.from_ndarray(f_stream, format="bgr24") diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 3e12a67ec..c389d422e 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -627,7 +627,7 @@ def delete_ext_safe(dir_path, extensions=[], logging=False): logger.debug("Deleted file: `{}`".format(file)) -def capPropId(property): +def capPropId(property, logging=True): """ ### capPropId @@ -635,6 +635,7 @@ def capPropId(property): Parameters: property (string): inputs OpenCV property as string. + logging (bool): enables logging for its operations **Returns:** Resultant integer value. """ @@ -642,12 +643,30 @@ def capPropId(property): try: integer_value = getattr(cv2, property) except Exception as e: - logger.exception(str(e)) - logger.critical("`{}` is not a valid OpenCV property!".format(property)) + if logging: + logger.exception(str(e)) + logger.critical("`{}` is not a valid OpenCV property!".format(property)) return None return integer_value +def retrieve_best_interpolation(interpolations): + """ + ### retrieve_best_interpolation + Retrieves best interpolation for resizing + + Parameters: + interpolations (list): list of interpolations as string. + **Returns:** Resultant integer value of found interpolation. + """ + if isinstance(interpolations, list): + for intp in interpolations: + interpolation = capPropId(intp, logging=False) + if not (interpolation is None): + return interpolation + return None + + def youtube_url_validator(url): """ ### youtube_url_validator @@ -671,7 +690,7 @@ def youtube_url_validator(url): return "" -def reducer(frame=None, percentage=0): +def reducer(frame=None, percentage=0, interpolation=cv2.INTER_LANCZOS4): """ ### reducer @@ -680,6 +699,7 @@ def reducer(frame=None, percentage=0): Parameters: frame (numpy.ndarray): inputs numpy array(frame). percentage (int/float): inputs size-reduction percentage. + interpolation (int): Change resize interpolation. **Returns:** A reduced numpy ndarray array. """ @@ -693,6 +713,11 @@ def reducer(frame=None, percentage=0): "[Helper:ERROR] :: Given frame-size reduction percentage is invalid, Kindly refer docs." ) + if not (isinstance(interpolation, int)): + raise ValueError( + "[Helper:ERROR] :: Given interpolation is invalid, Kindly refer docs." + ) + # grab the frame size (height, width) = frame.shape[:2] @@ -703,7 +728,7 @@ def reducer(frame=None, percentage=0): dimensions = (int(reduction), int(height * ratio)) # return the resized frame - return cv2.resize(frame, dimensions, interpolation=cv2.INTER_LANCZOS4) + return cv2.resize(frame, dimensions, interpolation=interpolation) def dict2Args(param_dict): diff --git a/vidgear/gears/stabilizer.py b/vidgear/gears/stabilizer.py index 2f61c24da..7eb019d13 100644 --- a/vidgear/gears/stabilizer.py +++ b/vidgear/gears/stabilizer.py @@ -27,7 +27,7 @@ import logging as log from collections import deque -from .helper import logger_handler, check_CV_version +from .helper import logger_handler, check_CV_version, retrieve_best_interpolation # define logger logger = log.getLogger("Stabilizer") @@ -138,6 +138,11 @@ def __init__( # define OpenCV version self.__cv2_version = check_CV_version() + # retrieve best interpolation + self.__interpolation = retrieve_best_interpolation( + ["INTER_LINEAR_EXACT", "INTER_LINEAR", "INTER_CUBIC"] + ) + # define normalized box filter self.__box_filter = np.ones(smoothing_radius) / smoothing_radius @@ -374,11 +379,10 @@ def __apply_transformations(self): self.__crop_n_zoom : -self.__crop_n_zoom, ] # zoom stabilized frame - interpolation = ( - cv2.INTER_CUBIC if (self.__cv2_version < 4) else cv2.INTER_LINEAR_EXACT - ) frame_stabilized = cv2.resize( - frame_cropped, self.__frame_size[::-1], interpolation=interpolation + frame_cropped, + self.__frame_size[::-1], + interpolation=self.__interpolation, ) # finally return stabilized frame diff --git a/vidgear/tests/network_tests/asyncio_tests/test_helper.py b/vidgear/tests/network_tests/asyncio_tests/test_helper.py index 75901725f..6c2682d85 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_helper.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_helper.py @@ -20,11 +20,18 @@ # import the necessary packages import sys +import cv2 +import asyncio import numpy as np import pytest import logging as log -from vidgear.gears.asyncio.helper import reducer, create_blank_frame, logger_handler +from vidgear.gears.asyncio.helper import ( + reducer, + create_blank_frame, + logger_handler, + retrieve_best_interpolation, +) # define test logger logger = log.getLogger("Test_Asyncio_Helper") @@ -33,6 +40,14 @@ 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() + + def getframe(): """ returns empty numpy frame/array of dimensions: (500,800,3) @@ -40,25 +55,24 @@ def getframe(): return (np.random.standard_normal([500, 800, 3]) * 255).astype(np.uint8) -pytestmark = pytest.mark.asyncio - - -@pytest.mark.skipif( - sys.version_info >= (3, 8), - reason="python3.8 is not supported yet by pytest-asyncio", -) +@pytest.mark.asyncio @pytest.mark.parametrize( - "frame , percentage, result", - [(getframe(), 85, True), (None, 80, False), (getframe(), 95, False)], + "frame , percentage, interpolation, result", + [ + (getframe(), 85, cv2.INTER_AREA, True), + (None, 80, cv2.INTER_AREA, False), + (getframe(), 95, cv2.INTER_AREA, False), + (getframe(), 80, 797, False), + ], ) -async def test_reducer_asyncio(frame, percentage, result): +async def test_reducer_asyncio(frame, percentage, interpolation, result): """ Testing frame size reducer function """ if not (frame is None): org_size = frame.shape[:2] try: - reduced_frame = await reducer(frame, percentage) + reduced_frame = await reducer(frame, percentage, interpolation) logger.debug(reduced_frame.shape) assert not (reduced_frame is None) reduced_frame_size = reduced_frame.shape[:2] @@ -69,23 +83,20 @@ async def test_reducer_asyncio(frame, percentage, result): 100 * reduced_frame_size[1] // (100 - percentage) == org_size[1] ) # cross-check height except Exception as e: - if isinstance(e, ValueError) and not (result): - pass + if not (result): + pytest.xfail(str(e)) else: pytest.fail(str(e)) -@pytest.mark.skipif( - sys.version_info >= (3, 8), - reason="python3.8 is not supported yet by pytest-asyncio", -) +@pytest.mark.asyncio @pytest.mark.parametrize( "frame , text", [(getframe(), "ok"), (None, ""), (getframe(), 123)], ) async def test_create_blank_frame_asyncio(frame, text): """ - Testing frame size reducer function + Testing create_blank_frame function """ try: text_frame = create_blank_frame(frame=frame, text=text) @@ -94,3 +105,23 @@ async def test_create_blank_frame_asyncio(frame, text): except Exception as e: if not (frame is None): pytest.fail(str(e)) + + +@pytest.mark.parametrize( + "interpolations", + [ + "invalid", + ["invalid", "invalid2", "INTER_LANCZOS4"], + ["INTER_NEAREST_EXACT", "INTER_LINEAR_EXACT", "INTER_LANCZOS4"], + ], +) +def test_retrieve_best_interpolation(interpolations): + """ + Testing retrieve_best_interpolation method + """ + try: + output = retrieve_best_interpolation(interpolations) + if interpolations != "invalid": + assert output, "Test failed" + except Exception as e: + pytest.fail(str(e)) diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 4393ed04b..27472224f 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -52,6 +52,7 @@ generate_auth_certificates, get_supported_resolution, dimensions_to_resolutions, + retrieve_best_interpolation, ) from vidgear.gears.asyncio.helper import generate_webdata, validate_webdata @@ -320,17 +321,22 @@ def test_check_output(): @pytest.mark.parametrize( - "frame , percentage, result", - [(getframe(), 85, True), (None, 80, False), (getframe(), 95, False)], + "frame , percentage, interpolation, result", + [ + (getframe(), 85, cv2.INTER_AREA, True), + (None, 80, cv2.INTER_AREA, False), + (getframe(), 95, cv2.INTER_AREA, False), + (getframe(), 80, 797, False), + ], ) -def test_reducer(frame, percentage, result): +def test_reducer(frame, percentage, interpolation, result): """ Testing frame size reducer function """ if not (frame is None): org_size = frame.shape[:2] try: - reduced_frame = reducer(frame, percentage) + reduced_frame = reducer(frame, percentage, interpolation) logger.debug(reduced_frame.shape) assert not (reduced_frame is None) reduced_frame_size = reduced_frame.shape[:2] @@ -341,8 +347,8 @@ def test_reducer(frame, percentage, result): 100 * reduced_frame_size[1] // (100 - percentage) == org_size[1] ) # cross-check height except Exception as e: - if isinstance(e, ValueError) and not (result): - pass + if not (result): + pytest.xfail(str(e)) else: pytest.fail(str(e)) @@ -411,7 +417,7 @@ def test_validate_audio(path, result): ) def test_create_blank_frame(frame, text): """ - Testing frame size reducer function + Testing create_blank_frame function """ try: text_frame = create_blank_frame(frame=frame, text=text) @@ -535,3 +541,23 @@ def test_delete_ext_safe(ext, result): except Exception as e: if result: pytest.fail(str(e)) + + +@pytest.mark.parametrize( + "interpolations", + [ + "invalid", + ["invalid", "invalid2", "INTER_LANCZOS4"], + ["INTER_NEAREST_EXACT", "INTER_LINEAR_EXACT", "INTER_LANCZOS4"], + ], +) +def test_retrieve_best_interpolation(interpolations): + """ + Testing retrieve_best_interpolation method + """ + try: + output = retrieve_best_interpolation(interpolations) + if interpolations != "invalid": + assert output, "Test failed" + except Exception as e: + pytest.fail(str(e)) From 35408fa53f295911b5b9522b8d6504380d67f62c Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 19 Jul 2021 22:21:52 +0530 Subject: [PATCH 070/112] =?UTF-8?q?=F0=9F=93=9D=20WebGear:=20Updated=20doc?= =?UTF-8?q?s=20for=20new=20JPEG=20compression.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Updated old and added new usage examples. - 📝 Dropped support for depreciated attributes from WebGear and added new attributes. - ✏️ Fixed typos and contexts. --- docs/gears/netgear/advanced/compression.md | 12 ++-- docs/gears/webgear/advanced.md | 80 +++++++++++++++++----- docs/gears/webgear/params.md | 76 ++++++++++++-------- docs/gears/webgear/usage.md | 29 ++++---- docs/gears/webgear_rtc/params.md | 32 ++++++++- 5 files changed, 162 insertions(+), 67 deletions(-) diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index c3b4f3421..f35a84081 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -53,7 +53,7 @@ Frame Compression is enabled by default in NetGear, and can be easily controlled For implementing Frame Compression, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter to leverage performance with Frame Compression: -* `jpeg_compression` _(bool/str)_: This internal attribute is used to activate/deactivate JPEG Frame Compression as well as to specify incoming frames colorspace with compression. Its usage is as follows: +* `jpeg_compression`: _(bool/str)_ This internal attribute is used to activate/deactivate JPEG Frame Compression as well as to specify incoming frames colorspace with compression. Its usage is as follows: - [x] **For activating JPEG Frame Compression _(Boolean)_:** @@ -85,13 +85,13 @@ For implementing Frame Compression, NetGear API currently provide following attr # activate jpeg encoding and set quality 95% options = {"jpeg_compression": True, "jpeg_compression_quality": 95} ``` - * `jpeg_compression_fastdct` _(bool)_: This attribute if True, NetGear API uses fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`, and its usage is as follows: + * `jpeg_compression_fastdct`: _(bool)_ This attribute if True, NetGear API uses fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`, and its usage is as follows: ```python # activate jpeg encoding and enable fast dct options = {"jpeg_compression": True, "jpeg_compression_fastdct": True} ``` - * `jpeg_compression_fastupsample` _(bool)_: This attribute if True, NetGear API use fastest color upsampling method. Its default value is `False`, and its usage is as follows: + * `jpeg_compression_fastupsample`: _(bool)_ This attribute if True, NetGear API use fastest color upsampling method. Its default value is `False`, and its usage is as follows: ```python # activate jpeg encoding and enable fast upsampling @@ -234,12 +234,12 @@ from vidgear.gears import VideoGear from vidgear.gears import NetGear import cv2 -# open any valid video stream(for e.g `test.mp4` file) and change its colorspace to `GRAY` +# open any valid video stream(for e.g `test.mp4` file) and change its colorspace to grayscale stream = VideoGear(source="test.mp4", colorspace="COLOR_BGR2GRAY").start() # activate jpeg encoding and specify other related parameters options = { - "jpeg_compression": "GRAY", # grayscale + "jpeg_compression": "GRAY", # set grayscale "jpeg_compression_quality": 90, "jpeg_compression_fastdct": True, "jpeg_compression_fastupsample": True, @@ -304,7 +304,7 @@ while True: if frame is None: break - # {do something with the frame here} + # {do something with the grayscale frame here} # Show output window cv2.imshow("Output Grayscale Frame", frame) diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index 1e46717ad..6d8c2f49b 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -23,6 +23,50 @@ limitations under the License. !!! note "This is a continuation of the [WebGear doc ➶](../overview/#webgear-api). Thereby, It's advised to first get familiarize with this API, and its [requirements](../usage/#requirements)." +  + + +### Using WebGear with Variable Colorspace + +WebGear by default only supports "BGR" colorspace with consumer or client. But you can use [`jpeg_compression_colorspace`](../params/#webgear_rtc-specific-attributes) string attribute through its options dictionary parameter to specify incoming frames colorspace. + +Let's implement a bare-minimum example using WebGear, where we will be sending [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) frames to client browser: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! example "This example works in conjunction with [Source ColorSpace manipulation for VideoCapture Gears ➶](../../../../bonus/colorspace_manipulation/#source-colorspace-manipulation)" + +!!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" + +```python +# import required libraries +import uvicorn +from vidgear.gears.asyncio import WebGear_RTC + +# various performance tweaks and enable grayscale input +options = { + "frame_size_reduction": 25, + "jpeg_compression_colorspace": "GRAY", # set grayscale + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + +# initialize WebGear_RTC app and change its colorspace to grayscale +web = WebGear_RTC( + source="foo.mp4", colorspace="COLOR_BGR2GRAY", logging=True, **options +) + +# run this app on Uvicorn server at address http://0.0.0.0:8000/ +uvicorn.run(web(), host="0.0.0.0", port=8000) + +# close app safely +web.shutdown() +``` + +**And that's all, Now you can see output at [`http://localhost:8000/`](http://localhost:8000/) address on your local machine.** +   ## Using WebGear with a Custom Source(OpenCV) @@ -62,12 +106,12 @@ async def my_frame_producer(): # do something with your OpenCV frame here # reducer frames size if you want more performance otherwise comment this line - frame = await reducer(frame, percentage=30) # reduce frame by 30% + frame = await reducer(frame, percentage=30, interpolation=cv2.INTER_LINEAR) # reduce frame by 30% # handle JPEG encoding encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() # yield frame in byte format - yield (b"--frame\r\nContent-Type:video/jpeg2000\r\n\r\n" + encodedImage + b"\r\n") - await asyncio.sleep(0.00001) + yield (b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n") + await asyncio.sleep(0.0000001) # close stream stream.release() @@ -101,9 +145,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 @@ -172,9 +216,9 @@ async def hello_world(request): # add various performance tweaks as usual 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 with a valid source @@ -220,9 +264,9 @@ from vidgear.gears.asyncio import WebGear # add various performance tweaks as usual 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 with a valid source @@ -286,9 +330,9 @@ from vidgear.gears.asyncio import WebGear # various webgear performance and Raspberry Pi camera 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, "hflip": True, "exposure_mode": "auto", "iso": 800, @@ -323,9 +367,9 @@ from vidgear.gears.asyncio import WebGear # various webgear 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 with a raw source and enable video stabilization(`stabilize=True`) diff --git a/docs/gears/webgear/params.md b/docs/gears/webgear/params.md index 95c0c586c..ef07bc65a 100644 --- a/docs/gears/webgear/params.md +++ b/docs/gears/webgear/params.md @@ -75,7 +75,7 @@ This parameter can be used to pass user-defined parameter to WebGear API by form WebGear(logging=True, **options) ``` -* **`frame_size_reduction`** _(int/float)_ : This attribute controls the size reduction _(in percentage)_ of the frame to be streamed on Server. The value defaults to `20`, and must be no higher than `90` _(fastest, max compression, Barely Visible frame-size)_ and no lower than `0` _(slowest, no compression, Original frame-size)_. Its recommended value is between `40-60`. Its usage is as follows: +* **`frame_size_reduction`** _(int/float)_ : This attribute controls the size reduction _(in percentage)_ of the frame to be streamed on Server and it has the most significant effect on performance. The value defaults to `25`, and must be no higher than `90` _(fastest, max compression, Barely Visible frame-size)_ and no lower than `0` _(slowest, no compression, Original frame-size)_. Its recommended value is between `40-60`. Its usage is as follows: ```python # frame-size will be reduced by 50% @@ -84,49 +84,67 @@ This parameter can be used to pass user-defined parameter to WebGear API by form WebGear(logging=True, **options) ``` -* **`enable_infinite_frames`** _(boolean)_ : Can be used to continue streaming _(instead of terminating immediately)_ with emulated blank frames with text "No Input", whenever the input source disconnects. Its default value is `False`. Its usage is as follows +* **`jpeg_compression_quality`**: _(int/float)_ This attribute controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. Its usage is as follows: - !!! new "New in v0.2.1" - `enable_infinite_frames` attribute was added in `v0.2.1`. + !!! new "New in v0.2.2" + `enable_infinite_frames` attribute was added in `v0.2.2`. ```python - # emulate infinite frames - options = {"enable_infinite_frames": True} + # activate jpeg encoding and set quality 95% + options = {"jpeg_compression_quality": 95} # assign it WebGear(logging=True, **options) ``` -* **Various Encoding Parameters:** +* **`jpeg_compression_fastdct`**: _(bool)_ This attribute if True, WebGear API uses fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`, and its usage is as follows: - In WebGear, the input video frames are first encoded into [**Motion JPEG (M-JPEG or MJPEG**)](https://en.wikipedia.org/wiki/Motion_JPEG) video compression format in which each video frame or interlaced field of a digital video sequence is compressed separately as a JPEG image, before sending onto a server. Therefore, WebGear API provides various attributes to have full control over JPEG encoding performance and quality, which are as follows: + !!! new "New in v0.2.2" + `enable_infinite_frames` attribute was added in `v0.2.2`. + ```python + # activate jpeg encoding and enable fast dct + options = {"jpeg_compression_fastdct": True} + # assign it + WebGear(logging=True, **options) + ``` - * **`frame_jpeg_quality`** _(integer)_ : It controls the JPEG encoder quality and value varies from `0` to `100` (the higher is the better quality but performance will be lower). Its default value is `95`. Its usage is as follows: +* **`jpeg_compression_fastupsample`**: _(bool)_ This attribute if True, WebGear API use fastest color upsampling method. Its default value is `False`, and its usage is as follows: - ```python - # JPEG will be encoded at 80% quality - options = {"frame_jpeg_quality": 80} - # assign it - WebGear(logging=True, **options) - ``` + !!! new "New in v0.2.2" + `enable_infinite_frames` attribute was added in `v0.2.2`. - * **`frame_jpeg_optimize`** _(boolean)_ : It enables various JPEG compression optimizations such as Chroma subsampling, Quantization table, etc. Its default value is `False`. Its usage is as follows: + ```python + # activate jpeg encoding and enable fast upsampling + options = {"jpeg_compression_fastupsample": True} + # assign it + WebGear(logging=True, **options) + ``` - ```python - # JPEG optimizations are enabled - options = {"frame_jpeg_optimize": True} - # assign it - WebGear(logging=True, **options) - ``` + * **`jpeg_compression_colorspace`**: _(str)_ This internal attribute is used to specify incoming frames colorspace with compression. Its usage is as follows: - * **`frame_jpeg_progressive`** _(boolean)_ : It enables **Progressive** JPEG encoding instead of the **Baseline**. Progressive Mode. Its default value is `False` means baseline mode is in-use. Its usage is as follows: + !!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" - ```python - # Progressive JPEG encoding enabled - options = {"frame_jpeg_progressive": True} - # assign it - WebGear(logging=True, **options) - ``` + !!! new "New in v0.2.2" + `enable_infinite_frames` attribute was added in `v0.2.2`. + + ```python + # Specify incoming frames are `grayscale` + options = {"jpeg_compression": "GRAY"} + # assign it + WebGear(logging=True, **options) + ``` + +* **`enable_infinite_frames`** _(boolean)_ : Can be used to continue streaming _(instead of terminating immediately)_ with emulated blank frames with text "No Input", whenever the input source disconnects. Its default value is `False`. Its usage is as follows + + !!! new "New in v0.2.1" + `enable_infinite_frames` attribute was added in `v0.2.1`. + + ```python + # emulate infinite frames + options = {"enable_infinite_frames": True} + # assign it + WebGear(logging=True, **options) + ```   diff --git a/docs/gears/webgear/usage.md b/docs/gears/webgear/usage.md index 922efe350..fedf47ccc 100644 --- a/docs/gears/webgear/usage.md +++ b/docs/gears/webgear/usage.md @@ -54,24 +54,27 @@ WebGear provides certain performance enhancing attributes for its [`options`](.. * **Various Encoding Parameters:** - In WebGear API, the input video frames are first encoded into [**Motion JPEG (M-JPEG or MJPEG**)](https://en.wikipedia.org/wiki/Motion_JPEG) compression format, in which each video frame or interlaced field of a digital video sequence is compressed separately as a JPEG image, before sending onto a server. Therefore, WebGear API provides various attributes to have full control over JPEG encoding performance and quality, which are as follows: + In WebGear API, the input video frames are first encoded into [**Motion JPEG (M-JPEG or MJPEG**)](https://en.wikipedia.org/wiki/Motion_JPEG) compression format, in which each video frame or interlaced field of a digital video sequence is compressed separately as a JPEG image using [`simplejpeg`](https://gitlab.com/jfolz/simplejpeg) library, before sending onto a server. Therefore, WebGear API provides various attributes to have full control over JPEG encoding performance and quality, which are as follows: - * **`frame_jpeg_quality`**: _(int)_ It controls the JPEG encoder quality. Its value varies from `0` to `100` (the higher is the better quality but performance will be lower). Its default value is `95`. Its usage is as follows: + * **`jpeg_compression_quality`**: _(int/float)_ This attribute controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. Its usage is as follows: ```python - options={"frame_jpeg_quality": 80} #JPEG will be encoded at 80% quality. + # activate jpeg encoding and set quality 95% + options = {"jpeg_compression_quality": 95} ``` - * **`frame_jpeg_optimize`**: _(bool)_ It enables various JPEG compression optimizations such as Chroma sub-sampling, Quantization table, etc. These optimizations based on JPEG libs which are used while compiling OpenCV binaries, and recent versions of OpenCV uses [**TurboJPEG library**](https://libjpeg-turbo.org/), which is highly recommended for performance. Its default value is `False`. Its usage is as follows: - + * **`jpeg_compression_fastdct`**: _(bool)_ This attribute if True, WebGear API uses fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`, and its usage is as follows: + ```python - options={"frame_jpeg_optimize": True} #JPEG optimizations are enabled. + # activate jpeg encoding and enable fast dct + options = {"jpeg_compression_fastdct": True} ``` - * **`frame_jpeg_progressive`**: _(bool)_ It enables **Progressive** JPEG encoding instead of the **Baseline**. Progressive Mode, displays an image in such a way that it shows a blurry/low-quality photo in its entirety, and then becomes clearer as the image downloads, whereas in Baseline Mode, an image created using the JPEG compression algorithm that will start to display the image as the data is made available, line by line. Progressive Mode, can drastically improve the performance in WebGear but at the expense of additional CPU load, thereby suitable for powerful systems only. Its default value is `False` meaning baseline mode is in-use. Its usage is as follows: - + * **`jpeg_compression_fastupsample`**: _(bool)_ This attribute if True, WebGear API use fastest color upsampling method. Its default value is `False`, and its usage is as follows: + ```python - options={"frame_jpeg_progressive": True} #Progressive JPEG encoding enabled. + # activate jpeg encoding and enable fast upsampling + options = {"jpeg_compression_fastupsample": True} ```   @@ -96,9 +99,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 @@ -123,7 +126,7 @@ You can also access and run WebGear Server directly from the terminal commandlin !!! warning "If you're using `--options/-op` flag, then kindly wrap your dictionary value in single `''` quotes." ```sh -python3 -m vidgear.gears.asyncio --source test.avi --logging True --options '{"frame_size_reduction": 50, "frame_jpeg_quality": 80, "frame_jpeg_optimize": True, "frame_jpeg_progressive": False}' +python3 -m vidgear.gears.asyncio --source test.avi --logging True --options '{"frame_size_reduction": 50, "jpeg_compression_quality": 80, "jpeg_compression_fastdct": True, "jpeg_compression_fastupsample": False}' ``` which can also be accessed on any browser on the network at http://localhost:8000/. diff --git a/docs/gears/webgear_rtc/params.md b/docs/gears/webgear_rtc/params.md index 81ce84e03..5c14da1e4 100644 --- a/docs/gears/webgear_rtc/params.md +++ b/docs/gears/webgear_rtc/params.md @@ -75,7 +75,7 @@ This parameter can be used to pass user-defined parameter to WebGear_RTC API by WebGear_RTC(logging=True, **options) ``` -* **`frame_size_reduction`** _(int/float)_ : This attribute controls the size reduction _(in percentage)_ of the frame to be streamed on Server. The value defaults to `20`, and must be no higher than `90` _(fastest, max compression, Barely Visible frame-size)_ and no lower than `0` _(slowest, no compression, Original frame-size)_. Its recommended value is between `40-60`. Its usage is as follows: +* **`frame_size_reduction`** _(int/float)_ : This attribute controls the size reduction _(in percentage)_ of the frame to be streamed on Server and it has the most significant effect on performance. The value defaults to `20`, and must be no higher than `90` _(fastest, max compression, Barely Visible frame-size)_ and no lower than `0` _(slowest, no compression, Original frame-size)_. Its recommended value is between `40-60`. Its usage is as follows: ```python # frame-size will be reduced by 50% @@ -84,6 +84,36 @@ This parameter can be used to pass user-defined parameter to WebGear_RTC API by WebGear_RTC(logging=True, **options) ``` +* **`jpeg_compression_quality`** _(int/float)_ : This attribute controls the JPEG quantization factor. Its value varies from `10` to `100` (the higher is the better quality but performance will be lower). Its default value is `90`. Its usage is as follows: + + ```python + # activate jpeg encoding and set quality 95% + options = {"jpeg_compression": True, "jpeg_compression_quality": 95} + ``` + +* **`jpeg_compression_fastdct`** _(bool)_ : This attribute if True, WebGear API uses fastest DCT method that speeds up decoding by 4-5% for a minor loss in quality. Its default value is also `True`, and its usage is as follows: + + ```python + # activate jpeg encoding and enable fast dct + options = {"jpeg_compression": True, "jpeg_compression_fastdct": True} + ``` + +* **`jpeg_compression_fastupsample`** _(bool)_ : This attribute if True, WebGear API use fastest color upsampling method. Its default value is `False`, and its usage is as follows: + + ```python + # activate jpeg encoding and enable fast upsampling + options = {"jpeg_compression": True, "jpeg_compression_fastupsample": True} + ``` + +* **`jpeg_compression_colorspace`** _(str)_ : This internal attribute is used to specify incoming frames colorspace with compression. Its usage is as follows: + + !!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" + + ```python + # Specify incoming frames are `grayscale` + options = {"jpeg_compression": "GRAY"} + ``` + * **`enable_live_broadcast`** _(boolean)_ : WebGear_RTC by default only supports one-to-one peer connection with a single consumer/client, Hence this boolean attribute can be used to enable live broadcast to multiple peer consumers/clients at same time. Its default value is `False`. Its usage is as follows: !!! note "`enable_infinite_frames` is enforced by default when this attribute is enabled(`True`)." From fc08c4973f32f086e45b38899eef9b2269c7dc41 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 20 Jul 2021 07:16:49 +0530 Subject: [PATCH 071/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Updated=20tests=20?= =?UTF-8?q?to=20increase=20coverage.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../asyncio_tests/test_helper.py | 10 ++- .../asyncio_tests/test_webgear.py | 82 ++++++++++++------- vidgear/tests/test_helper.py | 21 ++++- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/vidgear/tests/network_tests/asyncio_tests/test_helper.py b/vidgear/tests/network_tests/asyncio_tests/test_helper.py index 6c2682d85..e3436c474 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_helper.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_helper.py @@ -62,6 +62,7 @@ def getframe(): (getframe(), 85, cv2.INTER_AREA, True), (None, 80, cv2.INTER_AREA, False), (getframe(), 95, cv2.INTER_AREA, False), + (getframe(), 80, "invalid", False), (getframe(), 80, 797, False), ], ) @@ -92,14 +93,19 @@ async def test_reducer_asyncio(frame, percentage, interpolation, result): @pytest.mark.asyncio @pytest.mark.parametrize( "frame , text", - [(getframe(), "ok"), (None, ""), (getframe(), 123)], + [ + (getframe(), "ok"), + (cv2.cvtColor(getframe(), cv2.COLOR_BGR2BGRA), "ok"), + (None, ""), + (getframe(), 123), + ], ) async def test_create_blank_frame_asyncio(frame, text): """ Testing create_blank_frame function """ try: - text_frame = create_blank_frame(frame=frame, text=text) + text_frame = create_blank_frame(frame=frame, text=text, logging=True) logger.debug(text_frame.shape) assert not (text_frame is None) except Exception as e: diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index 4b9fb7618..1324ac185 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -108,36 +108,58 @@ def test_webgear_class(source, stabilize, colorspace, time_delay): pytest.fail(str(e)) -test_data = [ - { - "frame_size_reduction": 47, - "frame_jpeg_quality": 88, - "frame_jpeg_optimize": True, - "frame_jpeg_progressive": False, - "overwrite_default_files": "invalid_value", - "enable_infinite_frames": "invalid_value", - "custom_data_location": True, - }, - { - "frame_size_reduction": "invalid_value", - "frame_jpeg_quality": "invalid_value", - "frame_jpeg_optimize": "invalid_value", - "frame_jpeg_progressive": "invalid_value", - "overwrite_default_files": True, - "enable_infinite_frames": False, - "custom_data_location": "im_wrong", - }, - {"custom_data_location": tempfile.gettempdir()}, -] - - -@pytest.mark.parametrize("options", test_data) +@pytest.mark.parametrize( + "options", + [ + { + "jpeg_compression_colorspace": "invalid", + "jpeg_compression_quality": 5, + "custom_data_location": True, + "jpeg_compression_fastdct": "invalid", + "jpeg_compression_fastupsample": "invalid", + "frame_size_reduction": "invalid", + "overwrite_default_files": "invalid", + "enable_infinite_frames": "invalid", + }, + { + "jpeg_compression": " gray ", + "jpeg_compression_quality": 50, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, + "overwrite_default_files": True, + "enable_infinite_frames": False, + "custom_data_location": tempfile.gettempdir(), + }, + { + "jpeg_compression_quality": 55.55, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, + "custom_data_location": "im_wrong", + }, + { + "enable_infinite_frames": True, + "custom_data_location": return_testvideo_path(), + }, + ], +) def test_webgear_options(options): """ Test for various WebGear API internal options """ try: - web = WebGear(source=return_testvideo_path(), logging=True, **options) + colorspace = ( + "COLOR_BGR2GRAY" + if "jpeg_compression_colorspace" in options + and isinstance(options["jpeg_compression_colorspace"], str) + and options["jpeg_compression_colorspace"].strip().upper() == "GRAY" + else None + ) + web = WebGear( + source=return_testvideo_path(), + colorspace=colorspace, + logging=True, + **options + ) client = TestClient(web(), raise_server_exceptions=True) response = client.get("/") assert response.status_code == 200 @@ -145,8 +167,8 @@ def test_webgear_options(options): assert response_video.status_code == 200 web.shutdown() except Exception as e: - if isinstance(e, AssertionError): - logger.exception(str(e)) + if isinstance(e, AssertionError) or isinstance(e, os.access): + pytest.xfail(str(e)) elif isinstance(e, requests.exceptions.Timeout): logger.exceptions(str(e)) else: @@ -209,9 +231,9 @@ def test_webgear_routes(): # add various performance tweaks as usual 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 web = WebGear(source=return_testvideo_path(), logging=True, **options) diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 27472224f..0a1d8e512 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -41,6 +41,7 @@ create_blank_frame, is_valid_url, logger_handler, + delete_file_safe, validate_audio, validate_video, validate_ffmpeg, @@ -326,6 +327,7 @@ def test_check_output(): (getframe(), 85, cv2.INTER_AREA, True), (None, 80, cv2.INTER_AREA, False), (getframe(), 95, cv2.INTER_AREA, False), + (getframe(), 80, "invalid", False), (getframe(), 80, 797, False), ], ) @@ -413,14 +415,19 @@ def test_validate_audio(path, result): @pytest.mark.parametrize( "frame , text", - [(getframe(), "ok"), (None, ""), (getframe(), 123)], + [ + (getframe(), "ok"), + (cv2.cvtColor(getframe(), cv2.COLOR_BGR2BGRA), "ok"), + (None, ""), + (cv2.cvtColor(getframe(), cv2.COLOR_BGR2GRAY), 123), + ], ) def test_create_blank_frame(frame, text): """ Testing create_blank_frame function """ try: - text_frame = create_blank_frame(frame=frame, text=text) + text_frame = create_blank_frame(frame=frame, text=text, logging=True) logger.debug(text_frame.shape) assert not (text_frame is None) except Exception as e: @@ -561,3 +568,13 @@ def test_retrieve_best_interpolation(interpolations): assert output, "Test failed" except Exception as e: pytest.fail(str(e)) + + +def test_delete_file_safe(): + """ + Testing delete_file_safe method + """ + try: + delete_file_safe(os.path.join(expanduser("~"), "invalid")) + except Exception as e: + pytest.fail(str(e)) From 3d64362332b314083415d1c56fe32329b72d0ae8 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 20 Jul 2021 07:24:53 +0530 Subject: [PATCH 072/112] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20CI:=20Fixed=20typo?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index 1324ac185..42ecd9851 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -122,7 +122,7 @@ def test_webgear_class(source, stabilize, colorspace, time_delay): "enable_infinite_frames": "invalid", }, { - "jpeg_compression": " gray ", + "jpeg_compression_colorspace": " gray ", "jpeg_compression_quality": 50, "jpeg_compression_fastdct": True, "jpeg_compression_fastupsample": True, From 4372edec935a22ae80f340ff8100bf89becceaba Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 20 Jul 2021 09:06:12 +0530 Subject: [PATCH 073/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Helper:=20Improved?= =?UTF-8?q?=20and=20simplified=20`create=5Fblank=5Fframe`=20method=20frame?= =?UTF-8?q?=20channels=20detection.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/asyncio/helper.py | 14 +++----------- vidgear/gears/helper.py | 14 +++----------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index a3619b324..48fe14a50 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -186,12 +186,12 @@ def create_blank_frame(frame=None, text="", logging=False): **Returns:** A reduced numpy ndarray array. """ # check if frame is valid - if frame is None: - raise ValueError("[Helper:ERROR] :: Input frame cannot be NoneType!") + if frame is None or not (isinstance(frame, np.ndarray)): + raise ValueError("[Helper:ERROR] :: Input frame is invalid!") # grab the frame size (height, width) = frame.shape[:2] # create blank frame - blank_frame = np.zeros((height, width, 3), np.uint8) + blank_frame = np.zeros(frame.shape, frame.dtype) # setup text if text and isinstance(text, str): if logging: @@ -209,14 +209,6 @@ def create_blank_frame(frame=None, text="", logging=False): blank_frame, text, (textX, textY), font, fontScale, (125, 125, 125), 6 ) - # correct channels - if frame.ndim == 2: - blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2GRAY) - elif frame.ndim == 3 and frame.shape[-1] == 4: - blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2BGRA) - else: - pass - # return frame return blank_frame diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index c389d422e..6c2d9a108 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -423,12 +423,12 @@ def create_blank_frame(frame=None, text="", logging=False): **Returns:** A reduced numpy ndarray array. """ # check if frame is valid - if frame is None: - raise ValueError("[Helper:ERROR] :: Input frame cannot be NoneType!") + if frame is None or not (isinstance(frame, np.ndarray)): + raise ValueError("[Helper:ERROR] :: Input frame is invalid!") # grab the frame size (height, width) = frame.shape[:2] # create blank frame - blank_frame = np.zeros((height, width, 3), np.uint8) + blank_frame = np.zeros(frame.shape, frame.dtype) # setup text if text and isinstance(text, str): if logging: @@ -446,14 +446,6 @@ def create_blank_frame(frame=None, text="", logging=False): blank_frame, text, (textX, textY), font, fontScale, (125, 125, 125), 6 ) - # correct channels - if frame.ndim == 2: - blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2GRAY) - elif frame.ndim == 3 and frame.shape[-1] == 4: - blank_frame = cv2.cvtColor(blank_frame, cv2.COLOR_BGR2BGRA) - else: - pass - # return frame return blank_frame From 545db174cfa39baab480caf324694515932c397c Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 23 Jul 2021 11:04:19 +0530 Subject: [PATCH 074/112] =?UTF-8?q?=E2=9C=A8=20StreamGear:=20Native=20Supp?= =?UTF-8?q?ort=20for=20Apple=20HLS=20(Fixes=20#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✨ Added support for new [Apple HLS](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_ HTTP streaming format in StreamGear. - ⚡️ Implemented default workflow for auto-generating primary HLS stream of same resolution and framerate as source. - ✨ Added HLS support in *Single-Source* and *Real-time Frames* Modes. - ⚡️ Implemented inherit support for `fmp4` and `mpegts` HLS segment types. - ✨ Added adequate default parameters required for transcoding HLS streams. - ✨ Added native support for HLS live-streaming. - 🚩 Added "hls" value to `format` parameter for easily selecting HLS format. - ✨ Added HLS support in `-streams` attribute for transcoding additional streams. - ✨ Added support for `.m3u8` and `.ts` extensions in `clear_prev_assets` workflow. - 🔒️ Added validity check for `.m3u8` extension in output when HLS format is used. - 🐛 Fixed expected aspect ratio not calculated correctly for additional streams. - 🏗️ Separated DASH and HLS command handlers. - 💡 Updated comments in source code. - 🔊 Updated logging. 👷 CI: Update `mpegdash` dependency to `0.3.0-dev2` version in Appveyor. Docs: - 📝 Re-aligned badges in README.md. - 📝 Updated context. --- README.md | 4 +- appveyor.yml | 2 +- docs/contribution/PR.md | 2 +- docs/contribution/issue.md | 2 +- docs/help/get_help.md | 2 +- docs/switch_from_cv.md | 12 +- vidgear/gears/streamgear.py | 259 ++++++++++++++++++++++++------------ 7 files changed, 183 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index f944246bb..c272f69f0 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ limitations under the License. [Releases][release]   |   [Gears][gears]   |   [Documentation][docs]   |   [Installation][installation]   |   [License](#license) -[![Build Status][github-cli]][github-flow] [![Build Status][appveyor]][app] [![Azure DevOps builds (branch)][azure-badge]][azure-pipeline] +[![Build Status][github-cli]][github-flow] [![Codecov branch][codecov]][code] [![Azure DevOps builds (branch)][azure-badge]][azure-pipeline] -[![PyPi version][pypi-badge]][pypi] [![Codecov branch][codecov]][code] [![Glitter chat][gitter-bagde]][gitter] +[![Glitter chat][gitter-bagde]][gitter] [![Build Status][appveyor]][app] [![PyPi version][pypi-badge]][pypi] [![Code Style][black-badge]][black] diff --git a/appveyor.yml b/appveyor.yml index f125375e5..78f7c6ccd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -52,7 +52,7 @@ install: - "python --version" - "python -m pip install --upgrade pip wheel" - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko async-asgi-testclient" - - "python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev/mpegdash-0.3.0.dev0-py3-none-any.whl" + - "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 diff --git a/docs/contribution/PR.md b/docs/contribution/PR.md index 19b4c3e75..8fa7dfa88 100644 --- a/docs/contribution/PR.md +++ b/docs/contribution/PR.md @@ -192,7 +192,7 @@ Testing VidGear requires additional test dependencies and dataset, which can be The [`mpegdash`](https://github.com/sangwonl/python-mpegdash) library has not yet been updated and bugs on windows machines. Therefore install the forked [DEV-version of `mpegdash`](https://github.com/abhiTronix/python-mpegdash) as follows: ```sh - python -m pip install https://github.com/abhiTronix/python-mpegdash/releases/download/0.3.0-dev/mpegdash-0.3.0.dev0-py3-none-any.whl + 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 ``` ```sh diff --git a/docs/contribution/issue.md b/docs/contribution/issue.md index c111c481c..5d7d4a3ba 100644 --- a/docs/contribution/issue.md +++ b/docs/contribution/issue.md @@ -39,7 +39,7 @@ If you've found a new bug or you've come up with some new feature which can impr * All VidGear APIs provides a `logging` boolean flag in parameters, to log debugged output to terminal. Kindly turn this parameter `True` in the respective API for getting debug output, and paste it with your Issue. * In order to reproduce bugs we will systematically ask you to provide a minimal reproduction code for your report. -* Check and paste, exact VidGear version by running command `python -c "import vidgear; print(vidgear.__version__)"`. +* Check and paste, exact VidGear version by running command `#!python python -c "import vidgear; print(vidgear.__version__)"`. ### Follow the Issue Template diff --git a/docs/help/get_help.md b/docs/help/get_help.md index 4390d148d..48a8996c7 100644 --- a/docs/help/get_help.md +++ b/docs/help/get_help.md @@ -73,7 +73,7 @@ There you can ask quick questions, swiftly troubleshoot your problems, help othe - [x] [Got a question or problem?](../../contribution/#got-a-question-or-problem) - [x] [Found a typo?](../../contribution/#found-a-typo) - [x] [Found a bug?](../../contribution/#found-a-bug) -- [x] [Missing a feature/improvement?](../../contribution/#request-for-a-featureimprovementt) +- [x] [Missing a feature/improvement?](../../contribution/#request-for-a-featureimprovement)   diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index 5483e4e8e..a8f65a8c2 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -26,7 +26,7 @@ limitations under the License. # Switching from OpenCV Library -Switching OpenCV with VidGear APIs is usually a fairly painless process, and will just require changing a few lines in your python script. +Switching OpenCV with VidGear APIs is fairly painless process, and will just require changing a few lines in your python script. !!! abstract "This document is intended to software developers who want to migrate their python code from OpenCV Library to VidGear APIs." @@ -42,12 +42,12 @@ Switching OpenCV with VidGear APIs is usually a fairly painless process, and wil VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art functionalities such as: -- [x] Accelerated [Multi-Threaded](../bonus/TQM/#c-accelerates-frame-processing) Performance. -- [x] Real-time Stabilization. -- [x] Inherit support for multiple sources. -- [x] Screen-casting, Live network-streaming, [plus way much more ➶](../gears) +- [x] Accelerated [Multi-Threaded](../bonus/TQM/#what-does-threaded-queue-mode-exactly-do) Performance. +- [x] Real-time [Stabilization](../gears/stabilizer/overview/). +- [x] Inherit support for multiple video sources and formats. +- [x] Screen-casting, Live network-streaming, and [way much more ➶](../gears) -Vidgear offers all this at once while maintaining the same standard OpenCV-Python _(Python API for OpenCV)_ coding syntax for all of its APIs, thereby making it even easier to implement complex OpenCV applications in way fewer lines and without changing your python code much. +Vidgear offers all this at once while maintaining the same standard OpenCV-Python _(Python API for OpenCV)_ coding syntax for all of its APIs, thereby making it even easier to implement complex real-time OpenCV applications in python code without changing things much.   diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 717fbb3f9..70ee586a7 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -23,6 +23,7 @@ import cv2 import sys import time +import math import difflib import logging as log import subprocess as sp @@ -52,17 +53,17 @@ class StreamGear: """ - StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH) in just few lines of python code. + StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH and HLS) in just few lines of python code. 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 fixed 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. - SteamGear also creates a Manifest file (such as MPD in-case of DASH) besides segments that describe these segment information (timing, URL, media characteristics like video resolution and bit rates) + SteamGear also creates a Manifest file (such as MPD in-case of DASH, 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 only supports MPEG-DASH (Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1) , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be + SteamGear currently only supports MPEG-DASH (Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1) and Apple HLS (HTTP live streaming), but other adaptive streaming technologies such as Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. """ @@ -74,7 +75,7 @@ def __init__( Parameters: output (str): sets the valid filename/path for storing the StreamGear assets. - format (str): select the adaptive HTTP streaming format. + 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 properities. @@ -182,7 +183,7 @@ def __init__( ) ) else: - logger.warning("No valid video_source detected.") + logger.warning("No valid video_source provided.") else: self.__video_source = "" @@ -208,12 +209,17 @@ def __init__( self.__livestreaming = False # handle Streaming formats - supported_formats = ["dash"] # will be extended in future + supported_formats = ["dash", "hls"] # will be extended in future # Validate if not (format is None) and 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( + self.__format.upper() + ) + ) elif difflib.get_close_matches(_format, supported_formats): raise ValueError( "[StreamGear:ERROR] :: Incorrect format! Did you mean `{}`?".format( @@ -248,6 +254,7 @@ def __init__( # get all assets extensions assets_exts = [ ("chunk-stream", ".m4s"), # filename prefix, extension + ("chunk-stream", ".ts"), # filename prefix, extension ".{}".format(valid_extension), ] # add source file extension too @@ -307,7 +314,7 @@ def __init__( ) ) # log Mode of operation - logger.critical( + logger.info( "StreamGear has been successfully configured for {} Mode.".format( "Single-Source" if self.__video_source else "Real-time Frames" ) @@ -417,14 +424,14 @@ def __PreProcess(self, channels=0, rgb=False): "libx265", "libvpx-vp9", ]: - output_parameters["-crf"] = self.__params.pop("-crf", "18") + output_parameters["-crf"] = self.__params.pop("-crf", "20") if output_parameters["-vcodec"] in ["libx264", "libx264rgb"]: if not (self.__video_source): output_parameters["-profile:v"] = self.__params.pop( "-profile:v", "high" ) output_parameters["-tune"] = self.__params.pop("-tune", "zerolatency") - output_parameters["-preset"] = self.__params.pop("-preset", "ultrafast") + output_parameters["-preset"] = self.__params.pop("-preset", "veryfast") if output_parameters["-vcodec"] == "libx265": output_parameters["-x265-params"] = self.__params.pop( "-x265-params", "lossless=1" @@ -494,12 +501,9 @@ def __PreProcess(self, channels=0, rgb=False): "[StreamGear:ERROR] :: Frames with channels outside range 1-to-4 are not supported!" ) # process assigned format parameters - process_params = None - if self.__format == "dash": - process_params = self.__generate_dash_stream( - input_params=input_parameters, - output_params=output_parameters, - ) + process_params = self.__handle_streams( + input_params=input_parameters, output_params=output_parameters + ) # check if processing completed successfully assert not ( process_params is None @@ -509,6 +513,103 @@ def __PreProcess(self, channels=0, rgb=False): # 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): + """ + An internal function that parses various streams and its parameters. + + Parameters: + input_params (dict): Input FFmpeg parameters + output_params (dict): Output FFmpeg parameters + """ + # 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 + else: + # reset to defaut if invalid + bpp = 0.1000 + # log it + if self.__logging: + 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 = int(gop) + else: + # reset to some recommended value + gop = 2 * int(self.__sourceframerate) + # log it + if self.__logging: + logger.debug("Setting GOP: {} for this stream.".format(gop)) + + # define and map default stream + output_params["-map"] = 0 + # assign 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 + 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, + ) + ) + + "k" + ) + # assign 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", "") + if "-acodec" in output_params and a_bitrate: + output_params["-b:a:0"] = a_bitrate + + # handle user-defined streams + streams = self.__params.pop("-streams", {}) + output_params = self.__evaluate_streams(streams, output_params, bpp) + + # define additional stream optimization parameters + if output_params["-vcodec"] in ["libx264", "libx264rgb"]: + if not "-bf" in self.__params: + output_params["-bf"] = 1 + if not "-sc_threshold" in self.__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"] == "libx265": + output_params["-core_x265"] = [ + "-x265-params", + "keyint={}:min-keyint={}".format(gop, gop), + ] + + # process given dash/hls stream + processed_params = None + if self.__format == "dash": + processed_params = self.__generate_dash_stream( + input_params=input_params, + output_params=output_params, + ) + else: + processed_params = self.__generate_hls_stream( + input_params=input_params, + output_params=output_params, + ) + + return processed_params + def __evaluate_streams(self, streams, output_params, bpp): """ Internal function that Extracts, Evaluates & Validates user-defined streams @@ -519,7 +620,7 @@ def __evaluate_streams(self, streams, output_params, bpp): """ # check if streams are empty if not streams: - logger.warning("No `-streams` are provided for this stream.") + logger.warning("No `-streams` are provided!") return output_params # check if data is valid @@ -552,10 +653,12 @@ def __evaluate_streams(self, streams, output_params, bpp): and dimensions[1].isnumeric() ): # verify resolution is w.r.t source aspect-ratio - expected_width = self.__inputheight * source_aspect_ratio - if int(dimensions[0]) != round(expected_width): + expected_width = math.floor( + int(dimensions[1]) * source_aspect_ratio + ) + 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( + "Given stream resolution `{}` is not in accordance with the Source Aspect-Ratio. Stream Output may appear Distorted!".format( resolution ) ) @@ -623,88 +726,68 @@ def __evaluate_streams(self, streams, output_params, bpp): return output_params - def __generate_dash_stream(self, input_params, output_params): + def __generate_hls_stream(self, input_params, output_params): """ An internal function that parses user-defined parameters and generates - suitable FFmpeg Terminal Command for transcoding input into MPEG-dash Stream. + suitable FFmpeg Terminal Command for transcoding input into HLS Stream. Parameters: input_params (dict): Input FFmpeg parameters output_params (dict): Output FFmpeg parameters """ - # 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 - else: - # reset to defaut if invalid - bpp = 0.1000 - # log it - if self.__logging: - logger.debug("Setting bit-per-pixels: {} for this stream.".format(bpp)) + # Check if live-streaming or not? - # handle gop - gop = self.__params.pop("-gop", 0) - if isinstance(gop, (int, float)) and gop > 0: - gop = int(gop) + # validate `hls_segment_type` + default_hls_segment_type = self.__params.pop("-hls_segment_type", "mpegts") + if isinstance( + default_hls_segment_type, int + ) and default_hls_segment_type.strip() in ["fmp4", "mpegts"]: + output_params["-hls_segment_type"] = default_hls_segment_type.strip() else: - # reset to some recommended value - gop = 2 * int(self.__sourceframerate) - # log it - if self.__logging: - logger.debug("Setting GOP: {} for this stream.".format(gop)) + output_params["-hls_segment_type"] = "mpegts" - # define and map default stream - output_params["-map"] = 0 - # assign 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 - 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, - ) + # gather required parameters + if self.__livestreaming: + # `hls_list_size` must be greater than 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: + output_params["-hls_list_size"] = default_hls_list_size + else: + # otherwise reset to default + 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_flags", "delete_segments+discont_start+split_by_time" ) - + "k" + # clean everything at exit? + output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) + else: + # enforce "contain all the segments" + output_params["-hls_list_size"] = 0 + output_params["-hls_playlist_type"] = "vod" + + # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) + output_params["-allowed_extensions"] = "ALL" + output_params["-hls_segment_filename"] = "{}-stream%v-%03d.{}".format( + os.path.join(os.path.dirname(self.__out_file), "chunk"), + "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", ) - # assign 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", "") - if "-acodec" in output_params and a_bitrate: - output_params["-b:a:0"] = a_bitrate + output_params["-hls_allow_cache"] = 0 + # enable hls formatting + output_params["-f"] = "hls" + return (input_params, output_params) - # handle user-defined streams - streams = self.__params.pop("-streams", {}) - output_params = self.__evaluate_streams(streams, output_params, bpp) + def __generate_dash_stream(self, input_params, output_params): + """ + An internal function that parses user-defined parameters and generates + suitable FFmpeg Terminal Command for transcoding input into MPEG-dash Stream. - # define additional stream optimization parameters - if output_params["-vcodec"] in ["libx264", "libx264rgb"]: - if not "-bf" in self.__params: - output_params["-bf"] = 1 - if not "-sc_threshold" in self.__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"] == "libx265": - output_params["-core_x265"] = [ - "-x265-params", - "keyint={}:min-keyint={}".format(gop, gop), - ] + Parameters: + input_params (dict): Input FFmpeg parameters + output_params (dict): Output FFmpeg parameters + """ # Check if live-streaming or not? if self.__livestreaming: @@ -872,7 +955,7 @@ def terminate(self): self.__process.wait() self.__process = None # log it - logger.debug( + logger.critical( "Transcoding Ended. {} Streaming assets are successfully generated at specified path.".format( self.__format.upper() ) From 3c90d837359a5374bc77486e3f1cd57ba1296602 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 2 Aug 2021 20:43:35 +0530 Subject: [PATCH 075/112] :bug: StreamGear: Fixed Multi Bitrate HLS VOD streams - :art: Reimplemented complete workflow for Multi Bitrate HLS VOD streams. - :wrench: Created HLS format exclusive params. - :art: Extended support to both *Single-Source* and *Real-time Frames* Modes. - :bug: Fixed bugs with audio-video mapping. - :bug: Fixed master playlist not generating in output. - :bulb: Updated code comments. - :pushpin: Setup: Fixed streamlink only supporting requests==2.25.1 on Windows. --- setup.py | 2 +- vidgear/gears/streamgear.py | 78 ++++++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index 37124e464..87de119a3 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def latest_version(package_name): ), # dropped support for 3.6.x legacies "youtube-dl{}".format(latest_version("youtube-dl")), "streamlink{}".format(latest_version("streamlink")), - "requests{}".format(latest_version("requests")), + "requests", "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format(latest_version("simplejpeg")), "colorlog", diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 70ee586a7..6a84b566e 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -449,7 +449,7 @@ def __PreProcess(self, channels=0, rgb=False): # assign audio codec output_parameters["-acodec"] = self.__params.pop("-acodec", "copy") output_parameters["a_bitrate"] = bitrate # temporary handler - output_parameters["-core_audio"] = ["-map", "1:a:0"] + output_parameters["-core_audio"] = ["-map", "a:0"] else: logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) @@ -544,7 +544,12 @@ def __handle_streams(self, input_params, output_params): logger.debug("Setting GOP: {} for this stream.".format(gop)) # define and map default stream - output_params["-map"] = 0 + if self.__format != "hls": + output_params["-map"] = 0 + else: + output_params["-corev0"] = ["-map", "0:v"] + if "-acodec" in output_params: + output_params["-corea0"] = ["-map", "0:a"] # assign resolution if "-s:v:0" in self.__params: # prevent duplicates @@ -616,16 +621,19 @@ def __evaluate_streams(self, streams, output_params, bpp): Parameters: streams (dict): Indivisual streams formatted as list of dict. - rgb_mode (boolean): activates RGB mode _(if enabled)_. + output_params (dict): Output FFmpeg parameters """ + # temporary streams count variable + output_params["stream_count"] = 0 # default is 0 + # check if streams are empty if not streams: logger.warning("No `-streams` are provided!") return output_params - # check if data is valid + # check if streams are valid if isinstance(streams, list) and all(isinstance(x, dict) for x in streams): - stream_num = 1 # keep track of streams + stream_count = 1 # keep track of streams # calculate source aspect-ratio source_aspect_ratio = self.__inputwidth / self.__inputheight # log the process @@ -637,7 +645,15 @@ def __evaluate_streams(self, streams, output_params, bpp): intermediate_dict = {} # handles intermediate stream data as dictionary # define and map stream to intermediate dict - intermediate_dict["-core{}".format(stream_num)] = ["-map", "0"] + if self.__format != "hls": + intermediate_dict["-core{}".format(stream_count)] = ["-map", "0"] + else: + intermediate_dict["-corev{}".format(stream_count)] = ["-map", "0:v"] + if "-acodec" in output_params: + intermediate_dict["-corea{}".format(stream_count)] = [ + "-map", + "0:a", + ] # extract resolution & indivisual dimension of stream resolution = stream.pop("-resolution", "") @@ -663,7 +679,7 @@ def __evaluate_streams(self, streams, output_params, bpp): ) ) # assign stream resolution to intermediate dict - intermediate_dict["-s:v:{}".format(stream_num)] = resolution + intermediate_dict["-s:v:{}".format(stream_count)] = resolution else: # otherwise log error and skip stream logger.error( @@ -681,12 +697,14 @@ def __evaluate_streams(self, streams, output_params, bpp): and video_bitrate.endswith(("k", "M")) ): # assign it - intermediate_dict["-b:v:{}".format(stream_num)] = video_bitrate + intermediate_dict["-b:v:{}".format(stream_count)] = video_bitrate else: # otherwise calculate video-bitrate fps = stream.pop("-framerate", 0.0) if dimensions and isinstance(fps, (float, int)) and fps > 0: - intermediate_dict["-b:v:{}".format(stream_num)] = "{}k".format( + intermediate_dict[ + "-b:v:{}".format(stream_count) + ] = "{}k".format( get_video_bitrate( int(dimensions[0]), int(dimensions[1]), fps, bpp ) @@ -703,13 +721,15 @@ def __evaluate_streams(self, streams, output_params, bpp): audio_bitrate = stream.pop("-audio_bitrate", "") if "-acodec" in output_params: if audio_bitrate and audio_bitrate.endswith(("k", "M")): - intermediate_dict["-b:a:{}".format(stream_num)] = audio_bitrate + intermediate_dict[ + "-b:a:{}".format(stream_count) + ] = audio_bitrate else: # otherwise calculate audio-bitrate if dimensions: aspect_width = int(dimensions[0]) intermediate_dict[ - "-b:a:{}".format(stream_num) + "-b:a:{}".format(stream_count) ] = "{}k".format(128 if (aspect_width > 800) else 96) # update output parameters output_params.update(intermediate_dict) @@ -718,11 +738,12 @@ def __evaluate_streams(self, streams, output_params, bpp): # clear stream copy stream_copy.clear() # increment to next stream - stream_num += 1 + stream_count += 1 + output_params["stream_count"] = stream_count if self.__logging: logger.debug("All streams processed successfully!") else: - logger.warning("Invalid `-streams` values skipped!") + logger.warning("Invalid type `-streams` skipped!") return output_params @@ -771,7 +792,7 @@ def __generate_hls_stream(self, input_params, output_params): # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-allowed_extensions"] = "ALL" output_params["-hls_segment_filename"] = "{}-stream%v-%03d.{}".format( - os.path.join(os.path.dirname(self.__out_file), "chunk"), + os.path.join(*[os.path.dirname(self.__out_file), "chunk"]), "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", ) output_params["-hls_allow_cache"] = 0 @@ -829,13 +850,32 @@ def __Build_n_Execute(self, input_params, output_params): if "-i" in output_params: output_params.move_to_end("-i", last=False) + # copy streams count + stream_count = output_params.pop("stream_count", 0) + # convert input parameters to list input_commands = dict2Args(input_params) # convert output parameters to list output_commands = dict2Args(output_params) # convert any additional parameters to list stream_commands = dict2Args(self.__params) - # log it + + # create exclusive HLS params + hls_commands = [] + # handle HLS multi-bitrate according to stream count + if self.__format == "hls" and stream_count > 0: + stream_map = "" + for count in range(0, stream_count): + stream_map += "v:{},a:{} ".format(count, count) + hls_commands += [ + "-master_pl_name", + os.path.basename(self.__out_file), + "-var_stream_map", + stream_map.strip(), + os.path.join(os.path.dirname(self.__out_file), "stream_%v.m3u8"), + ] + + # log it if enabled if self.__logging: logger.debug( "User-Defined Output parameters: `{}`".format( @@ -851,8 +891,8 @@ def __Build_n_Execute(self, input_params, output_params): ffmpeg_cmd = None hide_banner = ( [] if self.__logging else ["-hide_banner"] - ) # ensure less cluterring - # format command + ) # ensuring less cluterring if specified + # format commands if self.__video_source: ffmpeg_cmd = ( [self.__ffmpeg, "-y"] @@ -862,7 +902,6 @@ def __Build_n_Execute(self, input_params, output_params): + input_commands + output_commands + stream_commands - + [self.__out_file] ) else: ffmpeg_cmd = ( @@ -873,8 +912,9 @@ def __Build_n_Execute(self, input_params, output_params): + ["-i", "-"] + output_commands + stream_commands - + [self.__out_file] ) + # format outputs + ffmpeg_cmd.extend([self.__out_file] if not (hls_commands) else hls_commands) # Launch the FFmpeg pipeline with built command logger.critical("Transcoding streaming chunks. Please wait...") # log it self.__process = sp.Popen( From fb057ca40bd513774c28a9242f4ba7ae59cee108 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 2 Aug 2021 21:04:21 +0530 Subject: [PATCH 076/112] :memo: StreamGear: Updated docs --- docs/gears/streamgear/rtfm/overview.md | 11 ++++++----- docs/gears/streamgear/rtfm/usage.md | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index c480b78a1..f9bbf81d6 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -36,6 +36,12 @@ In this mode, StreamGear **DOES NOT** automatically maps video-source audio to g This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. + +!!! alert "Real-time Frames Mode is NOT Live-Streaming." + + 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 * 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`**! @@ -47,11 +53,6 @@ This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.g * Input framerate defaults to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined. -??? warning "Real-time Frames Mode is NOT Live-Streaming." - - You can enable live-streaming in Real-time Frames Mode by using using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of stream_params dictionary parameter in WebGear_RTC API. Checkout [this usage example](../usage/#bare-minimum-usage-with-live-streaming) for more information. - -   ## Usage Examples diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index c3aa05d01..a83d22a87 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -21,6 +21,10 @@ limitations under the License. # StreamGear API Usage Examples: Real-time Frames Mode +!!! alert "Real-time Frames Mode is NOT Live-Streaming." + + 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). + !!! warning "Important Information" * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../../ffmpeg_install/) for its installation. From 18bd74315ff416214a0f36ccf5c0414acd8df448 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 2 Aug 2021 21:29:16 +0530 Subject: [PATCH 077/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20contex?= =?UTF-8?q?ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- docs/contribution/issue.md | 2 +- docs/help/general_faqs.md | 14 ++++++++------ docs/index.md | 2 +- docs/switch_from_cv.md | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c272f69f0..aba01019e 100644 --- a/README.md +++ b/README.md @@ -89,13 +89,13 @@ The following **functional block diagram** clearly depicts the generalized funct #### What does it do? -> *"VidGear can read, write, process, send & receive video files/frames/streams from/to various devices in real-time, and faster than underline libraries."* +> *"VidGear can read, write, process, send & receive video files/frames/streams from/to various devices in real-time, and [**faster**][TQM-doc] than underline libraries."* #### What is its purpose? > *"Write Less and Accomplish More"* — **VidGear's Motto** -> *"Built with simplicity in mind, VidGear lets programmers and software developers to easily integrate and perform **Complex Video-Processing Tasks** in their existing or newer applications without going through hefty documentation and in just a [few lines of code][switch_from_cv]. Beneficial for both, if you're new to programming with Python language or already a pro at it."* +> *"Built with simplicity in mind, VidGear lets programmers and software developers to easily integrate and perform **Complex Video-Processing Tasks** in their existing or newer applications without going through hefty documentation and in just a [**few lines of code**][switch_from_cv]. Beneficial for both, if you're new to programming with Python language or already a pro at it."*   @@ -105,11 +105,11 @@ The following **functional block diagram** clearly depicts the generalized funct If this is your first time using VidGear, head straight to the [Installation ➶][installation] to install VidGear. -Once you have VidGear installed, **Checkout its Well-Documented Function-Specific [Gears ➶][gears]** +Once you have VidGear installed, **Checkout its Well-Documented [Function-Specific Gears ➶][gears]** Also, if you're already familiar with [OpenCV][opencv] library, then see [Switching from OpenCV Library ➶][switch_from_cv] -Or, if you're just getting started with OpenCV, then see [here ➶](https://abhitronix.github.io/vidgear/latest/help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects) +Or, if you're just getting started with OpenCV-Python programming, then refer this [FAQ ➶](https://abhitronix.github.io/vidgear/latest/help/general_faqs/#im-new-to-python-programming-or-its-usage-in-opencv-library-how-to-use-vidgear-in-my-projects)   diff --git a/docs/contribution/issue.md b/docs/contribution/issue.md index 5d7d4a3ba..c953beddd 100644 --- a/docs/contribution/issue.md +++ b/docs/contribution/issue.md @@ -43,7 +43,7 @@ If you've found a new bug or you've come up with some new feature which can impr ### Follow the Issue Template -* Please stick to the issue template. +* Please format your issue by choosing the appropriate templates. * Any improper/insufficient reports will be marked with **MISSING : INFORMATION :mag:** and **MISSING : TEMPLATE :grey_question:** like labels, and if we don't hear back from you we may close the issue. ### Raise the Issue diff --git a/docs/help/general_faqs.md b/docs/help/general_faqs.md index b7983af92..0b70c68bf 100644 --- a/docs/help/general_faqs.md +++ b/docs/help/general_faqs.md @@ -24,23 +24,25 @@ limitations under the License.   -## "I'm new to Python Programming or its usage in Computer Vision", How to use vidgear in my projects? +## "I'm new to Python Programming or its usage in OpenCV Library", How to use vidgear in my projects? -**Answer:** It's recommended to first go through the following dedicated tutorials/websites thoroughly, and learn how OpenCV-Python works _(with examples)_: +**Answer:** Before using vidgear, It's recommended to first go through the following dedicated blog sites and learn how OpenCV-Python syntax works _(with examples)_: -- [**PyImageSearch.com** ➶](https://www.pyimagesearch.com/) is the best resource for learning OpenCV and its Python implementation. Adrian Rosebrock provides many practical OpenCV techniques with tutorials, code examples, blogs, and books at PyImageSearch.com. I also learned a lot about computer vision methods and various useful techniques. Highly recommended! +- [**PyImageSearch.com** ➶](https://www.pyimagesearch.com/) is the best resource for learning OpenCV and its Python implementation. Adrian Rosebrock provides many practical OpenCV techniques with tutorials, code examples, blogs, and books at PyImageSearch.com. Highly recommended! - [**learnopencv.com** ➶](https://www.learnopencv.com) Maintained by OpenCV CEO Satya Mallick. This blog is for programmers, hackers, engineers, scientists, students, and self-starters interested in Computer Vision and Machine Learning. -- There's also the official [**OpenCV Tutorials** ➶](https://docs.opencv.org/master/d6/d00/tutorial_py_root.html), provided by the OpenCV folks themselves. +- There's also the official [**OpenCV Tutorials** ➶](https://docs.opencv.org/master/d6/d00/tutorial_py_root.html) curated by the OpenCV developers. -Finally, once done, see [Switching from OpenCV ➶](../../switch_from_cv/) and go through our [Gears ➶](../../gears/#gears-what-are-these) to learn how VidGear APIs works. If you run into any trouble or have any questions, then see [getting help ➶](../get_help) +Once done, visit [Switching from OpenCV ➶](../../switch_from_cv/) to easily replace OpenCV APIs with suitable [Gears ➶](../../gears/#gears-what-are-these) in your project. All the best! :smiley: + +!!! tip "If you run into any trouble or have any questions, then refer our [**Help**](../get_help) section."   ## "VidGear is using Multi-threading, but Python is notorious for its poor performance in multithreading?" -**Answer:** See [Threaded-Queue-Mode ➶](../../bonus/TQM/) +**Answer:** Refer vidgear's [Threaded-Queue-Mode ➶](../../bonus/TQM/)   diff --git a/docs/index.md b/docs/index.md index ad60c63a5..37d8f2a9a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -48,7 +48,7 @@ VidGear focuses on simplicity, and thereby lets programmers and software develop - [x] Also, if you're already familar with [**OpenCV**][opencv] library, then see **[Switching from OpenCV Library](switch_from_cv.md)**. -!!! alert "If you're just getting started with OpenCV-Python, then see [here ➶](../help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects)" +!!! alert "If you're just getting started with OpenCV-Python programming, then refer this [FAQ ➶](help/general_faqs/#im-new-to-python-programming-or-its-usage-in-opencv-library-how-to-use-vidgear-in-my-projects)" diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index a8f65a8c2..00d6d1c4b 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -32,7 +32,7 @@ Switching OpenCV with VidGear APIs is fairly painless process, and will just req !!! warning "Prior knowledge of Python or OpenCV won't be covered in this guide. Proficiency with OpenCV-Python _(Python API for OpenCV)_ is a must in order understand this document." -!!! tip "If you're just getting started with OpenCV-Python, then see [here ➶](../help/general_faqs/#im-new-to-python-programming-or-its-usage-in-computer-vision-how-to-use-vidgear-in-my-projects)" +!!! tip "If you're just getting started with OpenCV-Python programming, then refer this [FAQ ➶](../help/general_faqs/#im-new-to-python-programming-or-its-usage-in-opencv-library-how-to-use-vidgear-in-my-projects)"   From d174329c3d2805a02a2cd4b9c54d250e21d886e7 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 2 Aug 2021 22:28:10 +0530 Subject: [PATCH 078/112] :zap: StreamGear: Implemented `-hls_base_url` FFMpeg parameter support. --- vidgear/gears/streamgear.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 6a84b566e..e762bbf41 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -791,8 +791,9 @@ def __generate_hls_stream(self, input_params, output_params): # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-allowed_extensions"] = "ALL" + output_params["-hls_base_url"] = self.__params.pop("-hls_base_url", "") output_params["-hls_segment_filename"] = "{}-stream%v-%03d.{}".format( - os.path.join(*[os.path.dirname(self.__out_file), "chunk"]), + os.path.join(os.path.dirname(self.__out_file), "chunk"), "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", ) output_params["-hls_allow_cache"] = 0 @@ -915,6 +916,7 @@ def __Build_n_Execute(self, input_params, output_params): ) # format outputs ffmpeg_cmd.extend([self.__out_file] if not (hls_commands) else hls_commands) + print(ffmpeg_cmd) # Launch the FFmpeg pipeline with built command logger.critical("Transcoding streaming chunks. Please wait...") # log it self.__process = sp.Popen( From db7b89d6d697fa7b42ed7c355de00f9052687db0 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 2 Aug 2021 22:38:06 +0530 Subject: [PATCH 079/112] :pencil2: StreamGear: Minor typos fixed --- vidgear/gears/streamgear.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index e762bbf41..67f88a477 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -789,9 +789,11 @@ def __generate_hls_stream(self, input_params, output_params): 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", "") + # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-allowed_extensions"] = "ALL" - output_params["-hls_base_url"] = self.__params.pop("-hls_base_url", "") output_params["-hls_segment_filename"] = "{}-stream%v-%03d.{}".format( os.path.join(os.path.dirname(self.__out_file), "chunk"), "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", @@ -916,7 +918,6 @@ def __Build_n_Execute(self, input_params, output_params): ) # format outputs ffmpeg_cmd.extend([self.__out_file] if not (hls_commands) else hls_commands) - print(ffmpeg_cmd) # Launch the FFmpeg pipeline with built command logger.critical("Transcoding streaming chunks. Please wait...") # log it self.__process = sp.Popen( From 2aec45b70214a734f19c754240eb19206bdfc045 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 07:26:05 +0530 Subject: [PATCH 080/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Updated=20CI=20tes?= =?UTF-8?q?ts=20for=20new=20HLS=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 👷 Updated CI tests from scratch for new native HLS support in StreamGear. - ✅ Added support for `hls` format in existing CI tests. - ✅ Added new functions `check_valid_m3u8` and `extract_meta_video` for validating HLS files. - ➕ Added new `m3u8` dependency to CI workflows. - 💚 Fixed several bugs related to CI tests. - 🐛 StreamGear: Fixed bugs related to external audio not mapped correctly in HLS format. - 🔨 BASH: Added new `temp_m3u8` folder for generating M3U8 assets in CI tests. - Docs: - 💡 Updated code comments. - 📝 Updated docs. --- .github/workflows/ci_linux.yml | 2 +- appveyor.yml | 2 +- azure-pipelines.yml | 2 +- docs/contribution/PR.md | 2 +- scripts/bash/prepare_dataset.sh | 1 + vidgear/gears/streamgear.py | 15 +- vidgear/tests/streamer_tests/test_IO_rtf.py | 10 +- vidgear/tests/streamer_tests/test_IO_ss.py | 35 +- vidgear/tests/streamer_tests/test_init.py | 16 +- .../streamer_tests/test_streamgear_modes.py | 550 +++++++++++++----- 10 files changed, 471 insertions(+), 164 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index d0052d72d..1cee57188 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -51,7 +51,7 @@ jobs: pip install -U pip wheel numpy pip install -U .[asyncio] pip uninstall opencv-python -y - pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash paramiko async-asgi-testclient + pip install -U flake8 six codecov pytest pytest-asyncio pytest-cov youtube-dl mpegdash paramiko m3u8 async-asgi-testclient if: success() - name: run prepare_dataset_script run: bash scripts/bash/prepare_dataset.sh diff --git a/appveyor.yml b/appveyor.yml index 78f7c6ccd..41f935a41 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,7 +51,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - "python --version" - "python -m pip install --upgrade pip wheel" - - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko async-asgi-testclient" + - "python -m pip install --upgrade .[asyncio] six codecov pytest pytest-cov pytest-asyncio youtube-dl aiortc paramiko m3u8 async-asgi-testclient" - "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 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8f904c42e..38ed8ee69 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -55,7 +55,7 @@ steps: - script: | python -m pip install --upgrade pip wheel - pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash paramiko async-asgi-testclient + pip install --upgrade .[asyncio] six codecov youtube-dl mpegdash paramiko m3u8 async-asgi-testclient pip install --upgrade pytest pytest-asyncio pytest-cov pytest-azurepipelines displayName: 'Install pip dependencies' diff --git a/docs/contribution/PR.md b/docs/contribution/PR.md index 8fa7dfa88..2061eee78 100644 --- a/docs/contribution/PR.md +++ b/docs/contribution/PR.md @@ -196,7 +196,7 @@ Testing VidGear requires additional test dependencies and dataset, which can be ``` ```sh - pip install --upgrade six flake8 black pytest pytest-asyncio mpegdash paramiko async-asgi-testclient + pip install --upgrade six flake8 black pytest pytest-asyncio mpegdash paramiko m3u8 async-asgi-testclient ``` - [x] **Download Tests Dataset:** diff --git a/scripts/bash/prepare_dataset.sh b/scripts/bash/prepare_dataset.sh index 7b3a31934..65ab34724 100644 --- a/scripts/bash/prepare_dataset.sh +++ b/scripts/bash/prepare_dataset.sh @@ -19,6 +19,7 @@ TMPFOLDER=$(python -c 'import tempfile; print(tempfile.gettempdir())') # Creating necessary directories mkdir -p "$TMPFOLDER"/temp_mpd # MPD assets temp path +mkdir -p "$TMPFOLDER"/temp_m3u8 # M3U8 assets temp path mkdir -p "$TMPFOLDER"/temp_write # For testing WriteGear Assets. mkdir -p "$TMPFOLDER"/temp_ffmpeg # For downloading FFmpeg Static Binary Assets. mkdir -p "$TMPFOLDER"/Downloads diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 67f88a477..a686c574c 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -449,7 +449,9 @@ def __PreProcess(self, channels=0, rgb=False): # assign audio codec output_parameters["-acodec"] = self.__params.pop("-acodec", "copy") output_parameters["a_bitrate"] = bitrate # temporary handler - output_parameters["-core_audio"] = ["-map", "a:0"] + output_parameters["-core_audio"] = ( + ["-map", "1:a:0"] if self.__format == "dash" else [] + ) else: logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) @@ -549,7 +551,10 @@ def __handle_streams(self, input_params, output_params): else: output_params["-corev0"] = ["-map", "0:v"] if "-acodec" in output_params: - output_params["-corea0"] = ["-map", "0:a"] + output_params["-corea0"] = [ + "-map", + "{}:a".format(1 if "-core_audio" in output_params else 0), + ] # assign resolution if "-s:v:0" in self.__params: # prevent duplicates @@ -652,7 +657,7 @@ def __evaluate_streams(self, streams, output_params, bpp): if "-acodec" in output_params: intermediate_dict["-corea{}".format(stream_count)] = [ "-map", - "0:a", + "{}:a".format(1 if "-core_audio" in output_params else 0), ] # extract resolution & indivisual dimension of stream @@ -869,7 +874,9 @@ def __Build_n_Execute(self, input_params, output_params): if self.__format == "hls" and stream_count > 0: stream_map = "" for count in range(0, stream_count): - stream_map += "v:{},a:{} ".format(count, count) + stream_map += "v:{}{} ".format( + count, ",a:{}".format(count) if "-acodec" in output_params else "," + ) hls_commands += [ "-master_pl_name", os.path.basename(self.__out_file), diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index df19ee1b2..ee0c3913d 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -83,7 +83,8 @@ def test_method_call_rtf(): @pytest.mark.xfail(raises=ValueError) -def test_invalid_params_rtf(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_invalid_params_rtf(format): """ Invalid parameter Failure Test - Made to fail by calling invalid parameters """ @@ -93,7 +94,12 @@ def test_invalid_params_rtf(): input_data = random_data.astype(np.uint8) stream_params = {"-vcodec": "unknown"} - streamer = StreamGear(output="output.mpd", logging=True, **stream_params) + streamer = StreamGear( + output="output{}".format(".mpd" if format == "dash" else ".m3u8"), + format=format, + logging=True, + **stream_params + ) streamer.stream(input_data) streamer.stream(input_data) streamer.terminate() diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index f2ac47769..bc6c482ce 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -38,16 +38,16 @@ def return_testvideo_path(): return os.path.abspath(path) -def test_failedextension(): +@pytest.mark.xfail(raises=(AssertionError, ValueError)) +@pytest.mark.parametrize("output", ["garbage.garbage", "output.m3u8"]) +def test_failedextension(output): """ IO Test - made to fail with filename with wrong extension """ - # 'garbage' extension does not exist - with pytest.raises(AssertionError): - stream_params = {"-video_source": return_testvideo_path()} - streamer = StreamGear(output="garbage.garbage", logging=True, **stream_params) - streamer.transcode_source() - streamer.terminate() + stream_params = {"-video_source": return_testvideo_path()} + streamer = StreamGear(output=output, logging=True, **stream_params) + streamer.transcode_source() + streamer.terminate() def test_failedextensionsource(): @@ -63,20 +63,21 @@ def test_failedextensionsource(): @pytest.mark.parametrize( - "path", + "path, format", [ - "rtmp://live.twitch.tv/output.mpd", - "unknown://invalid.com/output.mpd", + ("rtmp://live.twitch.tv/output.mpd", "dash"), + ("rtmp://live.twitch.tv/output.m3u8", "hls"), + ("unknown://invalid.com/output.mpd", "dash"), ], ) -def test_paths_ss(path): +def test_paths_ss(path, format): """ Paths Test - Test various paths/urls supported by StreamGear. """ streamer = None try: stream_params = {"-video_source": return_testvideo_path()} - streamer = StreamGear(output=path, logging=True, **stream_params) + streamer = StreamGear(output=path, format=format, logging=True, **stream_params) except Exception as e: if isinstance(e, ValueError): pytest.xfail("Test Passed!") @@ -110,11 +111,17 @@ def test_method_call_ss(): @pytest.mark.xfail(raises=subprocess.CalledProcessError) -def test_invalid_params_ss(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_invalid_params_ss(format): """ Method calling Test - Made to fail by calling method in the wrong context. """ stream_params = {"-video_source": return_testvideo_path(), "-vcodec": "unknown"} - streamer = StreamGear(output="output.mpd", logging=True, **stream_params) + streamer = StreamGear( + output="output{}".format(".mpd" if format == "dash" else ".m3u8"), + format=format, + logging=True, + **stream_params + ) streamer.transcode_source() streamer.terminate() diff --git a/vidgear/tests/streamer_tests/test_init.py b/vidgear/tests/streamer_tests/test_init.py index 029d61339..e01504c64 100644 --- a/vidgear/tests/streamer_tests/test_init.py +++ b/vidgear/tests/streamer_tests/test_init.py @@ -66,8 +66,8 @@ def test_custom_ffmpeg(c_ffmpeg): streamer.terminate() -@pytest.mark.xfail(raises=ValueError) -@pytest.mark.parametrize("format", ["mash", "unknown", 1234, None]) +@pytest.mark.xfail(raises=(AssertionError, ValueError)) +@pytest.mark.parametrize("format", ["hls", "mash", 1234, None]) def test_formats(format): """ Testing different formats for StreamGear @@ -77,7 +77,8 @@ def test_formats(format): @pytest.mark.parametrize( - "output", [None, "output.mpd", os.path.join(expanduser("~"), "test_mpd")] + "output", + [None, "output.mpd", "output.m3u8"], ) def test_outputs(output): """ @@ -89,10 +90,15 @@ def test_outputs(output): else {"-clear_prev_assets": "invalid"} ) try: - streamer = StreamGear(output=output, logging=True, **stream_params) + streamer = StreamGear( + output=output, + format="hls" if output == "output.m3u8" else "dash", + logging=True, + **stream_params + ) streamer.terminate() except Exception as e: - if output is None: + if output is None or output.endswith("m3u8"): pytest.xfail(str(e)) else: pytest.fail(str(e)) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index b402f967b..f2254c5f9 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -23,6 +23,7 @@ import cv2 import queue import pytest +import m3u8 import logging as log import platform import tempfile @@ -30,7 +31,7 @@ from mpegdash.parser import MPEGDASHParser from vidgear.gears import CamGear, StreamGear -from vidgear.gears.helper import logger_handler +from vidgear.gears.helper import logger_handler, validate_video # define test logger logger = log.getLogger("Test_Streamgear") @@ -58,6 +59,26 @@ def return_testvideo_path(fmt="av"): return os.path.abspath(path) +def return_static_ffmpeg(): + """ + returns system specific FFmpeg static path + """ + path = "" + if platform.system() == "Windows": + path += os.path.join( + tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg.exe" + ) + elif platform.system() == "Darwin": + path += os.path.join( + tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/bin/ffmpeg" + ) + else: + path += os.path.join( + tempfile.gettempdir(), "Downloads/FFmpeg_static/ffmpeg/ffmpeg" + ) + return os.path.abspath(path) + + def check_valid_mpd(file="", exp_reps=1): """ checks if given file is a valid MPD(MPEG-DASH Manifest file) @@ -79,6 +100,37 @@ def check_valid_mpd(file="", exp_reps=1): return (all_adapts, all_reprs) if (len(all_reprs) >= exp_reps) else False +def check_valid_m3u8(file=""): + """ + checks if given file is a valid M3U8 file + """ + if not file or not os.path.isfile(file): + return False + metas = [] + try: + playlist = m3u8.load(file) + if playlist.is_variant: + for pl in playlist.playlists: + meta = {} + meta["resolution"] = pl.stream_info.resolution + meta["framerate"] = pl.stream_info.frame_rate + metas.append(meta) + else: + for seg in playlist.segments: + metas.append(extract_meta_video(seg)) + except Exception as e: + logger.error(str(e)) + return False + return metas + + +def extract_meta_video(file): + """ + Extracts metadata from a valid video file + """ + return validate_video(return_static_ffmpeg(), file) + + def extract_meta_mpd(file): """ Extracts metadata from a valid MPD(MPEG-DASH Manifest file) @@ -107,11 +159,11 @@ def extract_meta_mpd(file): return [] -def return_mpd_path(): +def return_assets_path(hls=False): """ - returns MPD assets temp path + returns assets temp path """ - return os.path.join(tempfile.gettempdir(), "temp_mpd") + return os.path.abspath("temp_m3u8" if hls else "temp_mpd") def string_to_float(value): @@ -134,10 +186,8 @@ def extract_resolutions(source, streams): return {} results = {} assert os.path.isfile(source), "Not a valid source" - s_cv = cv2.VideoCapture(source) - results[int(s_cv.get(cv2.CAP_PROP_FRAME_WIDTH))] = int( - s_cv.get(cv2.CAP_PROP_FRAME_HEIGHT) - ) + results["source"] = extract_meta_video(source) + num = 0 for stream in streams: if "-resolution" in stream: try: @@ -145,7 +195,8 @@ def extract_resolutions(source, streams): assert len(res) == 2 width, height = (res[0].strip(), res[1].strip()) assert width.isnumeric() and height.isnumeric() - results[int(width)] = int(height) + results["streams{}".format(num)] = {"resolution": (width, height)} + num += 1 except Exception as e: logger.error(str(e)) continue @@ -154,48 +205,67 @@ def extract_resolutions(source, streams): return results -def test_ss_stream(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_ss_stream(format): """ Testing Single-Source Mode """ - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), + ) try: stream_params = { "-video_source": return_testvideo_path(), "-clear_prev_assets": True, } - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) streamer.transcode_source() streamer.terminate() - assert check_valid_mpd(mpd_file_path) + 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)) -def test_ss_livestream(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_ss_livestream(format): """ Testing Single-Source Mode with livestream. """ - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), + ) try: stream_params = { "-video_source": return_testvideo_path(), "-livestream": True, "-remove_at_exit": 1, } - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) streamer.transcode_source() streamer.terminate() except Exception as e: pytest.fail(str(e)) -@pytest.mark.parametrize("conversion", [None, "COLOR_BGR2GRAY", "COLOR_BGR2BGRA"]) -def test_rtf_stream(conversion): +@pytest.mark.parametrize( + "conversion, format", + [(None, "dash"), ("COLOR_BGR2GRAY", "hls"), ("COLOR_BGR2BGRA", "dash")], +) +def test_rtf_stream(conversion, format): """ Testing Real-Time Frames Mode """ - mpd_file_path = return_mpd_path() + assets_file_path = return_assets_path(False if format == "dash" else True) + try: # Open stream options = {"THREAD_TIMEOUT": 300} @@ -206,7 +276,7 @@ def test_rtf_stream(conversion): "-clear_prev_assets": True, "-input_framerate": "invalid", } - streamer = StreamGear(output=mpd_file_path, **stream_params) + streamer = StreamGear(output=assets_file_path, format=format, **stream_params) while True: frame = stream.read() # check if frame is None @@ -218,23 +288,28 @@ def test_rtf_stream(conversion): streamer.stream(frame) stream.stop() streamer.terminate() - mpd_file = [ - os.path.join(mpd_file_path, f) - for f in os.listdir(mpd_file_path) - if f.endswith(".mpd") + asset_file = [ + os.path.join(assets_file_path, f) + for f in os.listdir(assets_file_path) + if f.endswith(".mpd" if format == "dash" else ".m3u8") ] - assert len(mpd_file) == 1, "Failed to create MPD file!" - assert check_valid_mpd(mpd_file[0]) + assert len(asset_file) == 1, "Failed to create asset file!" + if format == "dash": + assert check_valid_mpd(asset_file[0]), "Test Failed!" + else: + assert extract_meta_video(asset_file[0]), "Test Failed!" except Exception as e: if not isinstance(e, queue.Empty): pytest.fail(str(e)) -def test_rtf_livestream(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_rtf_livestream(format): """ Testing Real-Time Frames Mode with livestream. """ - mpd_file_path = return_mpd_path() + assets_file_path = return_assets_path(False if format == "dash" else True) + try: # Open stream options = {"THREAD_TIMEOUT": 300} @@ -242,7 +317,7 @@ def test_rtf_livestream(): stream_params = { "-livestream": True, } - streamer = StreamGear(output=mpd_file_path, **stream_params) + streamer = StreamGear(output=assets_file_path, format=format, **stream_params) while True: frame = stream.read() # check if frame is None @@ -256,19 +331,25 @@ def test_rtf_livestream(): pytest.fail(str(e)) -def test_input_framerate_rtf(): +@pytest.mark.parametrize("format", ["dash", "hls"]) +def test_input_framerate_rtf(format): """ Testing "-input_framerate" parameter provided by StreamGear """ try: - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), + ) stream = cv2.VideoCapture(return_testvideo_path()) # Open stream test_framerate = stream.get(cv2.CAP_PROP_FPS) stream_params = { "-clear_prev_assets": True, "-input_framerate": test_framerate, } - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) while True: (grabbed, frame) = stream.read() if not grabbed: @@ -276,37 +357,84 @@ def test_input_framerate_rtf(): streamer.stream(frame) stream.release() streamer.terminate() - meta_data = extract_meta_mpd(mpd_file_path) - assert meta_data and len(meta_data) > 0, "Test Failed!" - framerate_mpd = string_to_float(meta_data[0]["framerate"]) - assert framerate_mpd > 0.0 and isinstance(framerate_mpd, float), "Test Failed!" - assert round(framerate_mpd) == round(test_framerate), "Test Failed!" + if format == "dash": + meta_data = extract_meta_mpd(assets_file_path) + assert meta_data and len(meta_data) > 0, "Test Failed!" + framerate_mpd = string_to_float(meta_data[0]["framerate"]) + assert framerate_mpd > 0.0 and isinstance( + framerate_mpd, float + ), "Test Failed!" + assert round(framerate_mpd) == round(test_framerate), "Test Failed!" + else: + meta_data = extract_meta_video(assets_file_path) + assert meta_data and "framerate" in meta_data, "Test Failed!" + framerate_m3u8 = float(meta_data["framerate"]) + assert framerate_m3u8 > 0.0 and isinstance( + framerate_m3u8, float + ), "Test Failed!" + assert round(framerate_m3u8) == round(test_framerate), "Test Failed!" except Exception as e: pytest.fail(str(e)) @pytest.mark.parametrize( - "stream_params", + "stream_params, format", [ - {"-clear_prev_assets": True, "-bpp": 0.2000, "-gop": 125, "-vcodec": "libx265"}, - { - "-clear_prev_assets": True, - "-bpp": "unknown", - "-gop": "unknown", - "-s:v:0": "unknown", - "-b:v:0": "unknown", - "-b:a:0": "unknown", - }, + ( + { + "-clear_prev_assets": True, + "-bpp": 0.2000, + "-gop": 125, + "-vcodec": "libx265", + }, + "hls", + ), + ( + { + "-clear_prev_assets": True, + "-bpp": "unknown", + "-gop": "unknown", + "-s:v:0": "unknown", + "-b:v:0": "unknown", + "-b:a:0": "unknown", + }, + "hls", + ), + ( + { + "-clear_prev_assets": True, + "-bpp": 0.2000, + "-gop": 125, + "-vcodec": "libx265", + }, + "dash", + ), + ( + { + "-clear_prev_assets": True, + "-bpp": "unknown", + "-gop": "unknown", + "-s:v:0": "unknown", + "-b:v:0": "unknown", + "-b:a:0": "unknown", + }, + "dash", + ), ], ) -def test_params(stream_params): +def test_params(stream_params, format): """ - Testing "-input_framerate" parameter provided by StreamGear + Testing "-stream_params" parameters by StreamGear """ try: - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), + ) stream = cv2.VideoCapture(return_testvideo_path()) # Open stream - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) while True: (grabbed, frame) = stream.read() if not grabbed: @@ -314,119 +442,271 @@ def test_params(stream_params): streamer.stream(frame) stream.release() streamer.terminate() - assert check_valid_mpd(mpd_file_path) + 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)) @pytest.mark.parametrize( - "stream_params", + "stream_params, format", [ - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(fmt="vo"), - "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/invalid.aac", - }, - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(fmt="vo"), - "-audio": return_testvideo_path(fmt="ao"), - }, - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(fmt="vo"), - "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/big_buck_bunny_720p_1mb_ao.aac", - }, + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/invalid.aac", + }, + "dash", + ), + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": return_testvideo_path(fmt="ao"), + }, + "dash", + ), + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/big_buck_bunny_720p_1mb_ao.aac", + }, + "dash", + ), + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/invalid.aac", + }, + "hls", + ), + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": return_testvideo_path(fmt="ao"), + }, + "hls", + ), + ( + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": "https://raw.githubusercontent.com/abhiTronix/Imbakup/master/Images/big_buck_bunny_720p_1mb_ao.aac", + }, + "hls", + ), ], ) -def test_audio(stream_params): +def test_audio(stream_params, format): """ - Testing Single-Source Mode + Testing external and audio audio for stream. """ - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), + ) try: - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) streamer.transcode_source() streamer.terminate() - assert check_valid_mpd(mpd_file_path) + 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)) @pytest.mark.parametrize( - "stream_params", + "format, stream_params", [ - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(fmt="vo"), - "-streams": [ - { - "-video_bitrate": "unknown", - }, # Invalid Stream 1 - { - "-resolution": "unxun", - }, # Invalid Stream 2 - { - "-resolution": "640x480", - "-video_bitrate": "unknown", - }, # Invalid Stream 3 - { - "-resolution": "640x480", - "-framerate": "unknown", - }, # Invalid Stream 4 - { - "-resolution": "320x240", - "-framerate": 20.0, - }, # Stream: 320x240 at 20fps framerate - ], - }, - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(fmt="vo"), - "-audio": return_testvideo_path(fmt="ao"), - "-streams": [ - { - "-resolution": "640x480", - "-video_bitrate": "850k", - "-audio_bitrate": "128k", - }, # Stream1: 640x480 at 850kbps bitrate - { - "-resolution": "320x240", - "-framerate": 20.0, - }, # Stream2: 320x240 at 20fps framerate - ], - }, - { - "-clear_prev_assets": True, - "-video_source": return_testvideo_path(), - "-streams": [ - { - "-resolution": "960x540", - "-video_bitrate": "1350k", - }, # Stream1: 960x540 at 1350kbps bitrate - ], - }, + ( + "dash", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-streams": [ + { + "-video_bitrate": "unknown", + }, # Invalid Stream 1 + { + "-resolution": "unxun", + }, # Invalid Stream 2 + { + "-resolution": "640x480", + "-video_bitrate": "unknown", + }, # Invalid Stream 3 + { + "-resolution": "640x480", + "-framerate": "unknown", + }, # Invalid Stream 4 + { + "-resolution": "320x240", + "-framerate": 20.0, + }, # Stream: 320x240 at 20fps framerate + ], + }, + ), + ( + "hls", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-streams": [ + { + "-video_bitrate": "unknown", + }, # Invalid Stream 1 + { + "-resolution": "unxun", + }, # Invalid Stream 2 + { + "-resolution": "640x480", + "-video_bitrate": "unknown", + }, # Invalid Stream 3 + { + "-resolution": "640x480", + "-framerate": "unknown", + }, # Invalid Stream 4 + { + "-resolution": "320x240", + "-framerate": 20.0, + }, # Stream: 320x240 at 20fps framerate + ], + }, + ), + ( + "dash", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": return_testvideo_path(fmt="ao"), + "-streams": [ + { + "-resolution": "640x480", + "-video_bitrate": "850k", + "-audio_bitrate": "128k", + }, # Stream1: 640x480 at 850kbps bitrate + { + "-resolution": "320x240", + "-framerate": 20.0, + }, # Stream2: 320x240 at 20fps framerate + ], + }, + ), + ( + "hls", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(fmt="vo"), + "-audio": return_testvideo_path(fmt="ao"), + "-streams": [ + { + "-resolution": "640x480", + "-video_bitrate": "850k", + "-audio_bitrate": "128k", + }, # Stream1: 640x480 at 850kbps bitrate + { + "-resolution": "320x240", + "-framerate": 20.0, + }, # Stream2: 320x240 at 20fps framerate + ], + }, + ), + ( + "dash", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(), + "-streams": [ + { + "-resolution": "960x540", + "-video_bitrate": "1350k", + }, # Stream1: 960x540 at 1350kbps bitrate + ], + }, + ), + ( + "hls", + { + "-clear_prev_assets": True, + "-video_source": return_testvideo_path(), + "-streams": [ + { + "-resolution": "960x540", + "-video_bitrate": "1350k", + }, # Stream1: 960x540 at 1350kbps bitrate + ], + }, + ), ], ) -def test_multistreams(stream_params): +def test_multistreams(format, stream_params): """ Testing Support for additional Secondary Streams of variable bitrates or spatial resolutions. """ - mpd_file_path = os.path.join(return_mpd_path(), "dash_test.mpd") + assets_file_path = os.path.join( + return_assets_path(False if format == "dash" else True), + "asset_test.{}".format("mpd" if format == "dash" else "m3u8"), + ) results = extract_resolutions( stream_params["-video_source"], stream_params["-streams"] ) try: - streamer = StreamGear(output=mpd_file_path, logging=True, **stream_params) + streamer = StreamGear( + output=assets_file_path, format=format, logging=True, **stream_params + ) streamer.transcode_source() streamer.terminate() - metadata = extract_meta_mpd(mpd_file_path) - meta_videos = [x for x in metadata if x["mime_type"].startswith("video")] - assert meta_videos and (len(meta_videos) <= len(results)), "Test Failed!" - for s_v in meta_videos: - assert int(s_v["width"]) in results, "Width check failed!" - assert ( - int(s_v["height"]) == results[int(s_v["width"])] - ), "Height check failed!" + if format == "dash": + metadata = extract_meta_mpd(assets_file_path) + meta_videos = [x for x in metadata if x["mime_type"].startswith("video")] + assert meta_videos and (len(meta_videos) <= len(results)), "Test Failed!" + if len(meta_videos) == len(results): + for m_v, s_v in zip(meta_videos, list(results.values())): + assert int(m_v["width"]) == int( + s_v["resolution"][0] + ), "Width check failed!" + assert int(m_v["height"]) == int( + s_v["resolution"][1] + ), "Height check failed!" + else: + valid_widths = [int(x["resolution"][0]) for x in list(results.values())] + valid_heights = [ + int(x["resolution"][1]) for x in list(results.values()) + ] + for m_v in meta_videos: + assert int(m_v["width"]) in valid_widths, "Width check failed!" + assert int(m_v["height"]) in valid_heights, "Height check failed!" + else: + meta_videos = check_valid_m3u8(assets_file_path) + assert meta_videos and (len(meta_videos) <= len(results)), "Test Failed!" + if len(meta_videos) == len(results): + for m_v, s_v in zip(meta_videos, list(results.values())): + assert int(m_v["resolution"][0]) == int( + s_v["resolution"][0] + ), "Width check failed!" + assert int(m_v["resolution"][1]) == int( + s_v["resolution"][1] + ), "Height check failed!" + else: + valid_widths = [int(x["resolution"][0]) for x in list(results.values())] + valid_heights = [ + int(x["resolution"][1]) for x in list(results.values()) + ] + for m_v in meta_videos: + assert ( + int(m_v["resolution"][0]) in valid_widths + ), "Width check failed!" + assert ( + int(m_v["resolution"][1]) in valid_heights + ), "Height check failed!" except Exception as e: pytest.fail(str(e)) From aeab3b3198615ce7200ed2a2d018801188d4f972 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 08:06:40 +0530 Subject: [PATCH 081/112] =?UTF-8?q?=F0=9F=90=9B=20CI:=20Fixed=20`return=5F?= =?UTF-8?q?assets=5Fpath`=20path=20bug.?= 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 f2254c5f9..e94717a4b 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -163,7 +163,7 @@ def return_assets_path(hls=False): """ returns assets temp path """ - return os.path.abspath("temp_m3u8" if hls else "temp_mpd") + return os.path.join(tempfile.gettempdir(), "temp_m3u8" if hls else "temp_mpd") def string_to_float(value): From 9a55f222f4205fa14bd748ec31486580dfbe1384 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 09:15:53 +0530 Subject: [PATCH 082/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Updated=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../streamer_tests/test_streamgear_modes.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index e94717a4b..d91263bf7 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -22,6 +22,7 @@ import os import cv2 import queue +import time import pytest import m3u8 import logging as log @@ -100,6 +101,14 @@ def check_valid_mpd(file="", exp_reps=1): return (all_adapts, all_reprs) if (len(all_reprs) >= exp_reps) else False +def extract_meta_video(file): + """ + Extracts metadata from a valid video file + """ + logger.debug("Extracting Metadata from {}".format(file)) + return validate_video(return_static_ffmpeg(), file) + + def check_valid_m3u8(file=""): """ checks if given file is a valid M3U8 file @@ -124,13 +133,6 @@ def check_valid_m3u8(file=""): return metas -def extract_meta_video(file): - """ - Extracts metadata from a valid video file - """ - return validate_video(return_static_ffmpeg(), file) - - def extract_meta_mpd(file): """ Extracts metadata from a valid MPD(MPEG-DASH Manifest file) @@ -227,7 +229,7 @@ def test_ss_stream(format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert extract_meta_video(assets_file_path), "Test Failed!" + assert check_valid_m3u8(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) @@ -297,7 +299,7 @@ def test_rtf_stream(conversion, format): if format == "dash": assert check_valid_mpd(asset_file[0]), "Test Failed!" else: - assert extract_meta_video(asset_file[0]), "Test Failed!" + assert check_valid_m3u8(asset_file[0]), "Test Failed!" except Exception as e: if not isinstance(e, queue.Empty): pytest.fail(str(e)) @@ -445,7 +447,7 @@ def test_params(stream_params, format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert extract_meta_video(assets_file_path), "Test Failed!" + assert check_valid_m3u8(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) @@ -520,7 +522,7 @@ def test_audio(stream_params, format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert extract_meta_video(assets_file_path), "Test Failed!" + assert check_valid_m3u8(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) From 4f1b167bf84ec14d7fc39d70eef8d876ab2d2cae Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 10:02:17 +0530 Subject: [PATCH 083/112] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20CI:=20Reverted=20c?= =?UTF-8?q?hanges.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/streamer_tests/test_streamgear_modes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index d91263bf7..fabebb9ff 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -106,6 +106,9 @@ def extract_meta_video(file): Extracts metadata from a valid video file """ logger.debug("Extracting Metadata from {}".format(file)) + if not os.path.isfile(file): + logger.warning("Unable to find file!") + time.sleep(1) return validate_video(return_static_ffmpeg(), file) @@ -229,7 +232,7 @@ def test_ss_stream(format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert check_valid_m3u8(assets_file_path), "Test Failed!" + assert extract_meta_video(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) @@ -299,7 +302,7 @@ def test_rtf_stream(conversion, format): if format == "dash": assert check_valid_mpd(asset_file[0]), "Test Failed!" else: - assert check_valid_m3u8(asset_file[0]), "Test Failed!" + assert extract_meta_video(asset_file[0]), "Test Failed!" except Exception as e: if not isinstance(e, queue.Empty): pytest.fail(str(e)) @@ -447,7 +450,7 @@ def test_params(stream_params, format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert check_valid_m3u8(assets_file_path), "Test Failed!" + assert extract_meta_video(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) @@ -522,7 +525,7 @@ def test_audio(stream_params, format): if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: - assert check_valid_m3u8(assets_file_path), "Test Failed!" + assert extract_meta_video(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) From 98bf59aaa5329b309fd6970c307a0af554e263df Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 11:18:39 +0530 Subject: [PATCH 084/112] =?UTF-8?q?=F0=9F=94=8A=20CI:=20Enabled=20addition?= =?UTF-8?q?al=20logging.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../streamer_tests/test_streamgear_modes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index fabebb9ff..eb82c5a93 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -106,10 +106,19 @@ def extract_meta_video(file): Extracts metadata from a valid video file """ logger.debug("Extracting Metadata from {}".format(file)) - if not os.path.isfile(file): - logger.warning("Unable to find file!") - time.sleep(1) - return validate_video(return_static_ffmpeg(), file) + meta = validate_video(return_static_ffmpeg(), file) + if meta is None: + # debug content + logger.error("Unable to extract metadata!") + with open(file, "r") as f_in: + lines = list(line for line in (l.strip() for l in f_in) if line) + logger.debug(lines) + dir_name = os.path.dirname(file) + listfiles = [ + f for f in os.listdir(dir_name) if os.path.isfile(os.path.join(dir_name, f)) + ] + logger.debug(listfiles) + return meta def check_valid_m3u8(file=""): From d9471bbb0c9f5b123dca4779bcf75d9e17ad662e Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 6 Aug 2021 11:45:21 +0530 Subject: [PATCH 085/112] =?UTF-8?q?=F0=9F=94=8A=20CI:=20Enabled=20logging?= =?UTF-8?q?=20in=20`validate=5Fvideo`=20method.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/helper.py | 4 +++- .../tests/streamer_tests/test_streamgear_modes.py | 13 +------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 6c2d9a108..c9eaf4a21 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -374,7 +374,7 @@ def is_valid_url(path, url=None, logging=False): return False -def validate_video(path, video_path=None): +def validate_video(path, video_path=None, logging=False): """ ### validate_video @@ -396,6 +396,8 @@ def validate_video(path, video_path=None): ) # clean and search stripped_data = [x.decode("utf-8").strip() for x in metadata.split(b"\n")] + if logging: + logger.debug(stripped_data) result = {} for data in stripped_data: output_a = re.findall(r"([1-9]\d+)x([1-9]\d+)", data) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index eb82c5a93..b336249e6 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -106,18 +106,7 @@ def extract_meta_video(file): Extracts metadata from a valid video file """ logger.debug("Extracting Metadata from {}".format(file)) - meta = validate_video(return_static_ffmpeg(), file) - if meta is None: - # debug content - logger.error("Unable to extract metadata!") - with open(file, "r") as f_in: - lines = list(line for line in (l.strip() for l in f_in) if line) - logger.debug(lines) - dir_name = os.path.dirname(file) - listfiles = [ - f for f in os.listdir(dir_name) if os.path.isfile(os.path.join(dir_name, f)) - ] - logger.debug(listfiles) + meta = validate_video(return_static_ffmpeg(), file, logging=True) return meta From 3c6def546075690bb308a2f07c46b5826044b4bc Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 7 Aug 2021 10:40:12 +0530 Subject: [PATCH 086/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Added=20`-hls=5Fba?= =?UTF-8?q?se=5Furl`=20to=20streamgear=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../streamer_tests/test_streamgear_modes.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index b336249e6..2514a2d49 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -222,6 +222,15 @@ def test_ss_stream(format): "-video_source": return_testvideo_path(), "-clear_prev_assets": True, } + if format == "hls": + stream_params.update( + { + "-hls_base_url": return_assets_path( + False if format == "dash" else True + ) + + os.sep + } + ) streamer = StreamGear( output=assets_file_path, format=format, logging=True, **stream_params ) @@ -279,6 +288,15 @@ def test_rtf_stream(conversion, format): "-clear_prev_assets": True, "-input_framerate": "invalid", } + if format == "hls": + stream_params.update( + { + "-hls_base_url": return_assets_path( + False if format == "dash" else True + ) + + os.sep + } + ) streamer = StreamGear(output=assets_file_path, format=format, **stream_params) while True: frame = stream.read() @@ -434,6 +452,15 @@ def test_params(stream_params, format): return_assets_path(False if format == "dash" else True), "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), ) + if format == "hls": + stream_params.update( + { + "-hls_base_url": return_assets_path( + False if format == "dash" else True + ) + + os.sep + } + ) stream = cv2.VideoCapture(return_testvideo_path()) # Open stream streamer = StreamGear( output=assets_file_path, format=format, logging=True, **stream_params @@ -515,6 +542,15 @@ def test_audio(stream_params, format): "format_test{}".format(".mpd" if format == "dash" else ".m3u8"), ) try: + if format == "hls": + stream_params.update( + { + "-hls_base_url": return_assets_path( + False if format == "dash" else True + ) + + os.sep + } + ) streamer = StreamGear( output=assets_file_path, format=format, logging=True, **stream_params ) From 7ba60eded77649aa3bcab5afd8748585a4382c90 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sat, 7 Aug 2021 13:43:07 +0530 Subject: [PATCH 087/112] =?UTF-8?q?=F0=9F=91=B7=20CI:=20Updated=20test=20p?= =?UTF-8?q?atch.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/tests/streamer_tests/test_streamgear_modes.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index 2514a2d49..188709199 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -368,6 +368,15 @@ def test_input_framerate_rtf(format): "-clear_prev_assets": True, "-input_framerate": test_framerate, } + if format == "hls": + stream_params.update( + { + "-hls_base_url": return_assets_path( + False if format == "dash" else True + ) + + os.sep + } + ) streamer = StreamGear( output=assets_file_path, format=format, logging=True, **stream_params ) From 6de976331dc7e69138d32504c242ba874684ea33 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 11 Aug 2021 09:09:21 +0530 Subject: [PATCH 088/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20StreamGear:=20Adde?= =?UTF-8?q?d=20audio=20input=20from=20external=20device=20(Fixes=20#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✨ Implemented support for audio input from external device. - 🚸 Users can now easily added audio device and decoder by formatting them as python list. - 🎨 Modified `-audio` parameter to support `list` data type as value. - 🎨 Modified `validate_audio` helper function to validate external audio device. - 🐛 Fixed stream not terminating with input from external audio device. - 🐛 Fixed unsupported high audio high bit-rate bug. - 📝 Minor docs fixes. --- README.md | 2 +- docs/gears/videogear/overview.md | 2 +- vidgear/gears/helper.py | 21 +++++++++++++-------- vidgear/gears/streamgear.py | 28 +++++++++++++++++++++++----- vidgear/tests/test_helper.py | 2 +- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index aba01019e..1dbfe016e 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ The following **functional block diagram** clearly depicts the generalized funct -# TL;DR +## TL;DR #### What is vidgear? diff --git a/docs/gears/videogear/overview.md b/docs/gears/videogear/overview.md index 76336449c..6e99ef0b9 100644 --- a/docs/gears/videogear/overview.md +++ b/docs/gears/videogear/overview.md @@ -37,7 +37,7 @@ VideoGear is ideal when you need to switch to different video sources without ch !!! tip "Helpful Tips" - * If you're already familar with [OpenCV](https://github.com/opencv/opencv) library, then see [Switching from OpenCV ➶](../../switch_from_cv/#switching-videocapture-apis) + * If you're already familar with [OpenCV](https://github.com/opencv/opencv) library, then see [Switching from OpenCV ➶](../../../switch_from_cv/#switching-videocapture-apis) * It is advised to enable logging(`logging = True`) on the first run for easily identifying any runtime errors. diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index c9eaf4a21..7dd532f90 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -481,7 +481,7 @@ def extract_time(value): ) -def validate_audio(path, file_path=None): +def validate_audio(path, source=None): """ ### validate_audio @@ -489,23 +489,28 @@ def validate_audio(path, file_path=None): Parameters: path (string): absolute path of FFmpeg binaries - file_path (string): absolute path to file to be validated. + source (string/list): source to be validated. **Returns:** A string value, confirming whether audio is present, or not?. """ - if file_path is None or not (file_path): - logger.warning("File path is empty!") + if source is None or not (source): + logger.warning("Audio input source is empty!") return "" - # extract audio sample-rate from metadata - metadata = check_output( - [path, "-hide_banner", "-i", file_path], force_retrieve_stderr=True + # create ffmpeg command + cmd = [path, "-hide_banner"] + ( + source if isinstance(source, list) else ["-i", source] ) + # extract audio sample-rate from metadata + metadata = check_output(cmd, force_retrieve_stderr=True) audio_bitrate = re.findall(r"fltp,\s[0-9]+\s\w\w[/]s", metadata.decode("utf-8")) + sample_rate_identifiers = ["Audio", "Hz"] + ( + ["fltp"] if isinstance(source, str) else [] + ) audio_sample_rate = [ line.strip() for line in metadata.decode("utf-8").split("\n") - if all(x in line for x in ["Audio", "Hz", "fltp"]) + if all(x in line for x in sample_rate_identifiers) ] if audio_bitrate: filtered = audio_bitrate[0].split(" ")[1:3] diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index a686c574c..0214b6938 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -144,6 +144,8 @@ def __init__( self.__audio = audio else: self.__audio = "" + elif audio and isinstance(audio, list): + self.__audio = audio else: self.__audio = "" @@ -439,15 +441,23 @@ def __PreProcess(self, channels=0, rgb=False): # enable audio (if present) if self.__audio: # validate audio source - bitrate = validate_audio(self.__ffmpeg, file_path=self.__audio) + bitrate = validate_audio(self.__ffmpeg, source=self.__audio) if bitrate: logger.info( "Detected External Audio Source is valid, and will be used for streams." ) - # assign audio - output_parameters["-i"] = self.__audio + + # 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", "copy") + output_parameters["-acodec"] = self.__params.pop( + "-acodec", "aac" if isinstance(self.__audio, list) else "copy" + ) output_parameters["a_bitrate"] = bitrate # temporary handler output_parameters["-core_audio"] = ( ["-map", "1:a:0"] if self.__format == "dash" else [] @@ -458,7 +468,7 @@ def __PreProcess(self, channels=0, rgb=False): ) elif self.__video_source: # validate audio source - bitrate = validate_audio(self.__ffmpeg, file_path=self.__video_source) + bitrate = validate_audio(self.__ffmpeg, source=self.__video_source) if bitrate: logger.info("Source Audio will be used for streams.") # assign audio codec @@ -854,6 +864,10 @@ def __Build_n_Execute(self, input_params, output_params): input_params (dict): Input FFmpeg parameters 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) @@ -925,6 +939,7 @@ def __Build_n_Execute(self, input_params, output_params): ) # format outputs ffmpeg_cmd.extend([self.__out_file] if not (hls_commands) else hls_commands) + logger.debug(ffmpeg_cmd) # Launch the FFmpeg pipeline with built command logger.critical("Transcoding streaming chunks. Please wait...") # log it self.__process = sp.Popen( @@ -1001,6 +1016,9 @@ def terminate(self): # 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.wait() self.__process = None diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 0a1d8e512..3b13c4a77 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -406,7 +406,7 @@ def test_validate_audio(path, result): Testing validate_audio function """ try: - results = validate_audio(return_static_ffmpeg(), file_path=path) + results = validate_audio(return_static_ffmpeg(), source=path) if result: assert results, "Audio path validity test Failed!" except Exception as e: From d30c9ab7cd63dfaa5b4d45832f30e4839d29d80f Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 11 Aug 2021 16:20:15 +0530 Subject: [PATCH 089/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20exampl?= =?UTF-8?q?es.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Added example for audio input support from external device in StreamGear. - 📝 Added steps for using `-audio` attribute on different OS platforms in StreamGear. - 📝 Added steps for identifying and specifying sound card on different OS platforms in WriteGear. - ✏️ Updated context and fixed typos. --- README.md | 6 +- docs/contribution/issue.md | 2 +- docs/gears.md | 4 +- docs/gears/streamgear/introduction.md | 2 + docs/gears/streamgear/rtfm/usage.md | 195 +++++++++++++++++++++- docs/gears/writegear/compression/usage.md | 192 +++++++++++++++------ docs/gears/writegear/introduction.md | 4 +- docs/index.md | 2 + mkdocs.yml | 3 +- 9 files changed, 347 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 1dbfe016e..2906ae2b2 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,9 @@ stream.stop() > *WriteGear handles various powerful Video-Writer Tools that provide us the freedom to do almost anything imaginable with multimedia data.* -WriteGear API provides a complete, flexible, and robust wrapper around [**FFmpeg**][ffmpeg], a leading multimedia framework. WriteGear can process real-time frames into a lossless compressed video-file with any suitable specification _(such as`bitrate, codec, framerate, resolution, subtitles, etc.`)_. It is powerful enough to perform complex tasks such as [Live-Streaming][live-stream] _(such as for Twitch and YouTube)_ and [Multiplexing Video-Audio][live-audio-doc] with real-time frames in way fewer lines of code. +WriteGear API provides a complete, flexible, and robust wrapper around [**FFmpeg**][ffmpeg], a leading multimedia framework. WriteGear can process real-time frames into a lossless compressed video-file with any suitable specifications _(such as`bitrate, codec, framerate, resolution, subtitles, etc.`)_. + +WriteGear also supports streaming with traditional protocols such as RTMP, RTSP/RTP. It is powerful enough to perform complex tasks such as [Live-Streaming][live-stream] _(such as for Twitch, YouTube etc.)_ and [Multiplexing Video-Audio][live-audio-doc] with real-time frames in just few lines of code. Best of all, WriteGear grants users the complete freedom to play with any FFmpeg parameter with its exclusive **Custom Commands function** _(see this [doc][custom-command-doc])_ without relying on any third-party API. @@ -442,7 +444,7 @@ SteamGear easily transcodes source videos/audio files & real-time video-frames a SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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 only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming will be added soon. +SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming will be added soon. **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** diff --git a/docs/contribution/issue.md b/docs/contribution/issue.md index c953beddd..f7ecefef9 100644 --- a/docs/contribution/issue.md +++ b/docs/contribution/issue.md @@ -43,7 +43,7 @@ If you've found a new bug or you've come up with some new feature which can impr ### Follow the Issue Template -* Please format your issue by choosing the appropriate templates. +* Please format your issue by choosing the appropriate template. * Any improper/insufficient reports will be marked with **MISSING : INFORMATION :mag:** and **MISSING : TEMPLATE :grey_question:** like labels, and if we don't hear back from you we may close the issue. ### Raise the Issue diff --git a/docs/gears.md b/docs/gears.md index 40c3a0e38..3e55c2cb7 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -22,7 +22,7 @@ limitations under the License.
@Vidgear Functional Block Diagram -
Gears: generalized workflow diagram
+
Gears: generalized workflow
## Gears, What are these? @@ -54,6 +54,8 @@ These Gears can be classified as follows: > **Basic Function:** Transcodes/Broadcasts files & [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames for streaming. +!!! tip "You can also use [WriteGear](writegear/introduction/) for streaming with traditional protocols such as RTMP, RTSP/RTP." + * [StreamGear](streamgear/introduction/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. * **Asynchronous I/O Streaming Gear:** diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 947a6343b..af7e2c5fc 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -39,6 +39,8 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ besides s SteamGear currently only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. +!!! tip "For streaming with traditional protocols such as RTMP, RTSP/RTP you can use [WriteGear](../../writegear/introduction/) API instead." +   !!! danger "Important" diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index a83d22a87..305e97da2 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -396,9 +396,9 @@ streamer.terminate()   -## Usage with Audio-Input +## 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 need to input the path of your audio to this attribute as string value, and StreamGear API will automatically validate and map it to all generated streams. The complete example is as follows: +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 need to input the path of your audio file to this attribute as `string` value, and StreamGear API will automatically validate and map it 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 encounter multiple errors or no output at all." @@ -466,6 +466,197 @@ streamer.terminate()   +## 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 need to format your audio device name and suitable decoder as `list` and assign to this attribute, and StreamGear API will automatically validate and map it to all generated streams. The complete example is as follows: + + +!!! alert "Example Assumptions" + + * You're running are Windows machine. + * You already have appropriate audio drivers and software installed on your machine. + + +??? tip "Using `-audio` attribute on different OS platforms" + + === "On Windows" + + Windows OS users can use the [dshow](https://trac.ffmpeg.org/wiki/DirectShow) (DirectShow) to list audio input device which is the preferred option for Windows users. You can refer following steps to identify and specify your sound card: + + - [x] **[OPTIONAL] Enable sound card(if disabled):** First enable your Stereo Mix by opening the "Sound" window and select the "Recording" tab, then right click on the window and select "Show Disabled Devices" to toggle the Stereo Mix device visibility. **Follow this [post ➶](https://forums.tomshardware.com/threads/no-sound-through-stereo-mix-realtek-hd-audio.1716182/) for more details.** + + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `dshow` as follows: + + ```sh + c:\> ffmpeg -list_devices true -f dshow -i dummy + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [dshow @ 03ACF580] DirectShow video devices + [dshow @ 03ACF580] "Integrated Camera" + [dshow @ 03ACF580] "USB2.0 Camera" + [dshow @ 03ACF580] DirectShow audio devices + [dshow @ 03ACF580] "Microphone (Realtek High Definition Audio)" + [dshow @ 03ACF580] "Microphone (USB2.0 Camera)" + dummy: Immediate exit requested + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + 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" + + + === "On Linux" + + Linux OS users can use the [alsa](https://ffmpeg.org/ffmpeg-all.html#alsa) to list input device to capture live audio input such as from a webcam. You can refer following steps to identify and specify your sound card: + + - [x] **Identify Sound Card:** To get the list of all installed cards on your machine, you can type `arecord -l` or `arecord -L` _(longer output)_. + + ```sh + arecord -l + + **** List of CAPTURE Hardware Devices **** + card 0: ICH5 [Intel ICH5], device 0: Intel ICH [Intel ICH5] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 1: Intel ICH - MIC ADC [Intel ICH5 - MIC ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 2: Intel ICH - MIC2 ADC [Intel ICH5 - MIC2 ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 3: Intel ICH - ADC2 [Intel ICH5 - ADC2] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 1: U0x46d0x809 [USB Device 0x46d:0x809], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in WriteGear as follows: + + !!! info "The easiest thing to do is to reference sound card directly, namely "card 0" (Intel ICH5) and "card 1" (Microphone on the USB web cam), as `hw:0` or `hw:1`" + + ```python + # assign appropriate input audio-source "card 1" (Microphone on the USB web cam) + 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" + + + === "On MacOS" + + MAC OS users can use the [avfoundation](https://ffmpeg.org/ffmpeg-devices.html#avfoundation) to list input devices for grabbing audio from integrated iSight cameras as well as cameras connected via USB or FireWire. You can refer following steps to identify and specify your sound card on MacOS/OSX machines: + + + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `avfoundation` as follows: + + ```sh + ffmpeg -f qtkit -list_devices true -i "" + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation video devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] FaceTime HD camera (built-in) + [AVFoundation input device @ 0x7f8e2540ef20] [1] Capture screen 0 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation audio devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] Blackmagic Audio + [AVFoundation input device @ 0x7f8e2540ef20] [1] Built-in Microphone + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + 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" + + +!!! danger "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you 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." + + +```python +# 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() + +# add various streams, along with custom audio +stream_params = { + "-streams": [ + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps + ], + "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + stream_params = {"-audio": ["-f","dshow", "-i", "audio=Microphone (USB2.0 Camera)"]} # assign appropriate input audio-source +} + +# describe a suitable manifest-file location/name and assign params +streamer = StreamGear(output="dash_out.mpd", **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() +``` + +  + ## Usage with Hardware Video-Encoder diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 6abc3c622..5fe3aeac6 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -427,70 +427,154 @@ writer.close() ## Using Compression Mode with Live Audio Input -In Compression Mode, WriteGear API allows us to exploit almost all FFmpeg supported parameters that you can think of, in its Compression Mode. Hence, processing, encoding, and combining audio with video is pretty much straightforward. +In Compression Mode, WriteGear API allows us to exploit almost all FFmpeg supported parameters that you can think of in its Compression Mode. Hence, combining audio with live video frames is pretty easy. !!! alert "Example Assumptions" * You're running are Linux machine. - * You already have appropriate audio & video drivers and softwares installed on your machine. + * You already have appropriate audio driver and software installed on your machine. -!!! danger "Locate your Sound Card" - Remember to locate your Sound Card before running this example: +??? tip "Identifying and Specifying sound card on different OS platforms" + + === "On Windows" - * Note down the Sound Card value using `arecord -L` command on the your Linux terminal. - * It may be similar to this `plughw:CARD=CAMERA,DEV=0` + Windows OS users can use the [dshow](https://trac.ffmpeg.org/wiki/DirectShow) (DirectShow) to list audio input device which is the preferred option for Windows users. You can refer following steps to identify and specify your sound card: -??? tip "Tip for Windows" + - [x] **[OPTIONAL] Enable sound card(if disabled):** First enable your Stereo Mix by opening the "Sound" window and select the "Recording" tab, then right click on the window and select "Show Disabled Devices" to toggle the Stereo Mix device visibility. **Follow this [post ➶](https://forums.tomshardware.com/threads/no-sound-through-stereo-mix-realtek-hd-audio.1716182/) for more details.** - - [x] **Enable sound card(if disabled):** First enable your Stereo Mix by opening the "Sound" window and select the "Recording" tab, then right click on the window and select "Show Disabled Devices" to toggle the Stereo Mix device visibility. **Follow this [post ➶](https://forums.tomshardware.com/threads/no-sound-through-stereo-mix-realtek-hd-audio.1716182/) for more details.** + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `dshow` as follows: + + ```sh + c:\> ffmpeg -list_devices true -f dshow -i dummy + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [dshow @ 03ACF580] DirectShow video devices + [dshow @ 03ACF580] "Integrated Camera" + [dshow @ 03ACF580] "USB2.0 Camera" + [dshow @ 03ACF580] DirectShow audio devices + [dshow @ 03ACF580] "Microphone (Realtek High Definition Audio)" + [dshow @ 03ACF580] "Microphone (USB2.0 Camera)" + dummy: Immediate exit requested + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + output_params = { + "-i":"audio=Microphone (USB2.0 Camera)", + "-thread_queue_size": "512", + "-f": "dshow", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! 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" + + + === "On Linux" + + Linux OS users can use the [alsa](https://ffmpeg.org/ffmpeg-all.html#alsa) to list input device to capture live audio input such as from a webcam. You can refer following steps to identify and specify your sound card: + + - [x] **Identify Sound Card:** To get the list of all installed cards on your machine, you can type `arecord -l` or `arecord -L` _(longer output)_. + + ```sh + arecord -l + + **** List of CAPTURE Hardware Devices **** + card 0: ICH5 [Intel ICH5], device 0: Intel ICH [Intel ICH5] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 1: Intel ICH - MIC ADC [Intel ICH5 - MIC ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 2: Intel ICH - MIC2 ADC [Intel ICH5 - MIC2 ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 3: Intel ICH - ADC2 [Intel ICH5 - ADC2] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 1: U0x46d0x809 [USB Device 0x46d:0x809], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in WriteGear as follows: + + !!! info "The easiest thing to do is to reference sound card directly, namely "card 0" (Intel ICH5) and "card 1" (Microphone on the USB web cam), as `hw:0` or `hw:1`" + + ```python + # assign appropriate input audio-source + output_params = { + "-i": "hw:1", + "-thread_queue_size": "512", + "-f": "alsa", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + + + === "On MacOS" + + MAC OS users can use the [avfoundation](https://ffmpeg.org/ffmpeg-devices.html#avfoundation) to list input devices for grabbing audio from integrated iSight cameras as well as cameras connected via USB or FireWire. You can refer following steps to identify and specify your sound card on MacOS/OSX machines: + + + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `avfoundation` as follows: + + ```sh + ffmpeg -f qtkit -list_devices true -i "" + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation video devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] FaceTime HD camera (built-in) + [AVFoundation input device @ 0x7f8e2540ef20] [1] Capture screen 0 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation audio devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] Blackmagic Audio + [AVFoundation input device @ 0x7f8e2540ef20] [1] Built-in Microphone + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + output_params = { + "-audio_device_index": "0", + "-thread_queue_size": "512", + "-f": "avfoundation", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! fail "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 encounter multiple errors or no output at all." - - [x] **Locate Sound Card:** Then, You can locate your soundcard on windows using ffmpeg's [`directshow`](https://trac.ffmpeg.org/wiki/DirectShow) backend: - - ```sh - ffmpeg -list_devices true -f dshow -i dummy - ``` - - which will result in something similar output as following: - - ```sh - c:\> ffmpeg -list_devices true -f dshow -i dummy - ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect - libavutil 51. 74.100 / 51. 74.100 - libavcodec 54. 65.100 / 54. 65.100 - libavformat 54. 31.100 / 54. 31.100 - libavdevice 54. 3.100 / 54. 3.100 - libavfilter 3. 19.102 / 3. 19.102 - libswscale 2. 1.101 / 2. 1.101 - libswresample 0. 16.100 / 0. 16.100 - [dshow @ 03ACF580] DirectShow video devices - [dshow @ 03ACF580] "Integrated Camera" - [dshow @ 03ACF580] "screen-capture-recorder" - [dshow @ 03ACF580] DirectShow audio devices - [dshow @ 03ACF580] "Microphone (Realtek High Definition Audio)" - [dshow @ 03ACF580] "virtual-audio-capturer" - dummy: Immediate exit requested - ``` - - - - [x] **Specify Sound Card:** Then, you can specify your located soundcard in WriteGear as follows: - - ```python - # change with your webcam soundcard, plus add additional required FFmpeg parameters for your writer - output_params = { - "-i": "audio=Microphone (Realtek High Definition Audio)", - "-thread_queue_size": "512", - "-f": "dshow", - "-ac": "2", - "-acodec": "aac", - "-ar": "44100", - } - - # Define writer with defined parameters and suitable output filename for e.g. `Output.mp4 - writer = WriteGear(output_filename="Output.mp4", logging=True, **output_params) - ``` - - !!! 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" +!!! 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." In this example code, we will merge the audio from a Audio Source _(for e.g. Webcam inbuilt mic)_ to the frames of a Video Source _(for e.g external webcam)_, and save this data as a compressed video file, all in real time: diff --git a/docs/gears/writegear/introduction.md b/docs/gears/writegear/introduction.md index dbe1d5a13..97faec76a 100644 --- a/docs/gears/writegear/introduction.md +++ b/docs/gears/writegear/introduction.md @@ -29,7 +29,9 @@ limitations under the License. > *WriteGear handles various powerful Video-Writer Tools that provide us the freedom to do almost anything imaginable with multimedia data.* -WriteGear API provides a complete, flexible, and robust wrapper around [**FFmpeg**](https://ffmpeg.org/), a leading multimedia framework. WriteGear can process real-time frames into a lossless compressed video-file with any suitable specification _(such as`bitrate, codec, framerate, resolution, subtitles, etc.`)_. It is powerful enough to perform complex tasks such as [Live-Streaming](../compression/usage/#using-compression-mode-for-streaming-urls) _(such as for Twitch)_ and [Multiplexing Video-Audio](../compression/usage/#using-compression-mode-with-live-audio-input) with real-time frames in way fewer lines of code. +WriteGear API provides a complete, flexible, and robust wrapper around [**FFmpeg**](https://ffmpeg.org/), a leading multimedia framework. WriteGear can process real-time frames into a lossless compressed video-file with any suitable specifications _(such as`bitrate, codec, framerate, resolution, subtitles, etc.`)_. + +WriteGear also supports streaming with traditional protocols such as RTMP, RTSP/RTP. It is powerful enough to perform complex tasks such as [Live-Streaming](../compression/usage/#using-compression-mode-for-streaming-urls) _(such as for Twitch, YouTube etc.)_ and [Multiplexing Video-Audio](../compression/usage/#using-compression-mode-with-live-audio-input) with real-time frames in just few lines of code. Best of all, WriteGear grants users the complete freedom to play with any FFmpeg parameter with its exclusive ==Custom Commands function== _(see this [doc](../compression/advanced/cciw/))_ without relying on any third-party API. diff --git a/docs/index.md b/docs/index.md index 37d8f2a9a..4fac88d81 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,6 +75,8 @@ These Gears can be classified as follows: #### Streaming Gears +!!! tip "You can also use [WriteGear](gears/writegear/introduction/) for streaming with traditional protocols such as RTMP, RTSP/RTP." + * [StreamGear](gears/streamgear/introduction/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. * **Asynchronous I/O Streaming Gear:** diff --git a/mkdocs.yml b/mkdocs.yml index ed671148a..8b7404134 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -98,8 +98,7 @@ extra: provider: mike analytics: # Google analytics provider: google - property: UA-131929464-1 - + property: UA-131929464-1 extra_css: - assets/stylesheets/custom.css From a091d004f7fa308ccde19ef1ea38dd532e6166c0 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 11 Aug 2021 16:46:39 +0530 Subject: [PATCH 090/112] =?UTF-8?q?=F0=9F=93=84=20License:=20Dropped=20pub?= =?UTF-8?q?lication=20year=20range=20to=20avoid=20confusion.=20(Signed=20a?= =?UTF-8?q?nd=20Approved=20by=20@abhiTronix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📄 Updated Vidgear license's year of first publication of the work in accordance with US copyright notices defined by Title 17, Chapter 4(Visually perceptible copies): https://www.copyright.gov/title17/92chap4.html (Fixes #238) - 💥 Reflected changes in all copyright notices. --- .github/workflows/ci_linux.yml | 2 +- .github/workflows/deploy_docs.yml | 2 +- LICENSE | 2 +- README.md | 3 ++- appveyor.yml | 2 +- azure-pipelines.yml | 2 +- codecov.yml | 2 +- contributing.md | 2 +- docs/bonus/TQM.md | 2 +- docs/bonus/colorspace_manipulation.md | 2 +- docs/bonus/reference/camgear.md | 2 +- docs/bonus/reference/helper.md | 2 +- docs/bonus/reference/helper_async.md | 2 +- docs/bonus/reference/netgear.md | 2 +- docs/bonus/reference/netgear_async.md | 2 +- docs/bonus/reference/pigear.md | 2 +- docs/bonus/reference/screengear.md | 2 +- docs/bonus/reference/stabilizer.md | 2 +- docs/bonus/reference/streamgear.md | 2 +- docs/bonus/reference/videogear.md | 2 +- docs/bonus/reference/webgear.md | 2 +- docs/bonus/reference/webgear_rtc.md | 2 +- docs/bonus/reference/writegear.md | 2 +- docs/changelog.md | 2 +- docs/contribution.md | 2 +- docs/contribution/PR.md | 2 +- docs/contribution/issue.md | 2 +- docs/gears.md | 2 +- docs/gears/camgear/advanced/source_params.md | 2 +- docs/gears/camgear/overview.md | 2 +- docs/gears/camgear/params.md | 2 +- docs/gears/camgear/usage.md | 2 +- docs/gears/netgear/advanced/bidirectional_mode.md | 2 +- docs/gears/netgear/advanced/compression.md | 2 +- docs/gears/netgear/advanced/multi_client.md | 2 +- docs/gears/netgear/advanced/multi_server.md | 2 +- docs/gears/netgear/advanced/secure_mode.md | 2 +- docs/gears/netgear/advanced/ssh_tunnel.md | 2 +- docs/gears/netgear/overview.md | 2 +- docs/gears/netgear/params.md | 2 +- docs/gears/netgear/usage.md | 2 +- docs/gears/netgear_async/overview.md | 2 +- docs/gears/netgear_async/params.md | 2 +- docs/gears/netgear_async/usage.md | 2 +- docs/gears/pigear/overview.md | 2 +- docs/gears/pigear/params.md | 2 +- docs/gears/pigear/usage.md | 2 +- docs/gears/screengear/overview.md | 2 +- docs/gears/screengear/params.md | 2 +- docs/gears/screengear/usage.md | 2 +- docs/gears/stabilizer/overview.md | 2 +- docs/gears/stabilizer/params.md | 2 +- docs/gears/stabilizer/usage.md | 2 +- docs/gears/streamgear/ffmpeg_install.md | 2 +- docs/gears/streamgear/introduction.md | 2 +- docs/gears/streamgear/params.md | 2 +- docs/gears/streamgear/rtfm/overview.md | 2 +- docs/gears/streamgear/rtfm/usage.md | 2 +- docs/gears/streamgear/ssm/overview.md | 2 +- docs/gears/streamgear/ssm/usage.md | 2 +- docs/gears/videogear/overview.md | 2 +- docs/gears/videogear/params.md | 2 +- docs/gears/videogear/usage.md | 2 +- docs/gears/webgear/advanced.md | 2 +- docs/gears/webgear/overview.md | 2 +- docs/gears/webgear/params.md | 2 +- docs/gears/webgear/usage.md | 2 +- docs/gears/webgear_rtc/advanced.md | 2 +- docs/gears/webgear_rtc/overview.md | 2 +- docs/gears/webgear_rtc/params.md | 2 +- docs/gears/webgear_rtc/usage.md | 2 +- docs/gears/writegear/compression/advanced/cciw.md | 2 +- docs/gears/writegear/compression/advanced/ffmpeg_install.md | 2 +- docs/gears/writegear/compression/overview.md | 2 +- docs/gears/writegear/compression/params.md | 2 +- docs/gears/writegear/compression/usage.md | 2 +- docs/gears/writegear/introduction.md | 2 +- docs/gears/writegear/non_compression/overview.md | 2 +- docs/gears/writegear/non_compression/params.md | 2 +- docs/gears/writegear/non_compression/usage.md | 2 +- docs/help.md | 2 +- docs/help/camgear_faqs.md | 2 +- docs/help/general_faqs.md | 2 +- docs/help/get_help.md | 2 +- docs/help/netgear_async_faqs.md | 2 +- docs/help/netgear_faqs.md | 2 +- docs/help/pigear_faqs.md | 2 +- docs/help/screengear_faqs.md | 2 +- docs/help/stabilizer_faqs.md | 2 +- docs/help/streamgear_faqs.md | 2 +- docs/help/videogear_faqs.md | 2 +- docs/help/webgear_faqs.md | 2 +- docs/help/webgear_rtc_faqs.md | 2 +- docs/help/writegear_faqs.md | 2 +- docs/index.md | 2 +- docs/installation.md | 2 +- docs/installation/pip_install.md | 2 +- docs/installation/source_install.md | 2 +- docs/license.md | 4 ++-- docs/overrides/404.html | 2 +- docs/overrides/assets/javascripts/extra.js | 2 +- docs/overrides/assets/stylesheets/custom.css | 2 +- docs/overrides/main.html | 2 +- docs/switch_from_cv.md | 2 +- mkdocs.yml | 2 +- pytest.ini | 2 +- scripts/bash/install_opencv.sh | 2 +- scripts/bash/prepare_dataset.sh | 2 +- setup.py | 2 +- vidgear/gears/asyncio/__main__.py | 2 +- vidgear/gears/asyncio/helper.py | 2 +- vidgear/gears/asyncio/netgear_async.py | 2 +- vidgear/gears/asyncio/webgear.py | 2 +- vidgear/gears/asyncio/webgear_rtc.py | 2 +- vidgear/gears/camgear.py | 2 +- vidgear/gears/helper.py | 2 +- vidgear/gears/netgear.py | 2 +- vidgear/gears/pigear.py | 2 +- vidgear/gears/screengear.py | 2 +- vidgear/gears/stabilizer.py | 2 +- vidgear/gears/videogear.py | 2 +- vidgear/gears/writegear.py | 2 +- vidgear/tests/network_tests/asyncio_tests/test_helper.py | 2 +- .../tests/network_tests/asyncio_tests/test_netgear_async.py | 2 +- vidgear/tests/network_tests/test_netgear.py | 2 +- vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py | 2 +- .../tests/streamer_tests/asyncio_tests/test_webgear_rtc.py | 2 +- vidgear/tests/streamer_tests/test_IO_rtf.py | 2 +- vidgear/tests/streamer_tests/test_IO_ss.py | 2 +- vidgear/tests/streamer_tests/test_init.py | 2 +- vidgear/tests/streamer_tests/test_streamgear_modes.py | 2 +- vidgear/tests/test_helper.py | 2 +- vidgear/tests/utils/fake_picamera/picamera.py | 2 +- vidgear/tests/utils/fps.py | 2 +- vidgear/tests/videocapture_tests/test_camgear.py | 2 +- vidgear/tests/videocapture_tests/test_pigear.py | 2 +- vidgear/tests/videocapture_tests/test_screengear.py | 2 +- vidgear/tests/videocapture_tests/test_videogear.py | 2 +- vidgear/tests/writer_tests/test_IO.py | 2 +- vidgear/tests/writer_tests/test_compression_mode.py | 2 +- vidgear/tests/writer_tests/test_non_compression_mode.py | 2 +- 141 files changed, 143 insertions(+), 142 deletions(-) diff --git a/.github/workflows/ci_linux.yml b/.github/workflows/ci_linux.yml index 1cee57188..dd9dd851f 100644 --- a/.github/workflows/ci_linux.yml +++ b/.github/workflows/ci_linux.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/.github/workflows/deploy_docs.yml b/.github/workflows/deploy_docs.yml index 799047213..cabaeebae 100644 --- a/.github/workflows/deploy_docs.yml +++ b/.github/workflows/deploy_docs.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/LICENSE b/LICENSE index b37f76f9b..e36f93110 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) + 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. diff --git a/README.md b/README.md index 2906ae2b2..388dbeb04 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. @@ -436,6 +436,7 @@ In addition to this, WriteGear also provides flexible access to [**OpenCV's Vide

+ > *StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH) in just few lines of python code.* StreamGear provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**][ffmpeg] multimedia framework for generating chunked-encoded media segments of the content. diff --git a/appveyor.yml b/appveyor.yml index 41f935a41..45a4c9d3c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 38ed8ee69..9f7c50f50 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/codecov.yml b/codecov.yml index 306c6c4cc..cf2312c1d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/contributing.md b/contributing.md index 08b5a7aff..24215443b 100644 --- a/contributing.md +++ b/contributing.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/TQM.md b/docs/bonus/TQM.md index 0351c5a36..b6e5ca362 100644 --- a/docs/bonus/TQM.md +++ b/docs/bonus/TQM.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/colorspace_manipulation.md b/docs/bonus/colorspace_manipulation.md index 5e4dd73f9..c55b7c41a 100644 --- a/docs/bonus/colorspace_manipulation.md +++ b/docs/bonus/colorspace_manipulation.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/camgear.md b/docs/bonus/reference/camgear.md index ceb18bbb3..83f0b6b73 100644 --- a/docs/bonus/reference/camgear.md +++ b/docs/bonus/reference/camgear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/helper.md b/docs/bonus/reference/helper.md index 1a7e8e214..a161db312 100644 --- a/docs/bonus/reference/helper.md +++ b/docs/bonus/reference/helper.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/helper_async.md b/docs/bonus/reference/helper_async.md index e28f99f3e..79fa7ab6b 100644 --- a/docs/bonus/reference/helper_async.md +++ b/docs/bonus/reference/helper_async.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/netgear.md b/docs/bonus/reference/netgear.md index 70c6e190c..d4a5bf6f3 100644 --- a/docs/bonus/reference/netgear.md +++ b/docs/bonus/reference/netgear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/netgear_async.md b/docs/bonus/reference/netgear_async.md index e94ee0ef4..c59c749af 100644 --- a/docs/bonus/reference/netgear_async.md +++ b/docs/bonus/reference/netgear_async.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/pigear.md b/docs/bonus/reference/pigear.md index 00c66485e..a42586b71 100644 --- a/docs/bonus/reference/pigear.md +++ b/docs/bonus/reference/pigear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/screengear.md b/docs/bonus/reference/screengear.md index 26c31d0c7..a72d21448 100644 --- a/docs/bonus/reference/screengear.md +++ b/docs/bonus/reference/screengear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/stabilizer.md b/docs/bonus/reference/stabilizer.md index fc1896cf9..d22edf0a2 100644 --- a/docs/bonus/reference/stabilizer.md +++ b/docs/bonus/reference/stabilizer.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/streamgear.md b/docs/bonus/reference/streamgear.md index 6ea9ceea3..d2d0820e0 100644 --- a/docs/bonus/reference/streamgear.md +++ b/docs/bonus/reference/streamgear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/videogear.md b/docs/bonus/reference/videogear.md index 5c64c1bee..8d493a04e 100644 --- a/docs/bonus/reference/videogear.md +++ b/docs/bonus/reference/videogear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/webgear.md b/docs/bonus/reference/webgear.md index c0c950ceb..0832e5948 100644 --- a/docs/bonus/reference/webgear.md +++ b/docs/bonus/reference/webgear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/webgear_rtc.md b/docs/bonus/reference/webgear_rtc.md index cc75cd117..8a2c69d92 100644 --- a/docs/bonus/reference/webgear_rtc.md +++ b/docs/bonus/reference/webgear_rtc.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/bonus/reference/writegear.md b/docs/bonus/reference/writegear.md index 72b944c5e..545a9329a 100644 --- a/docs/bonus/reference/writegear.md +++ b/docs/bonus/reference/writegear.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/changelog.md b/docs/changelog.md index b8d77c64b..b21d7db5d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/contribution.md b/docs/contribution.md index 88355fff2..19b50b66b 100644 --- a/docs/contribution.md +++ b/docs/contribution.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/contribution/PR.md b/docs/contribution/PR.md index 2061eee78..9606500ef 100644 --- a/docs/contribution/PR.md +++ b/docs/contribution/PR.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/contribution/issue.md b/docs/contribution/issue.md index f7ecefef9..18571fc53 100644 --- a/docs/contribution/issue.md +++ b/docs/contribution/issue.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears.md b/docs/gears.md index 3e55c2cb7..c9a934137 100644 --- a/docs/gears.md +++ b/docs/gears.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/camgear/advanced/source_params.md b/docs/gears/camgear/advanced/source_params.md index 380a4780d..01cb9287d 100644 --- a/docs/gears/camgear/advanced/source_params.md +++ b/docs/gears/camgear/advanced/source_params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/camgear/overview.md b/docs/gears/camgear/overview.md index 48d08e41b..1d01e6238 100644 --- a/docs/gears/camgear/overview.md +++ b/docs/gears/camgear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/camgear/params.md b/docs/gears/camgear/params.md index bc0047b76..54fe891ac 100644 --- a/docs/gears/camgear/params.md +++ b/docs/gears/camgear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index e15f54e07..f40d4f10f 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index 4ded72358..b7aef1a2a 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index f35a84081..d09bf2070 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/multi_client.md b/docs/gears/netgear/advanced/multi_client.md index 80d3c36bf..b8ab39be1 100644 --- a/docs/gears/netgear/advanced/multi_client.md +++ b/docs/gears/netgear/advanced/multi_client.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/multi_server.md b/docs/gears/netgear/advanced/multi_server.md index 4f6b3db22..8d6dc61c1 100644 --- a/docs/gears/netgear/advanced/multi_server.md +++ b/docs/gears/netgear/advanced/multi_server.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 055aa8a2d..1fee3c798 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/advanced/ssh_tunnel.md b/docs/gears/netgear/advanced/ssh_tunnel.md index 2f13be1b1..150d79fa8 100644 --- a/docs/gears/netgear/advanced/ssh_tunnel.md +++ b/docs/gears/netgear/advanced/ssh_tunnel.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/overview.md b/docs/gears/netgear/overview.md index e9527712f..6c24ce05b 100644 --- a/docs/gears/netgear/overview.md +++ b/docs/gears/netgear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/params.md b/docs/gears/netgear/params.md index 892e63dda..c9562e362 100644 --- a/docs/gears/netgear/params.md +++ b/docs/gears/netgear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear/usage.md b/docs/gears/netgear/usage.md index 357a49819..95d310bd0 100644 --- a/docs/gears/netgear/usage.md +++ b/docs/gears/netgear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear_async/overview.md b/docs/gears/netgear_async/overview.md index fd982bc6d..caf43f195 100644 --- a/docs/gears/netgear_async/overview.md +++ b/docs/gears/netgear_async/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear_async/params.md b/docs/gears/netgear_async/params.md index f727ae5ee..820a3ff37 100644 --- a/docs/gears/netgear_async/params.md +++ b/docs/gears/netgear_async/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index 38a1933f7..e55775814 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/pigear/overview.md b/docs/gears/pigear/overview.md index d5b93d461..2cba27deb 100644 --- a/docs/gears/pigear/overview.md +++ b/docs/gears/pigear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/pigear/params.md b/docs/gears/pigear/params.md index 9ce6e0f1f..b28e72fd1 100644 --- a/docs/gears/pigear/params.md +++ b/docs/gears/pigear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 7b44e3f4d..7b9827685 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/screengear/overview.md b/docs/gears/screengear/overview.md index 9b1dbec55..e6505cc79 100644 --- a/docs/gears/screengear/overview.md +++ b/docs/gears/screengear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/screengear/params.md b/docs/gears/screengear/params.md index 0c22d9ae6..bc782b063 100644 --- a/docs/gears/screengear/params.md +++ b/docs/gears/screengear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/screengear/usage.md b/docs/gears/screengear/usage.md index e959a12d5..c8ccfa8ec 100644 --- a/docs/gears/screengear/usage.md +++ b/docs/gears/screengear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/stabilizer/overview.md b/docs/gears/stabilizer/overview.md index 51c41d440..7d6af0d99 100644 --- a/docs/gears/stabilizer/overview.md +++ b/docs/gears/stabilizer/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/stabilizer/params.md b/docs/gears/stabilizer/params.md index 8d9560de9..f49b21519 100644 --- a/docs/gears/stabilizer/params.md +++ b/docs/gears/stabilizer/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/stabilizer/usage.md b/docs/gears/stabilizer/usage.md index 6931c1c32..65167fe37 100644 --- a/docs/gears/stabilizer/usage.md +++ b/docs/gears/stabilizer/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/ffmpeg_install.md b/docs/gears/streamgear/ffmpeg_install.md index d41ab7b49..51e049bf2 100644 --- a/docs/gears/streamgear/ffmpeg_install.md +++ b/docs/gears/streamgear/ffmpeg_install.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index af7e2c5fc..4a299919f 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 88da99d3c..8da1319e3 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index f9bbf81d6..ea2ac3936 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 305e97da2..38fcf5a4b 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 1a7519744..8c9246dd6 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 79cc9f7c0..4c3c35ea2 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/videogear/overview.md b/docs/gears/videogear/overview.md index 6e99ef0b9..b13be384a 100644 --- a/docs/gears/videogear/overview.md +++ b/docs/gears/videogear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/videogear/params.md b/docs/gears/videogear/params.md index 19cbc529e..2e70add7c 100644 --- a/docs/gears/videogear/params.md +++ b/docs/gears/videogear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index 7beb267a2..b02541e34 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index 6d8c2f49b..823539615 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear/overview.md b/docs/gears/webgear/overview.md index 67267453a..a08a07187 100644 --- a/docs/gears/webgear/overview.md +++ b/docs/gears/webgear/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear/params.md b/docs/gears/webgear/params.md index ef07bc65a..ac7049e16 100644 --- a/docs/gears/webgear/params.md +++ b/docs/gears/webgear/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear/usage.md b/docs/gears/webgear/usage.md index fedf47ccc..3d6a66983 100644 --- a/docs/gears/webgear/usage.md +++ b/docs/gears/webgear/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index a8dda61d7..99e094c61 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear_rtc/overview.md b/docs/gears/webgear_rtc/overview.md index a1541b8e9..df6d7cb39 100644 --- a/docs/gears/webgear_rtc/overview.md +++ b/docs/gears/webgear_rtc/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear_rtc/params.md b/docs/gears/webgear_rtc/params.md index 5c14da1e4..ba244f17b 100644 --- a/docs/gears/webgear_rtc/params.md +++ b/docs/gears/webgear_rtc/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/webgear_rtc/usage.md b/docs/gears/webgear_rtc/usage.md index 04c15950e..4ed7f3046 100644 --- a/docs/gears/webgear_rtc/usage.md +++ b/docs/gears/webgear_rtc/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/compression/advanced/cciw.md b/docs/gears/writegear/compression/advanced/cciw.md index 825155315..bdd9a008b 100644 --- a/docs/gears/writegear/compression/advanced/cciw.md +++ b/docs/gears/writegear/compression/advanced/cciw.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/compression/advanced/ffmpeg_install.md b/docs/gears/writegear/compression/advanced/ffmpeg_install.md index af7471b5a..5da9bf757 100644 --- a/docs/gears/writegear/compression/advanced/ffmpeg_install.md +++ b/docs/gears/writegear/compression/advanced/ffmpeg_install.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/compression/overview.md b/docs/gears/writegear/compression/overview.md index 800d726a6..b7972d359 100644 --- a/docs/gears/writegear/compression/overview.md +++ b/docs/gears/writegear/compression/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index b0f3b91d4..d1845b5fe 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 5fe3aeac6..5c528771c 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/introduction.md b/docs/gears/writegear/introduction.md index 97faec76a..5deb1010e 100644 --- a/docs/gears/writegear/introduction.md +++ b/docs/gears/writegear/introduction.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/non_compression/overview.md b/docs/gears/writegear/non_compression/overview.md index 7048da12f..553631d3e 100644 --- a/docs/gears/writegear/non_compression/overview.md +++ b/docs/gears/writegear/non_compression/overview.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/non_compression/params.md b/docs/gears/writegear/non_compression/params.md index f5c21a877..c8f38a17e 100644 --- a/docs/gears/writegear/non_compression/params.md +++ b/docs/gears/writegear/non_compression/params.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/gears/writegear/non_compression/usage.md b/docs/gears/writegear/non_compression/usage.md index f1f1e54d3..f26b7fdcf 100644 --- a/docs/gears/writegear/non_compression/usage.md +++ b/docs/gears/writegear/non_compression/usage.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help.md b/docs/help.md index d7914ea36..c43535723 100644 --- a/docs/help.md +++ b/docs/help.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index 7d136b8c3..a55cdc848 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/general_faqs.md b/docs/help/general_faqs.md index 0b70c68bf..dbf4dd344 100644 --- a/docs/help/general_faqs.md +++ b/docs/help/general_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/get_help.md b/docs/help/get_help.md index 48a8996c7..619e2d56a 100644 --- a/docs/help/get_help.md +++ b/docs/help/get_help.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/netgear_async_faqs.md b/docs/help/netgear_async_faqs.md index f68ecf407..289ca8d29 100644 --- a/docs/help/netgear_async_faqs.md +++ b/docs/help/netgear_async_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/netgear_faqs.md b/docs/help/netgear_faqs.md index 3367d69a0..f8aab71fe 100644 --- a/docs/help/netgear_faqs.md +++ b/docs/help/netgear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/pigear_faqs.md b/docs/help/pigear_faqs.md index 176e7a6d0..41725348e 100644 --- a/docs/help/pigear_faqs.md +++ b/docs/help/pigear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/screengear_faqs.md b/docs/help/screengear_faqs.md index 63118e583..7fb76d802 100644 --- a/docs/help/screengear_faqs.md +++ b/docs/help/screengear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/stabilizer_faqs.md b/docs/help/stabilizer_faqs.md index 3855575dc..7406c3795 100644 --- a/docs/help/stabilizer_faqs.md +++ b/docs/help/stabilizer_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/streamgear_faqs.md b/docs/help/streamgear_faqs.md index c622f4eef..955b15f76 100644 --- a/docs/help/streamgear_faqs.md +++ b/docs/help/streamgear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/videogear_faqs.md b/docs/help/videogear_faqs.md index 50be6a58f..67cdb89cb 100644 --- a/docs/help/videogear_faqs.md +++ b/docs/help/videogear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/webgear_faqs.md b/docs/help/webgear_faqs.md index 43e00d79b..ca7e1b42d 100644 --- a/docs/help/webgear_faqs.md +++ b/docs/help/webgear_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/webgear_rtc_faqs.md b/docs/help/webgear_rtc_faqs.md index 71be6bda9..ac468f546 100644 --- a/docs/help/webgear_rtc_faqs.md +++ b/docs/help/webgear_rtc_faqs.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/help/writegear_faqs.md b/docs/help/writegear_faqs.md index a2c3f7416..53fe2950c 100644 --- a/docs/help/writegear_faqs.md +++ b/docs/help/writegear_faqs.md @@ -3,7 +3,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/index.md b/docs/index.md index 4fac88d81..b2322bc49 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/installation.md b/docs/installation.md index 256ea7c3c..b77f53e44 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 88ef67db1..8d6af3436 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index 49a8598bf..f71a97e09 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/license.md b/docs/license.md index 6e972a8b2..b65ef570a 100644 --- a/docs/license.md +++ b/docs/license.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. @@ -24,7 +24,7 @@ This library is released under the **[Apache 2.0 License](https://github.com/abh ## Copyright Notice - Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) + 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. diff --git a/docs/overrides/404.html b/docs/overrides/404.html index 8b01ba204..ab2502822 100644 --- a/docs/overrides/404.html +++ b/docs/overrides/404.html @@ -56,7 +56,7 @@

UH OH! You're lost.

=============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/overrides/assets/javascripts/extra.js b/docs/overrides/assets/javascripts/extra.js index 7acf89f8b..e35ae05d8 100755 --- a/docs/overrides/assets/javascripts/extra.js +++ b/docs/overrides/assets/javascripts/extra.js @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 97d692b4c..b7310e090 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/overrides/main.html b/docs/overrides/main.html index db86e4201..9ce592a86 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -32,7 +32,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index 00d6d1c4b..b99c2b8ec 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/mkdocs.yml b/mkdocs.yml index 8b7404134..74f817ca6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/pytest.ini b/pytest.ini index ad0cc59c3..915b55546 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/scripts/bash/install_opencv.sh b/scripts/bash/install_opencv.sh index ccdb7a9d5..fccf20a92 100644 --- a/scripts/bash/install_opencv.sh +++ b/scripts/bash/install_opencv.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/scripts/bash/prepare_dataset.sh b/scripts/bash/prepare_dataset.sh index 65ab34724..ee83101d0 100644 --- a/scripts/bash/prepare_dataset.sh +++ b/scripts/bash/prepare_dataset.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +# 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. diff --git a/setup.py b/setup.py index 87de119a3..9185b83db 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/asyncio/__main__.py b/vidgear/gears/asyncio/__main__.py index 6a6283f12..67b07704e 100644 --- a/vidgear/gears/asyncio/__main__.py +++ b/vidgear/gears/asyncio/__main__.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index 48fe14a50..abebb9682 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index c91d33f2e..83a54510a 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index 119e2f6f1..da38cec0b 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 295d8e9b4..c8b6efc72 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index 3feef0388..251425fbe 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 7dd532f90..d290e0b39 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 1a0284a49..ff5c471a5 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index a1d9fae7e..c322fff8e 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/screengear.py b/vidgear/gears/screengear.py index e5995f9b8..004186be3 100644 --- a/vidgear/gears/screengear.py +++ b/vidgear/gears/screengear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/stabilizer.py b/vidgear/gears/stabilizer.py index 7eb019d13..91746de1b 100644 --- a/vidgear/gears/stabilizer.py +++ b/vidgear/gears/stabilizer.py @@ -5,7 +5,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/videogear.py b/vidgear/gears/videogear.py index 10dc4e639..681a9abdd 100644 --- a/vidgear/gears/videogear.py +++ b/vidgear/gears/videogear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 32ebb8a7c..0e26af04c 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/network_tests/asyncio_tests/test_helper.py b/vidgear/tests/network_tests/asyncio_tests/test_helper.py index e3436c474..5741146c0 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_helper.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_helper.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. 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 7bb889e69..9f84f3209 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 5071b20de..23ab48996 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index 42ecd9851..386c8613d 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. 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 d36b0b23f..a1a188810 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index ee0c3913d..377c0dec0 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index bc6c482ce..217ad6fe3 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/streamer_tests/test_init.py b/vidgear/tests/streamer_tests/test_init.py index e01504c64..06cec587a 100644 --- a/vidgear/tests/streamer_tests/test_init.py +++ b/vidgear/tests/streamer_tests/test_init.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index 188709199..55a9f6230 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 3b13c4a77..79e0ab9c6 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/utils/fake_picamera/picamera.py b/vidgear/tests/utils/fake_picamera/picamera.py index eaea53f44..4fabd0313 100644 --- a/vidgear/tests/utils/fake_picamera/picamera.py +++ b/vidgear/tests/utils/fake_picamera/picamera.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/utils/fps.py b/vidgear/tests/utils/fps.py index c30e5befc..85908181b 100644 --- a/vidgear/tests/utils/fps.py +++ b/vidgear/tests/utils/fps.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/videocapture_tests/test_camgear.py b/vidgear/tests/videocapture_tests/test_camgear.py index 1760a9a1e..18f58cfe2 100644 --- a/vidgear/tests/videocapture_tests/test_camgear.py +++ b/vidgear/tests/videocapture_tests/test_camgear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/videocapture_tests/test_pigear.py b/vidgear/tests/videocapture_tests/test_pigear.py index 600370b14..2da403c11 100644 --- a/vidgear/tests/videocapture_tests/test_pigear.py +++ b/vidgear/tests/videocapture_tests/test_pigear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/videocapture_tests/test_screengear.py b/vidgear/tests/videocapture_tests/test_screengear.py index 202a193d0..e9eb5de91 100644 --- a/vidgear/tests/videocapture_tests/test_screengear.py +++ b/vidgear/tests/videocapture_tests/test_screengear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/videocapture_tests/test_videogear.py b/vidgear/tests/videocapture_tests/test_videogear.py index 1a4a9e49f..560249b16 100644 --- a/vidgear/tests/videocapture_tests/test_videogear.py +++ b/vidgear/tests/videocapture_tests/test_videogear.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/writer_tests/test_IO.py b/vidgear/tests/writer_tests/test_IO.py index b2079e205..26ec86a7c 100644 --- a/vidgear/tests/writer_tests/test_IO.py +++ b/vidgear/tests/writer_tests/test_IO.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/writer_tests/test_compression_mode.py b/vidgear/tests/writer_tests/test_compression_mode.py index 55dfdda02..06a1719e6 100644 --- a/vidgear/tests/writer_tests/test_compression_mode.py +++ b/vidgear/tests/writer_tests/test_compression_mode.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. diff --git a/vidgear/tests/writer_tests/test_non_compression_mode.py b/vidgear/tests/writer_tests/test_non_compression_mode.py index 34edaf7c9..98d975988 100644 --- a/vidgear/tests/writer_tests/test_non_compression_mode.py +++ b/vidgear/tests/writer_tests/test_non_compression_mode.py @@ -2,7 +2,7 @@ =============================================== vidgear library source-code is deployed under the Apache 2.0 License: -Copyright (c) 2019-2020 Abhishek Thakur(@abhiTronix) +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. From 3580991a90988aa3757b7c2bd71c2daf7d32cfaa Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 11 Aug 2021 23:48:13 +0530 Subject: [PATCH 091/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Updated=20docs?= =?UTF-8?q?=20for=20new=20Apple=20HLS=20StreamGear=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Added StreamGear HLS transcoding examples for both StreamGear modes. - 🎨 Updated StreamGear parameters to w.r.t new HLS configurations. - 🚸 Added open-sourced "Sintel" - project Durian Teaser Demo with StreamGear's HLS stream using Clappr and raw.githack.com. - 🍱 Added new HLS chunks at https://github.com/abhiTronix/vidgear-docs-additionals for StreamGear - 🎨 Added support for HLS video in Clappr within `custom.js` using HlsjsPlayback plugin. - 💄 Added support for Video Thumbnail preview for HLS video in Clappr within `custom.js` - 🍱 Added `hlsjs-playback.min.js` JS script and suitable configuration for HlsjsPlayback plugin. - 💄 Added custom labels for quality levels selector in `custom.js`. - 📝 Added new docs content related to new Apple HLS format. - 🚚 Updated DASH chunk folder at https://github.com/abhiTronix/vidgear-docs-additionals. - ✏️ Fixed docs contexts and typos. - 🐛 StreamGear: Fixed OPUS audio fragments not supported with MP4 video in HLS. --- README.md | 8 +- docs/gears/streamgear/introduction.md | 51 +- docs/gears/streamgear/params.md | 91 +- docs/gears/streamgear/rtfm/overview.md | 25 +- docs/gears/streamgear/rtfm/usage.md | 1206 ++++++++++++++------ docs/gears/streamgear/ssm/overview.md | 22 + docs/gears/streamgear/ssm/usage.md | 315 +++-- docs/gears/writegear/compression/params.md | 2 + docs/index.md | 2 - docs/overrides/assets/javascripts/extra.js | 57 +- docs/overrides/main.html | 1 + vidgear/gears/streamgear.py | 12 +- 12 files changed, 1300 insertions(+), 492 deletions(-) diff --git a/README.md b/README.md index 388dbeb04..38c71f46b 100644 --- a/README.md +++ b/README.md @@ -437,15 +437,15 @@ In addition to this, WriteGear also provides flexible access to [**OpenCV's Vide -> *StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH) in just few lines of python code.* +> *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 provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**][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 fixed 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. +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. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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**](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 [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming will be added soon. +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. **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 4a299919f..e597bafe4 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -29,17 +29,17 @@ limitations under the License. ## Overview -> StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH)_ in just few lines of python code. +> 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 provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) 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 fixed 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. +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. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ 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. +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 only supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ , but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. Also, Multiple DRM support is yet to be implemented. +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. -!!! tip "For streaming with traditional protocols such as RTMP, RTSP/RTP you can use [WriteGear](../../writegear/introduction/) API instead." +!!! tip "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead."   @@ -60,7 +60,7 @@ StreamGear primarily operates in following independent modes for transcoding: ??? warning "Real-time Frames Mode is NOT Live-Streaming." - You can enable live-streaming in Real-time Frames Mode by using using exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of stream_params dictionary parameter in WebGear_RTC 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 using 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**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. @@ -82,20 +82,39 @@ from vidgear.gears import StreamGear ## Watch Demo -Watch StreamGear transcoded MPEG-DASH Stream: +=== "Watch MPEG-DASH Stream" -
-
-
-
+ Watch StreamGear transcoded MPEG-DASH Stream: + +
+
+
+
+
+
-
-
-

Powered by clappr & shaka-player

+

Powered by clappr & shaka-player

+ + !!! info "This video assets _(Manifest and segments)_ are hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" + + !!! quote "Video Credits: [**"Tears of Steel"** - Project Mango Teaser](https://mango.blender.org/download/)" + +=== "Watch APPLE HLS Stream" + + Watch StreamGear transcoded APPLE HLS Stream: + +
+
+
+
+
+
+
+

Powered by clappr & HlsjsPlayback

-!!! info "This video assets _(Manifest and segments)_ are hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" + !!! info "This video assets _(Playlist and segments)_ are hosted on [GitHub Repository](https://github.com/abhiTronix/vidgear-docs-additionals) and served with [raw.githack.com](https://raw.githack.com)" -!!! quote "Video Credits: [**"Tears of Steel"** - Project Mango Teaser](https://mango.blender.org/download/)" + !!! quote "Video Credits: [**"Sintel"** - Project Durian Teaser](https://durian.blender.org/download/)"   diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 8da1319e3..5c4efcf97 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -24,10 +24,12 @@ limitations under the License. ## **`output`** -This parameter sets the valid filename/path for storing the StreamGear assets _(Manifest file(such as Media Presentation Description(MPD) in-case of DASH) & Transcoded sequence of segments)_. +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)_. !!! 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`." + !!! 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." @@ -40,41 +42,77 @@ 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: - ```python - streamer = StreamGear(output = '/home/foo/foo1') # Define streamer with manifest saving directory path - ``` + === "DASH" + + ```python + streamer = StreamGear(output = "/home/foo/foo1") # Define streamer with manifest saving directory path + ``` + + === "HLS" + + ```python + streamer = StreamGear(output = "/home/foo/foo1", format="hls") # Define streamer with playlist saving directory path + ``` * **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. - ```python - streamer = StreamGear(output = 'output_foo.mpd') # Define streamer with manifest file name - ``` + === "DASH" - !!! warning "Make sure to provide _valid filename with valid file-extension_ for selected [format](#format) value _(such as `output.mpd` in case of MPEG-DASH)_, otherwise StreamGear will throw `AssertionError`." + ```python + streamer = StreamGear(output = "output_foo.mpd") # Define streamer with manifest file name + ``` + + === "HLS" + ```python + streamer = StreamGear(output = "output_foo.m3u8", format="hls") # Define streamer with playlist file name + ``` * **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: - ```python - streamer = StreamGear(output = 'http://195.167.1.101/live/test.mpd') #Define streamer - ``` + + === "DASH" + + ```python + streamer = StreamGear(output = "http://195.167.1.101/live/test.mpd") #Define streamer + ``` + + === "HLS" + + ```python + streamer = StreamGear(output = "http://195.167.1.101/live/test.m3u8", format="hls") #Define streamer + ```   ## **`format`** -This parameter select the adaptive HTTP streaming format. HTTP streaming works by breaking the overall stream into a sequence of small HTTP-based file downloads, each downloading one short chunk of an overall potentially unbounded transport stream. For now, the only supported format is: `'dash'` _(i.e [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/))_, but other adaptive streaming technologies such as Apple HLS, Microsoft Smooth Streaming, will be added soon. +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!" + +!!! 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`." + **Data-Type:** String -**Default Value:** Its default value is `'dash'` +**Default Value:** Its default value is `dash` **Usage:** -```python -StreamGear(output = 'output_foo.mpd', format="dash") -``` +=== "DASH" + + ```python + StreamGear(output = "output_foo.mpd", format="dash") + ``` + +=== "HLS" + + ```python + StreamGear(output = "output_foo.m3u8", format="hls") + ``` +   @@ -183,17 +221,17 @@ StreamGear API provides some exclusive internal parameters to easily generate St   -* **`-audio`** _(dict)_: This attribute takes external custom audio path as audio-input for all StreamGear streams. Its value 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 "Make sure this audio-source is compatible with provided video -source, otherwise you encounter multiple errors, or even no output at all!" - !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-custom-audio)" - - * **Audio Filename**: Valid path to 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 ``` - * **Audio URL**: Valid URL of a network audio stream as follows: + !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-custom-audio)" + + * **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)_" @@ -201,6 +239,15 @@ StreamGear API provides some exclusive internal parameters to easily generate St stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 ``` + * **Device name and Demuxer** _(list)_: Valid audio device name followed by suitable demuxer as follows: + + ```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)" + + +   * **`-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: @@ -293,6 +340,8 @@ stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "ze 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: +!!! info "Similarily, supported demuxers and filters depends upons compiled FFmpeg in use." + ```sh # for checking encoder ffmpeg -encoders # use `ffmpeg.exe -encoders` on windows diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index ea2ac3936..52b7a8419 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -31,15 +31,17 @@ limitations under the License. When no valid input is received on [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, StreamGear API activates this mode where it directly transcodes 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. +SteamGear 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)_ with this mode. + 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. This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. - +  !!! alert "Real-time Frames Mode is NOT Live-Streaming." - 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). + 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 @@ -61,4 +63,23 @@ This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.g See here 🚀
+## Parameters + + + +## References + + + + +## FAQs + + +   \ No newline at end of file diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 38fcf5a4b..963d74815 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -45,52 +45,104 @@ 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." -```python -# 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() +=== "DASH" -# describe a suitable manifest-file location/name -streamer = StreamGear(output="dash_out.mpd") + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 -# loop over -while True: + # open any valid video stream(for e.g `foo1.mp4` file) + stream = CamGear(source='foo1.mp4').start() - # read frames from stream - frame = stream.read() + # describe a suitable manifest-file location/name + streamer = StreamGear(output="dash_out.mpd") - # check for frame if Nonetype - if frame is None: - break + # loop over + while True: + # read frames from stream + frame = stream.read() - # {do something with the frame here} + # check for frame if Nonetype + if frame is None: + break - # send frame to streamer - streamer.stream(frame) + # {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 + # send frame to streamer + streamer.stream(frame) -# close output window -cv2.destroyAllWindows() + # Show output window + cv2.imshow("Output Frame", frame) -# safely close video stream -stream.stop() + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# safely close streamer -streamer.terminate() -``` + # close output window + cv2.destroyAllWindows() + + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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 + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ``` !!! 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)_." @@ -107,54 +159,108 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, !!! 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." -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import StreamGear -import cv2 +=== "DASH" -# open any valid video stream(from web-camera attached at index `0`) -stream = CamGear(source=0).start() + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 -# 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} + # open any valid video stream(from web-camera attached at index `0`) + stream = CamGear(source=0).start() -# describe a suitable manifest-file location/name -streamer = StreamGear(output="dash_out.mpd", **stream_params) + # 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} -# loop over -while True: + # describe a suitable manifest-file location/name + streamer = StreamGear(output="dash_out.mpd", **stream_params) - # read frames from stream - frame = stream.read() + # loop over + while True: - # check for frame if Nonetype - if frame is None: - break + # read frames from stream + frame = stream.read() - # {do something with the frame here} + # check for frame if Nonetype + if frame is None: + break - # send frame to streamer - streamer.stream(frame) + # {do something with the frame here} - # Show output window - cv2.imshow("Output Frame", frame) + # send frame to streamer + streamer.stream(frame) - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # Show output window + cv2.imshow("Output Frame", frame) -# close output window -cv2.destroyAllWindows() + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# safely close video stream -stream.stop() + # close output window + cv2.destroyAllWindows() + + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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`) + 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", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ``` -# safely close streamer -streamer.terminate() -```   @@ -162,53 +268,106 @@ streamer.terminate() 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: -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import StreamGear -import cv2 +=== "DASH" + + ```python + # 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(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") -# describe a suitable manifest-file location/name -streamer = StreamGear(output="dash_out.mpd") + # loop over + while True: -# loop over -while True: + # read frames from stream + frame = stream.read() - # read frames from stream - frame = stream.read() + # check for frame if Nonetype + if frame is None: + break - # check for frame if Nonetype - if frame is None: - break + # {simulating RGB frame for this example} + frame_rgb = frame[:,:,::-1] - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] + # send frame to streamer + streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode - # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode + # Show output window + cv2.imshow("Output Frame", frame) - # Show output window - cv2.imshow("Output Frame", frame) + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # close output window + cv2.destroyAllWindows() -# close output window -cv2.destroyAllWindows() + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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] -# safely close video stream -stream.stop() -# safely close streamer -streamer.terminate() -``` + # 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.terminate() + ``` +   @@ -218,55 +377,110 @@ In Real-time Frames Mode, StreamGear API provides exclusive [`-input_framerate`] !!! 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." -```python -# 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() +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 -# retrieve framerate from CamGear Stream and pass it as `-input_framerate` value -stream_params = {"-input_framerate":stream.framerate} + # Open live video stream on webcam at first index(i.e. 0) device + stream = CamGear(source=0).start() -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) + # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value + stream_params = {"-input_framerate":stream.framerate} -# loop over -while True: + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", **stream_params) - # read frames from stream - frame = stream.read() + # loop over + while True: - # check for frame if Nonetype - if frame is None: - break + # read frames from stream + frame = stream.read() + # check for frame if Nonetype + if frame is None: + break - # {do something with the frame here} + # {do something with the frame here} - # send frame to streamer - streamer.stream(frame) - # Show output window - cv2.imshow("Output Frame", frame) + # send frame to streamer + streamer.stream(frame) - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # Show output window + cv2.imshow("Output Frame", frame) -# close output window -cv2.destroyAllWindows() + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# safely close video stream -stream.stop() + # close output window + cv2.destroyAllWindows() -# safely close streamer -streamer.terminate() -``` + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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} + + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ```   @@ -276,52 +490,104 @@ You can easily use StreamGear API directly with any other Video Processing libra !!! tip "This just a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature/example will work in the similar manner." -```python -# import required libraries -from vidgear.gears import StreamGear -import cv2 +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import StreamGear + import cv2 + + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = cv2.VideoCapture(0) -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = cv2.VideoCapture(0) + # describe a suitable manifest-file location/name + streamer = StreamGear(output="dash_out.mpd") -# describe a suitable manifest-file location/name -streamer = StreamGear(output="dash_out.mpd") + # loop over + while True: -# loop over -while True: + # read frames from stream + (grabbed, frame) = stream.read() - # read frames from stream - (grabbed, frame) = stream.read() + # check for frame if not grabbed + if not grabbed: + break - # check for frame if not grabbed - if not grabbed: - break + # {do something with the frame here} + # lets convert frame to gray for this example + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # {do something with the frame here} - # lets convert frame to gray for this example - gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + # send frame to streamer + streamer.stream(gray) - # send frame to streamer - streamer.stream(gray) + # Show output window + cv2.imshow("Output Gray Frame", gray) - # Show output window - cv2.imshow("Output Gray Frame", gray) + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # close output window + cv2.destroyAllWindows() -# close output window -cv2.destroyAllWindows() + # safely close video stream + stream.release() -# safely close video stream -stream.release() + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import StreamGear + import cv2 + + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = cv2.VideoCapture(0) + + # describe a suitable manifest-file location/name + streamer = StreamGear(output="hls_out.m3u8", format = "hls") + + # loop over + while True: + + # read frames from stream + (grabbed, frame) = stream.read() + + # check for frame if not grabbed + if not grabbed: + break + + # {do something with the frame here} + # lets convert frame to gray for this example + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + + # send frame to streamer + streamer.stream(gray) + + # Show output window + cv2.imshow("Output Gray Frame", gray) + + # 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.release() + + # safely close streamer + streamer.terminate() + ``` -# safely close streamer -streamer.terminate() -```   @@ -338,61 +604,122 @@ Similar to Single-Source Mode, you can easily generate any number of additional !!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import StreamGear -import cv2 -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = CamGear(source=0).start() +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 + + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = CamGear(source=0).start() -# 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": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate - ], -} + # 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": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], + } -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd") + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd") -# loop over -while True: + # loop over + while True: - # read frames from stream - frame = stream.read() + # read frames from stream + frame = stream.read() - # check for frame if Nonetype - if frame is None: - break + # check for frame if Nonetype + if frame is None: + break - # {do something with the frame here} + # {do something with the frame here} - # send frame to streamer - streamer.stream(frame) + # send frame to streamer + streamer.stream(frame) - # Show output window - cv2.imshow("Output Frame", frame) + # Show output window + cv2.imshow("Output Frame", frame) - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# close output window -cv2.destroyAllWindows() + # close output window + cv2.destroyAllWindows() -# safely close video stream -stream.stop() + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` -# safely close streamer -streamer.terminate() -``` +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 + + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = CamGear(source=0).start() + + # 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": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], + } + + # describe a suitable manifest-file location/name and assign params + 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 + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ```   @@ -406,75 +733,138 @@ In Real-time Frames Mode, if you want to add audio to your streams, you've to us !!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../../params/#a-exclusive-parameters)" -```python -# 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() +=== "DASH" + + ```python + # 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() + + # add various streams, along with custom audio + 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 + ], + "-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" + } -# add various streams, along with custom audio -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 - ], - "-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" -} + # 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 and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) + # loop over + while True: -# loop over -while True: + # read frames from stream + frame = stream.read() - # read frames from stream - frame = stream.read() + # check for frame if Nonetype + if frame is None: + break - # check for frame if Nonetype - if frame is None: - break + # {do something with the frame here} - # {do something with the frame here} + # send frame to streamer + streamer.stream(frame) - # send frame to streamer - streamer.stream(frame) + # Show output window + cv2.imshow("Output Frame", frame) - # Show output window - cv2.imshow("Output Frame", frame) + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # close output window + cv2.destroyAllWindows() -# close output window -cv2.destroyAllWindows() + # safely close video stream + stream.stop() -# safely close video stream -stream.stop() + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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() + + # add various streams, along with custom audio + 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 + ], + "-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" + } + + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} -# safely close streamer -streamer.terminate() -``` + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ```   ## 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 need to format your audio device name and suitable decoder as `list` and assign to this attribute, and StreamGear API will automatically validate and map it to all generated streams. The complete example is as follows: +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 need to format your audio device name followed by suitable demuxer as `list` and assign to this attribute, and StreamGear API will automatically validate and map it to all generated streams. The complete example is as follows: !!! alert "Example Assumptions" - * You're running are Windows machine. - * You already have appropriate audio drivers and software installed on your machine. + * 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. ??? tip "Using `-audio` attribute on different OS platforms" @@ -510,7 +900,7 @@ In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/ - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: ```python - # assign appropriate input audio-source + # assign appropriate input audio-source device and demuxer device and demuxer stream_params = {"-audio": ["-f","dshow", "-i", "audio=Microphone (USB2.0 Camera)"]} ``` @@ -550,7 +940,7 @@ In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/ !!! info "The easiest thing to do is to reference sound card directly, namely "card 0" (Intel ICH5) and "card 1" (Microphone on the USB web cam), as `hw:0` or `hw:1`" ```python - # assign appropriate input audio-source "card 1" (Microphone on the USB web cam) + # assign appropriate input audio-source device and demuxer device and demuxer stream_params = {"-audio": ["-f","alsa", "-i", "hw:1"]} ``` @@ -586,7 +976,7 @@ In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/ - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: ```python - # assign appropriate input audio-source + # assign appropriate input audio-source device and demuxer stream_params = {"-audio": ["-f","avfoundation", "-audio_device_index", "0"]} ``` @@ -597,63 +987,138 @@ In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/ !!! 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." -```python -# 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() +=== "DASH" -# add various streams, along with custom audio -stream_params = { - "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps - ], - "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - stream_params = {"-audio": ["-f","dshow", "-i", "audio=Microphone (USB2.0 Camera)"]} # assign appropriate input audio-source -} + ```python + # import required libraries + from vidgear.gears import CamGear + from vidgear.gears import StreamGear + import cv2 -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) + # open any valid video stream(for e.g `foo1.mp4` file) + stream = CamGear(source="foo1.mp4").start() -# loop over -while True: + # add various streams, along with custom audio + stream_params = { + "-streams": [ + { + "-resolution": "1280x720", + "-video_bitrate": "4000k", + }, # Stream1: 1920x1080 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps + ], + "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-audio": [ + "-f", + "dshow", + "-i", + "audio=Microphone (USB2.0 Camera)", + ], # assign appropriate input audio-source device and demuxer + } - # read frames from stream - frame = stream.read() + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", **stream_params) - # check for frame if Nonetype - if frame is None: - break + # loop over + while True: + # read frames from stream + frame = stream.read() - # {do something with the frame here} + # check for frame if Nonetype + if frame is None: + break + # {do something with the frame here} - # send frame to streamer - streamer.stream(frame) + # send frame to streamer + streamer.stream(frame) - # Show output window - cv2.imshow("Output Frame", frame) + # Show output window + cv2.imshow("Output Frame", frame) - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# close output window -cv2.destroyAllWindows() + # close output window + cv2.destroyAllWindows() -# safely close video stream -stream.stop() + # safely close video stream + stream.stop() -# safely close streamer -streamer.terminate() -``` + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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() + + # add various streams, along with custom audio + stream_params = { + "-streams": [ + { + "-resolution": "1280x720", + "-video_bitrate": "4000k", + }, # Stream1: 1920x1080 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps + ], + "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-audio": [ + "-f", + "dshow", + "-i", + "audio=Microphone (USB2.0 Camera)", + ], # assign appropriate input audio-source device and demuxer + } + + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.m3u8", format="hls", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ```   @@ -681,64 +1146,127 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also ``` -```python -# import required libraries -from vidgear.gears import VideoGear -from vidgear.gears import StreamGear -import cv2 +=== "DASH" -# Open suitable video stream, such as webcam on first index(i.e. 0) -stream = VideoGear(source=0).start() + ```python + # import required libraries + from vidgear.gears import VideoGear + from vidgear.gears import StreamGear + import cv2 -# add various streams with custom Video Encoder and optimizations -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 - ], - "-vcodec": "h264_vaapi", # define custom Video encoder - "-vaapi_device": "/dev/dri/renderD128", # define device location - "-vf": "format=nv12,hwupload", # define video pixformat -} + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = VideoGear(source=0).start() -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) + # add various streams with custom Video Encoder and optimizations + 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 + ], + "-vcodec": "h264_vaapi", # define custom Video encoder + "-vaapi_device": "/dev/dri/renderD128", # define device location + "-vf": "format=nv12,hwupload", # define video pixformat + } -# loop over -while True: + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", **stream_params) - # read frames from stream - frame = stream.read() + # loop over + while True: - # check for frame if Nonetype - if frame is None: - break + # read frames from stream + frame = stream.read() + # check for frame if Nonetype + if frame is None: + break - # {do something with the frame here} + # {do something with the frame here} - # send frame to streamer - streamer.stream(frame) - # Show output window - cv2.imshow("Output Frame", frame) + # send frame to streamer + streamer.stream(frame) - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break + # Show output window + cv2.imshow("Output Frame", frame) -# close output window -cv2.destroyAllWindows() + # check for 'q' key if pressed + key = cv2.waitKey(1) & 0xFF + if key == ord("q"): + break -# safely close video stream -stream.stop() + # close output window + cv2.destroyAllWindows() -# safely close streamer -streamer.terminate() -``` + # safely close video stream + stream.stop() + + # safely close streamer + streamer.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import VideoGear + from vidgear.gears import StreamGear + import cv2 + + # Open suitable video stream, such as webcam on first index(i.e. 0) + stream = VideoGear(source=0).start() + + # add various streams with custom Video Encoder and optimizations + 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 + ], + "-vcodec": "h264_vaapi", # define custom Video encoder + "-vaapi_device": "/dev/dri/renderD128", # define device location + "-vf": "format=nv12,hwupload", # define video pixformat + } + + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + + # {do something with the frame here} + + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ```   diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 8c9246dd6..ea785e088 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -30,6 +30,8 @@ limitations under the License. In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. +SteamGear 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)_ with this mode. + This mode provide [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function to process audio-video files into streamable chunks. This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#stream_params) dictionary parameter, during StreamGear initialization. @@ -47,4 +49,24 @@ This mode can be easily activated by assigning suitable video path as input to [ See here 🚀 +## Parameters + + + +## References + + + + +## FAQs + + + +   \ No newline at end of file diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 4c3c35ea2..a0e118a3f 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -39,19 +39,38 @@ Following is the bare-minimum code you need to get started with StreamGear API i !!! 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." -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode with valid video input -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 -streamer.transcode_source() -# terminate -streamer.terminate() -``` +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode with valid video input + 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 + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode with valid video input + 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 + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + !!! 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." @@ -67,19 +86,37 @@ You can easily activate ==Low-latency Livestreaming in Single-Source Mode==, whe !!! 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." -```python -# 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) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` +=== "DASH" + + ```python + # 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) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ```   @@ -98,27 +135,54 @@ In addition to Primary Stream, you can easily generate any number of additional !!! fail "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode and also define various streams -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 framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate - {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate - ], -} -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` + +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode and also define various streams + 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 framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], + } + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", **stream_params) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode and also define various streams + 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 framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate + ], + } + # describe a suitable master playlist location/name and assign params + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ```   @@ -130,27 +194,55 @@ By default, if input video-source _(i.e. `-video_source`)_ contains any audio, t !!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../../params/#a-exclusive-parameters)" -```python -# import required libraries -from vidgear.gears import StreamGear - -# activate Single-Source Mode and various streams, along with custom audio -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 - ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" -} -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` + +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode and various streams, along with custom audio + 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 + ], + "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + } + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", **stream_params) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import StreamGear + + # activate Single-Source Mode and various streams, along with custom audio + 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 + ], + "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + } + # describe a suitable master playlist location/name and assign params + streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ``` +   @@ -168,34 +260,65 @@ For this example, let us use our own [H.265/HEVC](https://trac.ffmpeg.org/wiki/E !!! fail "Always use `-streams` attribute to define additional streams safely, any duplicate or incorrect stream definition can break things!" - -```python -# 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 - "-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 - "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-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` -} - -# describe a suitable manifest-file location/name and assign params -streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) -# trancode source -streamer.transcode_source() -# terminate -streamer.terminate() -``` +=== "DASH" + + ```python + # 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 + "-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 + "-streams": [ + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-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` + } + + # describe a suitable manifest-file location/name and assign params + streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) + # trancode source + streamer.transcode_source() + # terminate + streamer.terminate() + ``` + +=== "HLS" + + ```python + # 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 + "-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 + "-streams": [ + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-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` + } + + # 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 + streamer.transcode_source() + # terminate + streamer.terminate() + ```   diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index d1845b5fe..0c62a1652 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -173,6 +173,8 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor All the encoders 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: +!!! info "Similarily, supported demuxers and filters depends upons compiled FFmpeg in use." + ```sh ffmpeg -encoders # use `ffmpeg.exe -encoders` on windows ``` diff --git a/docs/index.md b/docs/index.md index b2322bc49..d26ec494f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,8 +75,6 @@ These Gears can be classified as follows: #### Streaming Gears -!!! tip "You can also use [WriteGear](gears/writegear/introduction/) for streaming with traditional protocols such as RTMP, RTSP/RTP." - * [StreamGear](gears/streamgear/introduction/): Handles Transcoding of High-Quality, Dynamic & Adaptive Streaming Formats. * **Asynchronous I/O Streaming Gear:** diff --git a/docs/overrides/assets/javascripts/extra.js b/docs/overrides/assets/javascripts/extra.js index e35ae05d8..65c96542c 100755 --- a/docs/overrides/assets/javascripts/extra.js +++ b/docs/overrides/assets/javascripts/extra.js @@ -17,9 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. =============================================== */ - -var player = new Clappr.Player({ - source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/fbcf0377b171b777db5e0b3b939138df35a90676/streamgear_video_chunks/streamgear_dash.mpd', +var player_dash = new Clappr.Player({ + source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/dca65250d95eeeb87d594686c2f2c2208a015486/streamgear_video_segments/DASH/streamgear_dash.mpd', plugins: [DashShakaPlayback, LevelSelector], shakaConfiguration: { streaming: { @@ -29,10 +28,56 @@ var player = new Clappr.Player({ shakaOnBeforeLoad: function(shaka_player) { // shaka_player.getNetworkingEngine().registerRequestFilter() ... }, + levelSelectorConfig: { + title: 'Quality', + labels: { + 2: 'High', // 500kbps + 1: 'Med', // 240kbps + 0: 'Low', // 120kbps + }, + labelCallback: function(playbackLevel, customLabel) { + return customLabel; // High 720p + } + }, width: '100%', height: '100%', - parentId: '#player', - poster: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/674250e6c0387d0d0528406eec35bc580ceafee3/streamgear_video_chunks/hd_thumbnail.jpg', + parentId: '#player_dash', + poster: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/dca65250d95eeeb87d594686c2f2c2208a015486/streamgear_video_segments/DASH/hd_thumbnail.jpg', + preload: 'metadata', +}); + +var player_hls = new Clappr.Player({ + source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/abc0c193ab26e21f97fa30c9267de6beb8a72295/streamgear_video_segments/HLS/streamgear_hls.m3u8', + plugins: [HlsjsPlayback, LevelSelector], + hlsUseNextLevel: false, + hlsMinimumDvrSize: 60, + hlsRecoverAttempts: 16, + hlsPlayback: { + preload: true, + customListeners: [], + }, + playback: { + extrapolatedWindowNumSegments: 2, + triggerFatalErrorOnResourceDenied: false, + hlsjsConfig: { + // hls.js specific options + }, + }, + levelSelectorConfig: { + title: 'Quality', + labels: { + 2: 'High', // 500kbps + 1: 'Med', // 240kbps + 0: 'Low', // 120kbps + }, + labelCallback: function(playbackLevel, customLabel) { + return customLabel; // High 720p + } + }, + width: '100%', + height: '100%', + parentId: '#player_hls', + poster: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/abc0c193ab26e21f97fa30c9267de6beb8a72295/streamgear_video_segments/HLS/hd_thumbnail.jpg', preload: 'metadata', }); @@ -52,4 +97,4 @@ var player_stab = new Clappr.Player({ parentId: '#player_stab', poster: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/94bf767c28bf2fe61b9c327625af8e22745f9fdf/stabilizer_video_chunks/hd_thumbnail_2.png', preload: 'metadata', -}); +}); \ No newline at end of file diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 9ce592a86..969e3c710 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -26,6 +26,7 @@ + {% endblock %} commitsPull-Request (PR)CI test (passed)PR commits (external)merge \ No newline at end of file From 1d34f0fbad1ca5ae7edf2c6bf5b75aa27e63aed9 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 13 Aug 2021 07:54:15 +0530 Subject: [PATCH 093/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Helper:=20Implemen?= =?UTF-8?q?ted=20RTSP=20protocol=20validation=20as=20demuxer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ Implemented RSTP protocol validation as demuxer, since it's not a protocol but a demuxer. - 🎨 Implemented `get_supported_demuxers` method to get list of supported demuxers. - ✨ Added 4320p resolution support to `dimensions_to_resolutions` method. - 📝 Minor docs tweaks --- docs/gears/streamgear/params.md | 4 ++++ docs/gears/writegear/compression/params.md | 2 ++ vidgear/gears/helper.py | 28 ++++++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 5c4efcf97..ad1ab9470 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -328,6 +328,10 @@ 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." + + !!! 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 diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index 0c62a1652..76e6d3ed1 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -118,6 +118,8 @@ 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." + !!! 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" ```python diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index d290e0b39..a52cdb220 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -301,6 +301,7 @@ def dimensions_to_resolutions(value): "1920x1080": "1080p", "2560x1440": "1440p", "3840x2160": "2160p", + "7680x4320": "4320p", } return ( list(map(supported_resolutions.get, value, value)) @@ -332,10 +333,32 @@ def get_supported_vencoders(path): finder = re.compile(r"[A-Z]*[\.]+[A-Z]*\s[a-z0-9_-]*") # find all outputs outputs = finder.findall("\n".join(supported_vencoders)) - # return outputs + # return output findings return [[s for s in o.split(" ")][-1] for o in outputs] +def get_supported_demuxers(path): + """ + ### get_supported_demuxers + + Find and returns FFmpeg's supported demuxers + + Parameters: + path (string): absolute path of FFmpeg binaries + + **Returns:** List of supported demuxers. + """ + demuxers = check_output([path, "-hide_banner", "-demuxers"]) + splitted = [x.decode("utf-8").strip() for x in demuxers.split(b"\n")] + supported_demuxers = splitted[splitted.index("--") + 1 : len(splitted) - 1] + # compile regex + finder = re.compile(r"\s\s[a-z0-9_,-]+\s+") + # find all outputs + outputs = finder.findall("\n".join(supported_demuxers)) + # return output findings + return [o.strip() for o in outputs] + + def is_valid_url(path, url=None, logging=False): """ ### is_valid_url @@ -359,7 +382,8 @@ 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] - supported_protocols += ["rtsp"] # rtsp not included somehow + # rtsp is a demuxer somehow + supported_protocols += ["rtsp"] 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: if logging: From 67822bc48b7b768a59aed712eeeb72ce4ce0faef Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 13 Aug 2021 07:59:54 +0530 Subject: [PATCH 094/112] =?UTF-8?q?=F0=9F=9A=B8=20CI:=20Minor=20tweaks=20t?= =?UTF-8?q?o=20`needs-more-info`=20template.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/needs-more-info.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/needs-more-info.yml b/.github/needs-more-info.yml index 381f9b3c8..f3e05b1d5 100644 --- a/.github/needs-more-info.yml +++ b/.github/needs-more-info.yml @@ -4,6 +4,7 @@ labelToAdd: 'MISSING : TEMPLATE :grey_question:' issue: reactions: - eyes + - '-1' badTitles: - update - updates @@ -12,6 +13,7 @@ issue: - debug - demo - new + - help badTitleComment: > @{{ author }} Please re-edit this issue title to provide more relevant info. From ff275451d930b00bec3a4f55c2f37f194f2bfb64 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Fri, 13 Aug 2021 08:06:03 +0530 Subject: [PATCH 095/112] =?UTF-8?q?=F0=9F=91=B7=20Codecov:=20Added=20more?= =?UTF-8?q?=20directories=20to=20ignore=20list.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codecov.yml b/codecov.yml index cf2312c1d..544e750c8 100644 --- a/codecov.yml +++ b/codecov.yml @@ -28,5 +28,7 @@ coverage: ignore: - "vidgear/tests" + - "docs" + - "scripts" - "vidgear/gears/asyncio/__main__.py" #trivial - "setup.py" \ No newline at end of file From d46c598f58482d5ca59297a7621dc9b16a82688a Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 16 Aug 2021 21:17:32 +0530 Subject: [PATCH 096/112] =?UTF-8?q?=E2=9C=A8=20NetGear=5FAsync:=20New=20ex?= =?UTF-8?q?clusive=20Bidirectional=20Mode=20for=20bidirectional=20data=20t?= =?UTF-8?q?ransfer(Fixes=20#235)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✨ NetGear_Async's first-ever exclusive Bidirectional mode with pure asyncio implementation. - ⚠️ Bidirectional mode is only available with User-defined Custom Source(i.e. `source=None`) - ⚡️ Added support for `PAIR` & `REQ/REP` bidirectional patterns for this mode. - ⚡️ Added powerful `asyncio.Queues` for handling user data and frames in real-time. - ✨ Implemented new `transceive_data` method to Transmit _(in Recieve mode)_ and Receive _(in Send mode)_ data in real-time. - ✨ Implemented `terminate_connection` internal asyncio method to safely terminate ZMQ connection and queues. - ⚡️ Added `msgpack` automatic compression encoding and decoding of data and frames in bidirectional mode. - ⚡️ Added support for `np.ndarray` video frames. - ✨ Added new `bidirectional_mode` attribute for enabling this mode. - ⚡️ Added 8-digit random alphanumeric id generator for each device. - ⚠️ NetGear_Async will throw `RuntimeError` if bidirectional mode is disabled at server or client but not both. - ✨ Added new `secrets` and `string` imports. - 🐛 Fixed bug related asyncio queue freezing on calling `join()`. - 🚑️ Added `task_done()` method after every `get()` call to gracefully terminate queues. - 🔨 Improved custom source handling. - 🐛 Fixed ZMQ connection bugs in bidirectional mode. - ✏️ Fixed typos in error messages. - 💡 Added and Updated code comments. - 💄 Updated Admonitions. NetGear: - ⚡️ Update array indexing with `np.copy`. - 🎨 Minor tweaks. Docs: - 💄 Added support for search suggestions, search highlighting and search sharing (i.e. deep linking) - 🚸 Added more content to docs to make it more user-friendly. - ✏️ Fixed context and typos. --- docs/gears/streamgear/introduction.md | 16 +- docs/gears/streamgear/rtfm/overview.md | 5 + docs/gears/streamgear/rtfm/usage.md | 2 +- docs/gears/streamgear/ssm/overview.md | 7 + docs/gears/webgear/advanced.md | 4 +- docs/gears/webgear/params.md | 2 +- mkdocs.yml | 3 + vidgear/gears/asyncio/netgear_async.py | 384 +++++++++++++++++++++---- vidgear/gears/netgear.py | 69 ++--- 9 files changed, 390 insertions(+), 102 deletions(-) diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 9239c54e4..205e73f0c 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -43,6 +43,11 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast   +!!! new "New in v0.2.2" + + Apple HLS support was added in `v0.2.2`. + + !!! danger "Important" * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../ffmpeg_install/) for its installation. @@ -120,7 +125,14 @@ from vidgear.gears import StreamGear ## Recommended Players -!!! tip "Checkout out [this detailed blogpost](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works" +!!! 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 comparsion. + === "GUI Players" - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. @@ -132,7 +144,7 @@ from vidgear.gears import StreamGear - [x] **[ffplay](https://ffmpeg.org/ffplay.html):** FFplay is a very simple and portable media player using the FFmpeg libraries and the SDL library. It is mostly used as a testbed for the various FFmpeg APIs. === "Online Players" - !!! tip "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" + !!! alert "To run Online players locally, you'll need a HTTP server. For creating one yourself, See [this well-curated list ➶](https://gist.github.com/abhiTronix/7d2798bc9bc62e9e8f1e88fb601d7e7b)" - [x] **[Clapper](https://github.com/clappr/clappr):** Clappr is an extensible media player for the web. - [x] **[Shaka Player](https://github.com/google/shaka-player):** Shaka Player is an open-source JavaScript library for playing adaptive media in a browser. diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index 52b7a8419..d1c07ab82 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -39,6 +39,11 @@ This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.g   +!!! new "New in v0.2.2" + + Apple HLS support was added in `v0.2.2`. + + !!! alert "Real-time Frames Mode is NOT Live-Streaming." 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). diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 8bfa8572a..164a4c429 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -881,7 +881,7 @@ The complete example is as follows: * There's a audio device with named `"Microphone (USB2.0 Camera)"` connected to your windows machine. -??? tip "Using `-audio` attribute on different OS platforms" +??? tip "Using devices with `-audio` attribute on different OS platforms" === "On Windows" diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index fc28d91bb..99d985a04 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -36,6 +36,13 @@ This mode provide [`transcode_source()`](../../../../bonus/reference/streamgear/ This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#stream_params) dictionary parameter, during StreamGear initialization. +  + +!!! new "New in v0.2.2" + + Apple HLS support was added in `v0.2.2`. + + !!! warning * 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`**! diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index 823539615..d15340041 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -28,7 +28,7 @@ limitations under the License. ### Using WebGear with Variable Colorspace -WebGear by default only supports "BGR" colorspace with consumer or client. But you can use [`jpeg_compression_colorspace`](../params/#webgear_rtc-specific-attributes) string attribute through its options dictionary parameter to specify incoming frames colorspace. +WebGear by default only supports "BGR" colorspace frames as input, but you can use [`jpeg_compression_colorspace`](../params/#webgear_rtc-specific-attributes) string attribute through its options dictionary parameter to specify incoming frames colorspace. Let's implement a bare-minimum example using WebGear, where we will be sending [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) frames to client browser: @@ -37,7 +37,7 @@ Let's implement a bare-minimum example using WebGear, where we will be sending [ !!! example "This example works in conjunction with [Source ColorSpace manipulation for VideoCapture Gears ➶](../../../../bonus/colorspace_manipulation/#source-colorspace-manipulation)" -!!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" +!!! info "Supported `jpeg_compression_colorspace` colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" ```python # import required libraries diff --git a/docs/gears/webgear/params.md b/docs/gears/webgear/params.md index ac7049e16..d82839c7d 100644 --- a/docs/gears/webgear/params.md +++ b/docs/gears/webgear/params.md @@ -122,7 +122,7 @@ This parameter can be used to pass user-defined parameter to WebGear API by form * **`jpeg_compression_colorspace`**: _(str)_ This internal attribute is used to specify incoming frames colorspace with compression. Its usage is as follows: - !!! info "Supported colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" + !!! info "Supported `jpeg_compression_colorspace` colorspace values are `RGB`, `BGR`, `RGBX`, `BGRX`, `XBGR`, `XRGB`, `GRAY`, `RGBA`, `BGRA`, `ABGR`, `ARGB`, `CMYK`. More information can be found [here ➶](https://gitlab.com/jfolz/simplejpeg)" !!! new "New in v0.2.2" `enable_infinite_frames` attribute was added in `v0.2.2`. diff --git a/mkdocs.yml b/mkdocs.yml index 74f817ca6..0f39f2102 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -38,6 +38,8 @@ theme: - header.autohide - navigation.tabs - navigation.top + - search.suggest + - search.highlight palette: # Light mode - media: "(prefers-color-scheme: light)" @@ -66,6 +68,7 @@ theme: # Plugins plugins: - search + prebuild_index: true - git-revision-date-localized - minify: minify_html: true diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index 83a54510a..29f3feca9 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -27,6 +27,8 @@ import inspect import logging as log import msgpack +import string +import secrets import platform import zmq.asyncio import msgpack_numpy as m @@ -45,9 +47,9 @@ class NetGear_Async: """ NetGear_Async can generate the same performance as NetGear API at about one-third the memory consumption, and also provide complete server-client handling with various - options to use variable protocols/patterns similar to NetGear, but it doesn't support any of yet. + options to use variable protocols/patterns similar to NetGear, but only support bidirectional data transmission exclusive mode. - NetGear_Async is built on zmq.asyncio, and powered by a high-performance asyncio event loop called uvloop to achieve unmatchable high-speed and lag-free video streaming + NetGear_Async is built on `zmq.asyncio`, and powered by a high-performance asyncio event loop called uvloop to achieve unwatchable high-speed and lag-free video streaming over the network with minimal resource constraints. NetGear_Async can transfer thousands of frames in just a few seconds without causing any significant load on your system. @@ -100,7 +102,7 @@ def __init__( port (str): sets the valid Network Port of the Server/Client. protocol (str): sets the valid messaging protocol between Server/Client. pattern (int): sets the supported messaging pattern(flow of communication) between Server/Client - receive_mode (bool): select the Netgear's Mode of operation. + receive_mode (bool): select the NetGear_Async's Mode of operation. timeout (int/float): controls the maximum waiting time(in sec) after which Client throws `TimeoutError`. enablePiCamera (bool): provide access to PiGear(if True) or CamGear(if False) APIs respectively. stabilize (bool): enable access to Stabilizer Class for stabilizing frames. @@ -113,7 +115,7 @@ def __init__( colorspace (str): selects the colorspace of the input stream. logging (bool): enables/disables logging. time_delay (int): time delay (in sec) before start reading the frames. - options (dict): provides ability to alter Tweak Parameters of NetGear, CamGear, PiGear & Stabilizer. + options (dict): provides ability to alter Tweak Parameters of NetGear_Async, CamGear, PiGear & Stabilizer. """ # enable logging if specified @@ -161,14 +163,64 @@ def __init__( self.__stream = None # initialize Messaging Socket self.__msg_socket = None - # initialize NetGear's configuration dictionary + # initialize NetGear_Async's configuration dictionary self.config = {} + # asyncio queue handler + self.__queue = None + # define Bidirectional mode + self.__bi_mode = False # handles Bidirectional mode state # assign timeout for Receiver end if timeout > 0 and isinstance(timeout, (int, float)): self.__timeout = float(timeout) else: self.__timeout = 15.0 + + # generate 8-digit random system id + self.__id = "".join( + secrets.choice(string.ascii_uppercase + string.digits) for i in range(8) + ) + + # Handle user-defined options dictionary values + # reformat dictionary + options = {str(k).strip(): v for k, v in options.items()} + # handle bidirectional mode + if "bidirectional_mode" in options: + value = options["bidirectional_mode"] + # also check if pattern and source is valid + if isinstance(value, bool) and pattern < 2 and source is None: + # activate Bidirectional mode if specified + self.__bi_mode = value + else: + # otherwise disable it + self.__bi_mode = False + logger.warning("Bidirectional data transmission is disabled!") + # handle errors and logging + if pattern >= 2: + # raise error + raise ValueError( + "[NetGear_Async:ERROR] :: `{}` pattern is not valid when Bidirectional Mode is enabled. Kindly refer Docs for more Information!".format( + pattern + ) + ) + elif not (source is None): + raise ValueError( + "[NetGear_Async:ERROR] :: Custom source must be used when Bidirectional Mode is enabled. Kindly refer Docs for more Information!".format( + pattern + ) + ) + elif isinstance(value, bool) and self.__logging: + # log Bidirectional mode activation + logger.debug( + "Bidirectional Data Transmission is {} for this connection!".format( + "enabled" if value else "disabled" + ) + ) + else: + logger.error("`bidirectional_mode` value is invalid!") + # clean + del options["bidirectional_mode"] + # define messaging asynchronous Context self.__msg_context = zmq.asyncio.Context() @@ -240,7 +292,9 @@ def __init__( # Retrieve event loop and assign it self.loop = asyncio.get_event_loop() - + # create asyncio queue if bidirectional mode activated + self.__queue = asyncio.Queue(loop=self.loop) if self.__bi_mode else None + # log eventloop for debugging if self.__logging: # debugging logger.info( @@ -256,25 +310,23 @@ def launch(self): # check if receive mode enabled if self.__receive_mode: if self.__logging: - logger.debug("Launching NetGear asynchronous generator!") + logger.debug("Launching NetGear_Async asynchronous generator!") # run loop executor for Receiver asynchronous generator self.loop.run_in_executor(None, self.recv_generator) - # return instance - return self else: # Otherwise launch Server handler if self.__logging: - logger.debug("Creating NetGear asynchronous server handler!") + logger.debug("Creating NetGear_Async asynchronous server handler!") # create task for Server Handler self.task = asyncio.ensure_future(self.__server_handler(), loop=self.loop) - # return instance - return self + # return instance + return self async def __server_handler(self): """ Handles various Server-end processes/tasks. """ - # validate assigned frame generator in NetGear configuration + # validate assigned frame generator in NetGear_Async configuration if isinstance(self.config, dict) and "generator" in self.config: # check if its assigned value is a asynchronous generator if self.config["generator"] is None or not inspect.isasyncgen( @@ -287,7 +339,7 @@ async def __server_handler(self): else: # raise error if validation fails raise RuntimeError( - "[NetGear_Async:ERROR] :: Assigned NetGear configuration is invalid!" + "[NetGear_Async:ERROR] :: Assigned NetGear_Async configuration is invalid!" ) # define our messaging socket @@ -321,12 +373,16 @@ async def __server_handler(self): self.__msg_pattern, ) ) - logger.debug( - "Send Mode is successfully activated and ready to send data!" - ) + logger.critical( + "Send Mode is successfully activated and ready to send data!" + ) except Exception as e: # log ad raise error if failed logger.exception(str(e)) + if self.__bi_mode: + logger.error( + "Failed to activate Bidirectional Mode for this connection!" + ) raise ValueError( "[NetGear_Async:ERROR] :: Failed to connect address: {} and pattern: {}!".format( ( @@ -341,41 +397,96 @@ async def __server_handler(self): ) # loop over our Asynchronous frame generator - async for frame in self.config["generator"]: + async for dataframe in self.config["generator"]: + # extract data if Bidirection mode + if self.__bi_mode and len(dataframe) == 2: + (data, frame) = dataframe + if not (data is None) and isinstance(data, np.ndarray): + logger.warning( + "Skipped unsupported `data` of datatype: {}!".format( + type(data).__name__ + ) + ) + data = None + assert isinstance( + frame, np.ndarray + ), "[NetGear_Async:ERROR] :: Invalid data recieved from server end!" + elif self.__bi_mode: + # raise error for invaid data + raise ValueError( + "[NetGear_Async:ERROR] :: Send Mode only accepts tuple(data, frame) as input in Bidirectional Mode. \ + Kindly refer vidgear docs!" + ) + else: + # otherwise just make a copy of frame + frame = np.copy(dataframe) + # check if retrieved frame is `CONTIGUOUS` if not (frame.flags["C_CONTIGUOUS"]): # otherwise make it frame = np.ascontiguousarray(frame, dtype=frame.dtype) - # encode message - msg_enc = msgpack.packb(frame, default=m.encode) - # send it over network - await self.__msg_socket.send_multipart([msg_enc]) - # check if bidirectional patterns + + # create data dict + data_dict = dict( + terminate=False, + bi_mode=self.__bi_mode, + data=data if not (data is None) else "", + ) + # encode it + data_enc = msgpack.packb(data_dict) + # send the encoded data with correct flags + await self.__msg_socket.send(data_enc, flags=zmq.SNDMORE) + + # encode frame + frame_enc = msgpack.packb(frame, default=m.encode) + # send the encoded frame + await self.__msg_socket.send_multipart([frame_enc]) + + # check if bidirectional patterns used if self.__msg_pattern < 2: - # then receive and log confirmation - recv_confirmation = await self.__msg_socket.recv_multipart() - if self.__logging: - logger.debug(recv_confirmation) - - # send `exit` flag when done! - await self.__msg_socket.send_multipart([b"exit"]) - # check if bidirectional patterns - if self.__msg_pattern < 2: - # then receive and log confirmation - recv_confirmation = await self.__msg_socket.recv_multipart() - if self.__logging: - logger.debug(recv_confirmation) + # handle birectional data transfer if enabled + if self.__bi_mode: + # get reciever encoded message withing timeout limit + recvdmsg_encoded = await asyncio.wait_for( + self.__msg_socket.recv(), timeout=self.__timeout + ) + # retrieve reciever data from encoded message + recvd_data = msgpack.unpackb(recvdmsg_encoded, use_list=False) + # check message type + if recvd_data["return_type"] == "ndarray": # nummpy.ndarray + # get encoded frame from reciever + recvdframe_encooded = await asyncio.wait_for( + self.__msg_socket.recv_multipart(), timeout=self.__timeout + ) + # retrieve frame and put in queue + await self.__queue.put( + msgpack.unpackb( + recvdframe_encooded[0], + use_list=False, + object_hook=m.decode, + ) + ) + else: + # otherwise put data directly in queue + await self.__queue.put(recvd_data["return_data"]) + else: + # otherwise log recieved confirmation + recv_confirmation = await asyncio.wait_for( + self.__msg_socket.recv(), timeout=self.__timeout + ) + if self.__logging: + logger.debug(recv_confirmation) async def recv_generator(self): """ - A default Asynchronous Frame Generator for NetGear's Receiver-end. + A default Asynchronous Frame Generator for NetGear_Async's Receiver-end. """ # check whether `receive mode` is activated if not (self.__receive_mode): # raise Value error and exit self.__terminate = True raise ValueError( - "[NetGear:ERROR] :: `recv_generator()` function cannot be accessed while `receive_mode` is disabled. Kindly refer vidgear docs!" + "[NetGear_Async:ERROR] :: `recv_generator()` function cannot be accessed while `receive_mode` is disabled. Kindly refer vidgear docs!" ) # initialize and define messaging socket @@ -405,11 +516,15 @@ async def recv_generator(self): self.__msg_pattern, ) ) - logger.debug("Receive Mode is activated successfully!") + logger.critical("Receive Mode is activated successfully!") except Exception as e: logger.exception(str(e)) - raise ValueError( - "[NetGear:ERROR] :: Failed to bind address: {} and pattern: {}!".format( + if self.__bi_mode: + logger.error( + "Failed to activate Bidirectional Mode for this connection!" + ) + raise RuntimeError( + "[NetGear_Async:ERROR] :: Failed to bind address: {} and pattern: {}!".format( ( self.__protocol + "://" @@ -423,27 +538,107 @@ async def recv_generator(self): # loop until terminated while not self.__terminate: - # get message withing timeout limit - recvd_msg = await asyncio.wait_for( + # get encoded data message from server withing timeout limit + datamsg_encoded = await asyncio.wait_for( + self.__msg_socket.recv(), timeout=self.__timeout + ) + # retrieve data from message + data = msgpack.unpackb(datamsg_encoded, use_list=False) + + # terminate if exit` flag received from server + if data["terminate"]: + # send confirmation message to server if birectional patterns + if self.__msg_pattern < 2: + # create termination confirmation message + return_dict = dict( + terminated="Device-`{}` successfully terminated!".format( + self.__id + ), + ) + # encode message + retdata_enc = msgpack.packb(return_dict) + # send message back to server + await self.__msg_socket.send(retdata_enc) + # break loop + break + + # get encoded frame message from server withing timeout limit + framemsg_encoded = await asyncio.wait_for( self.__msg_socket.recv_multipart(), timeout=self.__timeout ) + # retrieve frame from message + frame = msgpack.unpackb( + framemsg_encoded[0], use_list=False, object_hook=m.decode + ) + # check if bidirectional patterns if self.__msg_pattern < 2: - # send confirmation - await self.__msg_socket.send_multipart([b"Message Received!"]) - # terminate if exit` flag received - if recvd_msg[0] == b"exit": - break - # retrieve frame from message - frame = msgpack.unpackb(recvd_msg[0], object_hook=m.decode) - # yield received frame - yield frame + # handle birectional data transfer if enabled + if self.__bi_mode and data["bi_mode"]: + # handle empty queue + if not self.__queue.empty(): + return_data = await self.__queue.get() + self.__queue.task_done() + else: + return_data = None + # check if we are returning `ndarray` frames + if not (return_data is None) and isinstance( + return_data, np.ndarray + ): + # check whether the incoming frame is contiguous + if not (return_data.flags["C_CONTIGUOUS"]): + return_data = np.ascontiguousarray( + return_data, dtype=return_data.dtype + ) + + # create return type dict without data + rettype_dict = dict( + return_type=(type(return_data).__name__), + return_data=None, + ) + # encode it + rettype_enc = msgpack.packb(rettype_dict) + # send it to server with correct flags + await self.__msg_socket.send(rettype_enc, flags=zmq.SNDMORE) + + # encode return ndarray data + retframe_enc = msgpack.packb(return_data, default=m.encode) + # send it over network to server + await self.__msg_socket.send_multipart([retframe_enc]) + else: + # otherwise create type and data dict + return_dict = dict( + return_type=(type(return_data).__name__), + return_data=return_data + if not (return_data is None) + else "", + ) + # encode it + retdata_enc = msgpack.packb(return_dict) + # send it over network to server + await self.__msg_socket.send(retdata_enc) + elif self.__bi_mode or data["bi_mode"]: + # raise error if bidirectional mode is disabled at server or client but not both + raise RuntimeError( + "[NetGear_Async:ERROR] :: Invalid configuration! Bidirectional Mode is not activate on {} end.".format( + "client" if self.__bi_mode else "server" + ) + ) + else: + # otherwise just send confirmation message to server + await self.__msg_socket.send( + bytes( + "Data received on device: {} !".format(self.__id), "utf-8" + ) + ) + # yield received tuple(data-frame) if birectional mode or else just frame + yield (data["data"], frame) if self.__bi_mode else frame # sleep for sometime await asyncio.sleep(0.00001) async def __frame_generator(self): """ - Returns a default frame-generator for NetGear's Server Handler. + Returns a default frame-generator for NetGear_Async's Server Handler. """ # start stream self.__stream.start() @@ -459,21 +654,34 @@ async def __frame_generator(self): # sleep for sometime await asyncio.sleep(0.00001) - def close(self, skip_loop=False): + async def transceive_data(self, data=None): """ - Terminates all NetGear Asynchronous processes gracefully. + Bidirectional Mode exculsive method to Transmit _(in Recieve mode)_ and Receive _(in Send mode)_ data in real-time. Parameters: - skip_loop (Boolean): (optional)used only if closing executor loop throws an error. + data (any): inputs data _(of any datatype)_ for sending back to Server. """ - # log termination - if self.__logging: - logger.debug( - "Terminating various {} Processes.".format( - "Receive Mode" if self.__receive_mode else "Send Mode" + recvd_data = None + if not self.__terminate: + if self.__bi_mode: + if self.__receive_mode: + await self.__queue.put(data) + elif not self.__receive_mode and not self.__queue.empty(): + recvd_data = await self.__queue.get() + self.__queue.task_done() + else: + pass + else: + logger.error( + "`transceive_data()` function cannot be used when Bidirectional Mode is disabled." ) - ) - # whether `receive_mode` is enabled or not + return recvd_data + + async def __terminate_connection(self): + """ + Internal asyncio method to safely terminate ZMQ connection and queues + """ + # check whether `receive_mode` is enabled or not if self.__receive_mode: # indicate that process should be terminated self.__terminate = True @@ -484,6 +692,56 @@ def close(self, skip_loop=False): if not (self.__stream is None): self.__stream.stop() + # signal `exit` flag for termination! + data_dict = dict(terminate=True) + data_enc = msgpack.packb(data_dict) + await self.__msg_socket.send(data_enc) + # check if bidirectional patterns + if self.__msg_pattern < 2: + # then receive and log confirmation + recv_confirmation = await asyncio.wait_for( + self.__msg_socket.recv(), timeout=self.__timeout + ) + recvd_conf = msgpack.unpackb(recv_confirmation, use_list=False) + if self.__logging and "terminated" in recvd_conf: + logger.debug(recvd_conf["terminated"]) + + # handle asyncio queues in bidirectional mode + if self.__bi_mode: + # empty queue if not + while not self.__queue.empty(): + try: + self.__queue.get_nowait() + except asyncio.QueueEmpty: + continue + self.__queue.task_done() + # join queues + await self.__queue.join() + + logger.critical( + "{} successfully terminated!".format( + "Receive Mode" if self.__receive_mode else "Send Mode" + ) + ) + + def close(self, skip_loop=False): + """ + Terminates all NetGear_Async Asynchronous processes gracefully. + + Parameters: + skip_loop (Boolean): (optional)used only if closing executor loop throws an error. + """ + # log termination + if self.__logging: + logger.debug( + "Terminating various {} Processes. Please wait...".format( + "Receive Mode" if self.__receive_mode else "Send Mode" + ) + ) + + # close connection gracefully + self.loop.run_until_complete(self.__terminate_connection()) + # close event loop if specified if not (skip_loop): self.loop.close() diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index ff5c471a5..aacad21b0 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -184,8 +184,8 @@ def __init__( # define Multi-Client mode self.__multiclient_mode = False # handles multi-client mode state - # define Bi-directional mode - self.__bi_mode = False # handles bi-directional mode state + # define Bidirectional mode + self.__bi_mode = False # handles Bidirectional mode state # define Secure mode valid_security_mech = {0: "Grasslands", 1: "StoneHouse", 2: "IronHouse"} @@ -227,13 +227,12 @@ def __init__( self.__request_timeout = 4000 # 4 secs # Handle user-defined options dictionary values - # reformat dictionary options = {str(k).strip(): v for k, v in options.items()} # loop over dictionary key & values and assign to global variables if valid for key, value in options.items(): - + # handle multi-server mode if key == "multiserver_mode" and isinstance(value, bool): # check if valid pattern assigned if pattern > 0: @@ -249,7 +248,8 @@ def __init__( ) ) - if key == "multiclient_mode" and isinstance(value, bool): + # handle multi-client mode + elif key == "multiclient_mode" and isinstance(value, bool): # check if valid pattern assigned if pattern > 0: # activate Multi-client mode @@ -264,6 +264,23 @@ def __init__( ) ) + # handle bidirectional mode + elif key == "bidirectional_mode" and isinstance(value, bool): + # check if pattern is valid + if pattern < 2: + # activate Bidirectional mode if specified + self.__bi_mode = value + else: + # otherwise disable it and raise error + self.__bi_mode = False + logger.warning("Bidirectional data transmission is disabled!") + raise ValueError( + "[NetGear:ERROR] :: `{}` pattern is not valid when Bidirectional Mode is enabled. Kindly refer Docs for more Information!".format( + pattern + ) + ) + + # handle secure mode elif ( key == "secure_mode" and isinstance(value, int) @@ -289,26 +306,11 @@ def __init__( ), "[NetGear:ERROR] :: Permission Denied!, cannot write ZMQ authentication certificates to '{}' directory!".format( value ) - elif key == "overwrite_cert" and isinstance(value, bool): # enable/disable auth certificate overwriting in secure mode overwrite_cert = value - elif key == "bidirectional_mode" and isinstance(value, bool): - # check if pattern is valid - if pattern < 2: - # activate bi-directional mode if specified - self.__bi_mode = value - else: - # otherwise disable it and raise error - self.__bi_mode = False - logger.critical("Bi-Directional data transmission is disabled!") - raise ValueError( - "[NetGear:ERROR] :: `{}` pattern is not valid when Bi-Directional Mode is enabled. Kindly refer Docs for more Information!".format( - pattern - ) - ) - + # handle ssh-tunneling mode elif key == "ssh_tunnel_mode" and isinstance(value, str): # enable SSH Tunneling Mode self.__ssh_tunnel_mode = value.strip() @@ -325,6 +327,7 @@ def __init__( ) ) + # handle jpeg compression elif key == "jpeg_compression" and isinstance(value, (bool, str)): if isinstance(value, str) and value.strip().upper() in [ "RGB", @@ -373,7 +376,7 @@ def __init__( else: logger.warning("Invalid `request_timeout` value skipped!") - # assign ZMQ flags + # handle ZMQ flags elif key == "flag" and isinstance(value, int): self.__msg_flag = value elif key == "copy" and isinstance(value, bool): @@ -483,12 +486,12 @@ def __init__( "[NetGear:ERROR] :: Multi-Client and Multi-Server Mode cannot be enabled simultaneously!" ) elif self.__multiserver_mode or self.__multiclient_mode: - # check if Bi-directional Mode also enabled + # check if Bidirectional Mode also enabled if self.__bi_mode: # disable bi_mode if enabled self.__bi_mode = False logger.warning( - "Bi-Directional Data Transmission is disabled when {} Mode is Enabled due to incompatibility!".format( + "Bidirectional Data Transmission is disabled when {} Mode is Enabled due to incompatibility!".format( "Multi-Server" if self.__multiserver_mode else "Multi-Client" ) ) @@ -501,13 +504,13 @@ def __init__( ) ) elif self.__bi_mode: - # log Bi-directional mode activation + # log Bidirectional mode activation if self.__logging: logger.debug( - "Bi-Directional Data Transmission is enabled for this connection!" + "Bidirectional Data Transmission is enabled for this connection!" ) elif self.__ssh_tunnel_mode: - # log Bi-directional mode activation + # log Bidirectional mode activation if self.__logging: logger.debug( "SSH Tunneling is enabled for host:`{}` with `{}` back-end.".format( @@ -669,7 +672,7 @@ def __init__( else: if self.__bi_mode: logger.critical( - "Failed to activate Bi-Directional Mode for this connection!" + "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( @@ -887,7 +890,7 @@ def __init__( else: if self.__bi_mode: logger.critical( - "Failed to activate Bi-Directional Mode for this connection!" + "Failed to activate Bidirectional Mode for this connection!" ) if self.__ssh_tunnel_mode: logger.critical( @@ -1041,7 +1044,7 @@ def __recv_handler(self): self.__return_data, np.ndarray ): # handle return data for compression - return_data = self.__return_data[:] + return_data = np.copy(self.__return_data) # check whether exit_flag is False if not (return_data.flags["C_CONTIGUOUS"]): @@ -1164,7 +1167,7 @@ def __recv_handler(self): else: # append recovered unique port and frame to queue self.__queue.append((msg_json["port"], frame)) - # extract if any message from server if Bi-Directional Mode is enabled + # extract if any message from server if Bidirectional Mode is enabled elif self.__bi_mode: if msg_json["message"]: # append grouped frame and data to queue @@ -1193,7 +1196,7 @@ def recv(self, return_data=None): "[NetGear:ERROR] :: `recv()` function cannot be used while receive_mode is disabled. Kindly refer vidgear docs!" ) - # handle bi-directional return data + # handle Bidirectional return data if (self.__bi_mode or self.__multiclient_mode) and not (return_data is None): self.__return_data = return_data @@ -1298,7 +1301,7 @@ def send(self, frame, message=None): # check if synchronous patterns, then wait for confirmation if self.__pattern < 2: - # check if bi-directional data transmission is enabled + # check if Bidirectional data transmission is enabled if self.__bi_mode or self.__multiclient_mode: # handles return data From ec5587e3d3351813314a28b84b2c3c966abd8cbf Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 16 Aug 2021 21:56:28 +0530 Subject: [PATCH 097/112] =?UTF-8?q?=F0=9F=92=A5=20Asyncio:=20Changed=20`as?= =?UTF-8?q?yncio.sleep`=20value=20to=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 💥 The amount of time sleep is irrelevant; the only purpose await asyncio.sleep() serves is to force asyncio to suspend execution to the event loop, and give other tasks a chance to run. Also, `await asyncio.sleep(0)` will achieve the same effect. https://stackoverflow.com/a/55782965/10158117 - 🚸 Added warning that JPEG Frame-Compression is disabled with Custom Source in WebGear. - ✏️ Fixed links in docs. --- docs/gears/netgear_async/usage.md | 10 +++--- docs/gears/webgear/advanced.md | 6 ++-- vidgear/gears/asyncio/netgear_async.py | 6 ++-- vidgear/gears/asyncio/webgear.py | 32 ++++++++++++------- .../asyncio_tests/test_netgear_async.py | 4 +-- .../asyncio_tests/test_webgear.py | 2 +- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index e55775814..5a0fd40a6 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -100,7 +100,7 @@ async def main(): key = cv2.waitKey(1) & 0xFF # await before continuing - await asyncio.sleep(0.00001) + await asyncio.sleep(0) if __name__ == "__main__": @@ -162,7 +162,7 @@ async def main(): key = cv2.waitKey(1) & 0xFF # await before continuing - await asyncio.sleep(0.00001) + await asyncio.sleep(0) if __name__ == "__main__": @@ -263,7 +263,7 @@ async def my_frame_generator(): # yield frame yield frame # sleep for sometime - await asyncio.sleep(0.00001) + await asyncio.sleep(0) if __name__ == "__main__": @@ -313,7 +313,7 @@ async def main(): key = cv2.waitKey(1) & 0xFF # await before continuing - await asyncio.sleep(0.01) + await asyncio.sleep(0) if __name__ == "__main__": @@ -404,7 +404,7 @@ async def main(): key = cv2.waitKey(1) & 0xFF # await before continuing - await asyncio.sleep(0.00001) + await asyncio.sleep(0) if __name__ == "__main__": diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index d15340041..a29751622 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -28,7 +28,7 @@ limitations under the License. ### Using WebGear with Variable Colorspace -WebGear by default only supports "BGR" colorspace frames as input, but you can use [`jpeg_compression_colorspace`](../params/#webgear_rtc-specific-attributes) string attribute through its options dictionary parameter to specify incoming frames colorspace. +WebGear by default only supports "BGR" colorspace frames as input, but you can use [`jpeg_compression_colorspace`](../params/#webgear-specific-attributes) string attribute through its options dictionary parameter to specify incoming frames colorspace. Let's implement a bare-minimum example using WebGear, where we will be sending [**GRAY**](https://en.wikipedia.org/wiki/Grayscale) frames to client browser: @@ -76,6 +76,8 @@ web.shutdown() WebGear allows you to easily define your own custom Source that you want to use to manipulate your frames before sending them onto the browser. +!!! warning "JPEG Frame-Compression and all of its [performance enhancing attributes](../usage/#performance-enhancements) are disabled with a Custom Source!" + Let's implement a bare-minimum example with a Custom Source using WebGear API and OpenCV: @@ -111,7 +113,7 @@ async def my_frame_producer(): encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() # yield frame in byte format yield (b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n") - await asyncio.sleep(0.0000001) + await asyncio.sleep(0) # close stream stream.release() diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index 29f3feca9..3a8427241 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -631,10 +631,10 @@ async def recv_generator(self): "Data received on device: {} !".format(self.__id), "utf-8" ) ) - # yield received tuple(data-frame) if birectional mode or else just frame + # yield received tuple(data-frame) if bidirectional mode or else just frame yield (data["data"], frame) if self.__bi_mode else frame # sleep for sometime - await asyncio.sleep(0.00001) + await asyncio.sleep(0) async def __frame_generator(self): """ @@ -652,7 +652,7 @@ async def __frame_generator(self): # yield frame yield frame # sleep for sometime - await asyncio.sleep(0.00001) + await asyncio.sleep(0) async def transceive_data(self, data=None): """ diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index da38cec0b..bf7f2b2a6 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -243,14 +243,6 @@ def __init__( data_path ) ) - logger.debug( - "Enabling JPEG Frame-Compression 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", - ) - ) # define Jinja2 templates handler self.__templates = Jinja2Templates(directory="{}/templates".format(data_path)) @@ -273,8 +265,6 @@ def __init__( if source is None: self.config = {"generator": None} self.__stream = None - if self.__logging: - logger.warning("Given source is of NoneType!") else: # define stream with necessary params self.__stream = VideoGear( @@ -293,6 +283,25 @@ def __init__( ) # define default frame generator in configuration self.config = {"generator": self.__producer} + + # log if specified + if self.__logging: + if source is None: + logger.warning( + "Given source is of NoneType. Therefore, JPEG Frame-Compression is disabled!" + ) + else: + logger.debug( + "Enabling JPEG Frame-Compression 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", + ) + ) + # copying original routing tables for further validation self.__rt_org_copy = self.routes[:] # initialize blank frame @@ -408,7 +417,8 @@ async def __producer(self): yield ( b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n" ) - # await asyncio.sleep(0.00000001) + # sleep for sometime. + await asyncio.sleep(0) async def __video(self, scope): """ 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 9f84f3209..b748274a1 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -65,7 +65,7 @@ async def custom_frame_generator(): # yield frame yield frame # sleep for sometime - await asyncio.sleep(0.000001) + await asyncio.sleep(0) # close stream stream.release() @@ -77,7 +77,7 @@ async def client_iterator(client): # test frame validity assert not (frame is None or np.shape(frame) == ()), "Failed Test" # await before continuing - await asyncio.sleep(0.000001) + await asyncio.sleep(0) @pytest.fixture diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index 386c8613d..de46afbc4 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -74,7 +74,7 @@ async def custom_frame_generator(): encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() # yield frame in byte format yield (b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n") - await asyncio.sleep(0.00001) + await asyncio.sleep(0) # close stream stream.release() From 9a8d8d836b0443a723d5e80ee6777a1f51363b76 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 17 Aug 2021 20:12:23 +0530 Subject: [PATCH 098/112] =?UTF-8?q?=F0=9F=91=B7=20NetGear=5FAsync:=20Added?= =?UTF-8?q?=20complete=20CI=20tests=20for=20Bidirectional=20Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 👷 Added complete CI tests for new Bidirectional Mode. - ✨ Implemented new exclusive `Custom_Generator` class for testing bidirectional data dynamically on server-end. - ✨ Implemented new exclusive `client_dataframe_iterator` method for testing bidirectional data on client-end. - ✨ Implemented `test_netgear_async_options` and `test_netgear_async_bidirectionalmode` two new tests. - 🎨 Added missing socket termination in both server and client end. - 🎨 Removed deprecated `loop` parameter from asyncio methods. - 🎨 Re-implemented `skip_loop` parameter in `close()` method. - 💥 `run_until_complete` will not used if `skip_loop` is enabled. - ✨ Added new `disable_confirmation` used to force disable termination confirmation from client in `terminate_connection`. - 💥 `skip_loop` now will create asyncio task instead and will enable `disable_confirmation` by default. - 🚑️ Fixed several critical bugs in event loop handling. - 🐛 Fixed several bugs in bidirectional mode implementation. - 🐛 Fixed several bugs in new CI tests. - 💚 Added `timeout` value on server end in CI tests. - 📝 Added `stream.release()` missing in docs. - ✏️ Fixed several typos in code comments. - 🔊 Updated logging messages. --- docs/gears/netgear_async/usage.md | 4 +- vidgear/gears/asyncio/netgear_async.py | 108 +++++---- .../asyncio_tests/test_netgear_async.py | 211 ++++++++++++++++-- 3 files changed, 258 insertions(+), 65 deletions(-) diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index 5a0fd40a6..a1102d1b7 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -255,7 +255,6 @@ async def my_frame_generator(): # check if frame empty if not grabbed: - # if True break the infinite loop break # do something with the frame to be sent here @@ -264,6 +263,9 @@ async def my_frame_generator(): yield frame # sleep for sometime await asyncio.sleep(0) + + # close stream + stream.release() if __name__ == "__main__": diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index 3a8427241..dd66b2160 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -226,7 +226,7 @@ def __init__( # check whether `Receive Mode` is enabled if receive_mode: - # assign local ip address if None + # assign local IP address if None if address is None: self.__address = "*" # define address else: @@ -293,7 +293,7 @@ def __init__( # Retrieve event loop and assign it self.loop = asyncio.get_event_loop() # create asyncio queue if bidirectional mode activated - self.__queue = asyncio.Queue(loop=self.loop) if self.__bi_mode else None + self.__queue = asyncio.Queue() if self.__bi_mode else None # log eventloop for debugging if self.__logging: # debugging @@ -318,7 +318,7 @@ def launch(self): if self.__logging: logger.debug("Creating NetGear_Async asynchronous server handler!") # create task for Server Handler - self.task = asyncio.ensure_future(self.__server_handler(), loop=self.loop) + self.task = asyncio.ensure_future(self.__server_handler()) # return instance return self @@ -398,7 +398,7 @@ async def __server_handler(self): # loop over our Asynchronous frame generator async for dataframe in self.config["generator"]: - # extract data if Bidirection mode + # extract data if bidirectional mode if self.__bi_mode and len(dataframe) == 2: (data, frame) = dataframe if not (data is None) and isinstance(data, np.ndarray): @@ -410,9 +410,9 @@ async def __server_handler(self): data = None assert isinstance( frame, np.ndarray - ), "[NetGear_Async:ERROR] :: Invalid data recieved from server end!" + ), "[NetGear_Async:ERROR] :: Invalid data received from server end!" elif self.__bi_mode: - # raise error for invaid data + # raise error for invalid data raise ValueError( "[NetGear_Async:ERROR] :: Send Mode only accepts tuple(data, frame) as input in Bidirectional Mode. \ Kindly refer vidgear docs!" @@ -420,6 +420,7 @@ async def __server_handler(self): else: # otherwise just make a copy of frame frame = np.copy(dataframe) + data = None # check if retrieved frame is `CONTIGUOUS` if not (frame.flags["C_CONTIGUOUS"]): @@ -444,33 +445,37 @@ async def __server_handler(self): # check if bidirectional patterns used if self.__msg_pattern < 2: - # handle birectional data transfer if enabled + # handle bidirectional data transfer if enabled if self.__bi_mode: - # get reciever encoded message withing timeout limit + # get receiver encoded message withing timeout limit recvdmsg_encoded = await asyncio.wait_for( self.__msg_socket.recv(), timeout=self.__timeout ) - # retrieve reciever data from encoded message + # retrieve receiver data from encoded message recvd_data = msgpack.unpackb(recvdmsg_encoded, use_list=False) # check message type - if recvd_data["return_type"] == "ndarray": # nummpy.ndarray - # get encoded frame from reciever - recvdframe_encooded = await asyncio.wait_for( + if recvd_data["return_type"] == "ndarray": # numpy.ndarray + # get encoded frame from receiver + recvdframe_encoded = await asyncio.wait_for( self.__msg_socket.recv_multipart(), timeout=self.__timeout ) # retrieve frame and put in queue await self.__queue.put( msgpack.unpackb( - recvdframe_encooded[0], + recvdframe_encoded[0], use_list=False, object_hook=m.decode, ) ) else: # otherwise put data directly in queue - await self.__queue.put(recvd_data["return_data"]) + await self.__queue.put( + recvd_data["return_data"] + if recvd_data["return_data"] + else None + ) else: - # otherwise log recieved confirmation + # otherwise log received confirmation recv_confirmation = await asyncio.wait_for( self.__msg_socket.recv(), timeout=self.__timeout ) @@ -505,7 +510,7 @@ async def recv_generator(self): # finally log progress if self.__logging: logger.debug( - "Successfully Binded to address: {} with pattern: {}.".format( + "Successfully binded to address: {} with pattern: {}.".format( ( self.__protocol + "://" @@ -519,12 +524,8 @@ async def recv_generator(self): logger.critical("Receive Mode is activated successfully!") except Exception as e: logger.exception(str(e)) - if self.__bi_mode: - logger.error( - "Failed to activate Bidirectional Mode for this connection!" - ) raise RuntimeError( - "[NetGear_Async:ERROR] :: Failed to bind address: {} and pattern: {}!".format( + "[NetGear_Async:ERROR] :: Failed to bind address: {} and pattern: {}{}!".format( ( self.__protocol + "://" @@ -533,6 +534,7 @@ async def recv_generator(self): + str(self.__port) ), self.__msg_pattern, + " and Bidirectional Mode enabled" if self.__bi_mode else "", ) ) @@ -544,14 +546,13 @@ async def recv_generator(self): ) # retrieve data from message data = msgpack.unpackb(datamsg_encoded, use_list=False) - # terminate if exit` flag received from server if data["terminate"]: - # send confirmation message to server if birectional patterns + # send confirmation message to server if bidirectional patterns if self.__msg_pattern < 2: # create termination confirmation message return_dict = dict( - terminated="Device-`{}` successfully terminated!".format( + terminated="Client-`{}` successfully terminated!".format( self.__id ), ) @@ -559,9 +560,11 @@ async def recv_generator(self): retdata_enc = msgpack.packb(return_dict) # send message back to server await self.__msg_socket.send(retdata_enc) - # break loop + if self.__logging: + logger.info("Termination signal received from server!") + # break loop and terminate + self.__terminate = True break - # get encoded frame message from server withing timeout limit framemsg_encoded = await asyncio.wait_for( self.__msg_socket.recv_multipart(), timeout=self.__timeout @@ -573,7 +576,7 @@ async def recv_generator(self): # check if bidirectional patterns if self.__msg_pattern < 2: - # handle birectional data transfer if enabled + # handle bidirectional data transfer if enabled if self.__bi_mode and data["bi_mode"]: # handle empty queue if not self.__queue.empty(): @@ -628,11 +631,14 @@ async def recv_generator(self): # otherwise just send confirmation message to server await self.__msg_socket.send( bytes( - "Data received on device: {} !".format(self.__id), "utf-8" + "Data received on client: {} !".format(self.__id), "utf-8" ) ) # yield received tuple(data-frame) if bidirectional mode or else just frame - yield (data["data"], frame) if self.__bi_mode else frame + if self.__bi_mode: + yield (data["data"], frame) if data["data"] else (None, frame) + else: + yield frame # sleep for sometime await asyncio.sleep(0) @@ -656,7 +662,7 @@ async def __frame_generator(self): async def transceive_data(self, data=None): """ - Bidirectional Mode exculsive method to Transmit _(in Recieve mode)_ and Receive _(in Send mode)_ data in real-time. + Bidirectional Mode exclusive method to Transmit _(in Receive mode)_ and Receive _(in Send mode)_ data in real-time. Parameters: data (any): inputs data _(of any datatype)_ for sending back to Server. @@ -677,10 +683,21 @@ async def transceive_data(self, data=None): ) return recvd_data - async def __terminate_connection(self): + async def __terminate_connection(self, disable_confirmation=False): """ Internal asyncio method to safely terminate ZMQ connection and queues + + Parameters: + disable_confirmation (boolean): Force disable termination confirmation from client in bidirectional patterns. """ + # log termination + if self.__logging: + logger.debug( + "Terminating various {} Processes. Please wait.".format( + "Receive Mode" if self.__receive_mode else "Send Mode" + ) + ) + # check whether `receive_mode` is enabled or not if self.__receive_mode: # indicate that process should be terminated @@ -691,21 +708,20 @@ async def __terminate_connection(self): # terminate stream if not (self.__stream is None): self.__stream.stop() - # signal `exit` flag for termination! data_dict = dict(terminate=True) data_enc = msgpack.packb(data_dict) await self.__msg_socket.send(data_enc) # check if bidirectional patterns - if self.__msg_pattern < 2: + if self.__msg_pattern < 2 and not disable_confirmation: # then receive and log confirmation - recv_confirmation = await asyncio.wait_for( - self.__msg_socket.recv(), timeout=self.__timeout - ) + recv_confirmation = await self.__msg_socket.recv() recvd_conf = msgpack.unpackb(recv_confirmation, use_list=False) if self.__logging and "terminated" in recvd_conf: logger.debug(recvd_conf["terminated"]) - + # close socket + self.__msg_socket.setsockopt(zmq.LINGER, 0) + self.__msg_socket.close() # handle asyncio queues in bidirectional mode if self.__bi_mode: # empty queue if not @@ -729,19 +745,13 @@ def close(self, skip_loop=False): Terminates all NetGear_Async Asynchronous processes gracefully. Parameters: - skip_loop (Boolean): (optional)used only if closing executor loop throws an error. + skip_loop (Boolean): (optional)used only if don't want to close eventloop(required in pytest). """ - # log termination - if self.__logging: - logger.debug( - "Terminating various {} Processes. Please wait...".format( - "Receive Mode" if self.__receive_mode else "Send Mode" - ) - ) - - # close connection gracefully - self.loop.run_until_complete(self.__terminate_connection()) - # close event loop if specified if not (skip_loop): + # close connection gracefully + self.loop.run_until_complete(self.__terminate_connection()) self.loop.close() + else: + # otherwise create a task + asyncio.create_task(self.__terminate_connection(disable_confirmation=True)) 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 b748274a1..b1f1f987d 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -41,6 +41,14 @@ 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() + + def return_testvideo_path(): """ returns Test Video path @@ -66,10 +74,53 @@ async def custom_frame_generator(): yield frame # sleep for sometime await asyncio.sleep(0) + # close stream stream.release() +class Custom_Generator: + """ + Custom Generator using OpenCV, for testing bidirectional mode. + """ + + def __init__(self, server=None, data=""): + # initialize global params + assert not (server is None), "Invalid Value" + # assign server + self.server = server + # data + self.data = data + + # Create a async data and frame generator as custom source + async def custom_dataframe_generator(self): + # loop over stream until its terminated + stream = cv2.VideoCapture(return_testvideo_path()) + while True: + # read frames + (grabbed, frame) = stream.read() + + # check if frame empty + if not grabbed: + break + + # recieve client's data + recv_data = await self.server.transceive_data() + if not (recv_data is None): + if isinstance(recv_data, np.ndarray): + assert not ( + recv_data is None or np.shape(recv_data) == () + ), "Failed Test" + else: + logger.debug(recv_data) + + # yield data and frame + yield (self.data, frame) + # sleep for sometime + await asyncio.sleep(0) + stream.release() + + # Create a async function where you want to show/manipulate your received frames async def client_iterator(client): # loop over Client's Asynchronous Frame Generator @@ -80,12 +131,22 @@ async def client_iterator(client): await asyncio.sleep(0) -@pytest.fixture -def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = asyncio.SelectorEventLoop() - yield loop - loop.close() +# 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(): + if not (recvd_data is None): + # {do something with received server recv_data here} + logger.debug(recvd_data) + + # {do something with received frames here} + + # test frame validity + assert not (frame is None or np.shape(frame) == ()), "Failed Test" + # send data + await client.transceive_data(data=data) + # await before continuing + await asyncio.sleep(0) @pytest.mark.asyncio @@ -103,6 +164,7 @@ async def test_netgear_async_playback(pattern): server = NetGear_Async( source=return_testvideo_path(), pattern=pattern, + timeout=7.0, logging=True, **options_gear ).launch() @@ -128,7 +190,9 @@ async def test_netgear_async_playback(pattern): @pytest.mark.parametrize("generator, result", test_data_class) async def test_netgear_async_custom_server_generator(generator, result): try: - server = NetGear_Async(protocol="udp", logging=True) # invalid protocol + server = NetGear_Async( + protocol="udp", timeout=5.0, logging=True + ) # invalid protocol server.config["generator"] = generator server.launch() # define and launch Client with `receive_mode = True` and timeout = 5.0 @@ -139,10 +203,75 @@ async def test_netgear_async_custom_server_generator(generator, result): except Exception as e: if result: pytest.fail(str(e)) + else: + pytest.xfail(str(e)) finally: + server.close(skip_loop=True) + client.close(skip_loop=True) + + +test_data_class = [ + ( + custom_frame_generator(), + "Hi", + {"bidirectional_mode": True}, + {"bidirectional_mode": True}, + False, + ), + ( + [], + 444404444, + {"bidirectional_mode": True}, + {"bidirectional_mode": False}, + False, + ), + ( + [], + [1, "string", ["list"]], + {"bidirectional_mode": True}, + {"bidirectional_mode": True}, + True, + ), + ( + [], + (np.random.random(size=(480, 640, 3)) * 255).astype(np.uint8), + {"bidirectional_mode": True}, + {"bidirectional_mode": True}, + True, + ), +] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "generator, data, options_server, options_client, result", + test_data_class, +) +async def test_netgear_async_bidirectionalmode( + generator, data, options_server, options_client, result +): + try: + server = NetGear_Async(logging=True, timeout=5.0, **options_server) + if not generator: + cg = Custom_Generator(server, data=data) + generator = cg.custom_dataframe_generator() + server.config["generator"] = generator + server.launch() + # define and launch Client with `receive_mode = True` and timeout = 5.0 + client = NetGear_Async( + logging=True, receive_mode=True, timeout=5.0, **options_client + ).launch() + # gather and run tasks + input_coroutines = [server.task, client_dataframe_iterator(client, data=data)] + res = await asyncio.gather(*input_coroutines, return_exceptions=True) + except Exception as e: if result: - server.close(skip_loop=True) - client.close(skip_loop=True) + pytest.fail(str(e)) + else: + pytest.xfail(str(e)) + finally: + server.close(skip_loop=True) + client.close(skip_loop=True) @pytest.mark.asyncio @@ -159,6 +288,7 @@ async def test_netgear_async_addresses(address, port): source=return_testvideo_path(), address=address, port=port, + timeout=5.0, logging=True, **options_gear ).launch() @@ -179,10 +309,61 @@ async def test_netgear_async_addresses(address, port): @pytest.mark.asyncio -@pytest.mark.xfail(raises=ValueError) async def test_netgear_async_recv_generator(): - # define and launch server - server = NetGear_Async(source=return_testvideo_path(), logging=True) - async for frame in server.recv_generator(): - logger.error("Failed") - server.close(skip_loop=True) + server = None + try: + # define and launch server + server = NetGear_Async( + source=return_testvideo_path(), timeout=5.0, logging=True + ) + async for frame in server.recv_generator(): + logger.warning("Failed") + except Exception as e: + if isinstance(e, (ValueError, asyncio.exceptions.TimeoutError)): + pytest.xfail(str(e)) + else: + pytest.fail(str(e)) + finally: + if not (server is None): + server.close(skip_loop=True) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "pattern, options", + [ + (0, {"bidirectional_mode": True}), + (0, {"bidirectional_mode": False}), + (1, {"bidirectional_mode": "invalid"}), + (2, {"bidirectional_mode": True}), + ], +) +async def test_netgear_async_options(pattern, options): + client = None + try: + # define and launch server + client = NetGear_Async( + source=None + if options["bidirectional_mode"] != True + else return_testvideo_path(), + receive_mode=True, + timeout=5.0, + pattern=pattern, + logging=True, + **options + ) + async for frame in client.recv_generator(): + if not options["bidirectional_mode"]: + # create target data + target_data = "Client here." + # send it + await client.transceive_data(data=target_data) + logger.warning("Failed") + except Exception as e: + if isinstance(e, (ValueError, asyncio.exceptions.TimeoutError)): + pytest.xfail(str(e)) + else: + pytest.fail(str(e)) + finally: + if not (client is None): + client.close(skip_loop=True) From 9f05a2ca37d1868d461dfc5be263e288445d2304 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 17 Aug 2021 20:42:47 +0530 Subject: [PATCH 099/112] =?UTF-8?q?=F0=9F=92=9A=20CI:=20Fixed=20typo=20in?= =?UTF-8?q?=20`TimeoutError`=20exception=20import.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/network_tests/asyncio_tests/test_netgear_async.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 b1f1f987d..3d0e77d68 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -319,7 +319,7 @@ async def test_netgear_async_recv_generator(): async for frame in server.recv_generator(): logger.warning("Failed") except Exception as e: - if isinstance(e, (ValueError, asyncio.exceptions.TimeoutError)): + if isinstance(e, (ValueError, asyncio.TimeoutError)): pytest.xfail(str(e)) else: pytest.fail(str(e)) @@ -360,7 +360,7 @@ async def test_netgear_async_options(pattern, options): await client.transceive_data(data=target_data) logger.warning("Failed") except Exception as e: - if isinstance(e, (ValueError, asyncio.exceptions.TimeoutError)): + if isinstance(e, (ValueError, asyncio.TimeoutError)): pytest.xfail(str(e)) else: pytest.fail(str(e)) From 009412fedd92a4598f27e007bd0cea1f5b1952f5 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 17 Aug 2021 21:36:52 +0530 Subject: [PATCH 100/112] =?UTF-8?q?=F0=9F=9A=91=EF=B8=8F=20NetGear=5FAsync?= =?UTF-8?q?:=20Replaced=20`create=5Ftask`=20with=20`ensure=5Ffuture`=20to?= =?UTF-8?q?=20ensure=20backward=20compatibility=20with=20python-3.6=20lega?= =?UTF-8?q?cies.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vidgear/gears/asyncio/netgear_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index dd66b2160..97684cb70 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -754,4 +754,4 @@ def close(self, skip_loop=False): self.loop.close() else: # otherwise create a task - asyncio.create_task(self.__terminate_connection(disable_confirmation=True)) + asyncio.ensure_future(self.__terminate_connection(disable_confirmation=True)) From 466f49a42075b49d8e9d7b288232d00c74794289 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 22 Aug 2021 07:46:07 +0530 Subject: [PATCH 101/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20usage=20?= =?UTF-8?q?examples=20for=20NetGear=5FAsync's=20Bidirectional=20Mode.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Added new Usage examples and Reference doc for NetGear_Async's Bidirectional Mode. - 🍱 Added new image asset for NetGear_Async's Bidirectional Mode. - 🚩 Added NetGear_Async's `option` parameter reference. - 📝 Updated NetGear_Async definition in docs. - 💄 Changed font size for Helper methods. - 📝 Renamed `Bonus` to `References` in mkdocs.yml - 📝 Added missing helper methods in Reference. - 📝 Added more content to docs. - 🩹 Fixed redundant properties in CSS - 🐛 Fixed bugs in mkdocs.yml - ✏️ Fixed typos and context. - 💡 Updated Code Comments. - NetGear_Async: - 🎨 Simplified code for `transceive_data` method. - 🐛 Fixed `timeout` parameter logic. - CI: ☂️ Bumped CodeCov. --- README.md | 12 +- docs/bonus/reference/helper.md | 31 +- docs/bonus/reference/helper_async.md | 6 +- .../netgear/advanced/bidirectional_mode.md | 1 + .../advanced/bidirectional_mode.md | 726 ++++++++++++++++++ docs/gears/netgear_async/overview.md | 6 +- docs/gears/netgear_async/params.md | 28 + docs/gears/netgear_async/usage.md | 6 +- docs/gears/streamgear/rtfm/usage.md | 8 +- docs/overrides/assets/images/bidir_async.png | Bin 0 -> 51563 bytes docs/overrides/assets/stylesheets/custom.css | 5 +- docs/switch_from_cv.md | 8 +- mkdocs.yml | 10 +- vidgear/gears/asyncio/helper.py | 18 +- vidgear/gears/asyncio/netgear_async.py | 24 +- vidgear/gears/helper.py | 60 +- .../asyncio_tests/test_netgear_async.py | 39 +- 17 files changed, 906 insertions(+), 82 deletions(-) create mode 100644 docs/gears/netgear_async/advanced/bidirectional_mode.md create mode 100644 docs/overrides/assets/images/bidir_async.png diff --git a/README.md b/README.md index 38c71f46b..f4eadc415 100644 --- a/README.md +++ b/README.md @@ -472,7 +472,7 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast NetGear implements a high-level wrapper around [**PyZmQ**][pyzmq] python library that contains python bindings for [**ZeroMQ**][zmq] - a high-performance asynchronous distributed messaging library. -NetGear seamlessly supports [**Bidirectional data transmission**][netgear_bidata_doc] along with video-frames between receiver(client) and sender(server). +NetGear seamlessly supports additional [**bidirectional data transmission**][netgear_bidata_doc] between receiver(client) and sender(server) while transferring video-frames all in real-time. NetGear can also robustly handle [**Multiple Server-Systems**][netgear_multi_server_doc] and [**Multiple Client-Systems**][netgear_multi_client_doc] and at once, thereby providing access to a seamless exchange of video-frames & data between multiple devices across the network at the same time. @@ -611,11 +611,13 @@ web.shutdown()

. -> _NetGear_Async can generate the same performance as [NetGear API](#netgear) at about one-third the memory consumption, and also provide complete server-client handling with various options to use variable protocols/patterns similar to NetGear, but it doesn't support any of [NetGear's Exclusive Modes][netgear-exm] yet._ +> _NetGear_Async can generate the same performance as [NetGear API](#netgear) at about one-third the memory consumption, and also provide complete server-client handling with various options to use variable protocols/patterns similar to NetGear, but lacks in term of flexibility as it supports only a few [NetGear's Exclusive Modes][netgear-exm]._ NetGear_Async is built on [`zmq.asyncio`][asyncio-zmq], and powered by a high-performance asyncio event loop called [**`uvloop`**][uvloop] to achieve unmatchable high-speed and lag-free video streaming over the network with minimal resource constraints. NetGear_Async can transfer thousands of frames in just a few seconds without causing any significant load on your system. -NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](#netgear) but doesn't support any [NetGear Exclusive modes][netgear-exm] yet. Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc][netgear_Async-cs] example). +NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](#netgear). Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc][netgear_Async-cs] example). + +NetGear_Async now supports additional [**bidirectional data transmission**][btm_netgear_async] between receiver(client) and sender(server) while transferring video-frames. Users can easily build complex applications such as like [Real-Time Video Chat][rtvc] in just few lines of code. NetGear_Async as of now supports [all four ZeroMQ messaging patterns](#attributes-and-parameters-wrench): * [**`zmq.PAIR`**][zmq-pair] _(ZMQ Pair Pattern)_ @@ -708,7 +710,7 @@ Here is a Bibtex entry you can use to cite this project in a publication: # Copyright -**Copyright © abhiTronix 2019-2021** +**Copyright © abhiTronix 2019** This library is released under the **[Apache 2.0 License][license]**. @@ -747,6 +749,8 @@ Internal URLs [azure-pipeline]:https://dev.azure.com/abhiuna12/public/_build?definitionId=2 [app]:https://ci.appveyor.com/project/abhiTronix/vidgear [code]:https://codecov.io/gh/abhiTronix/vidgear +[btm_netgear_async]: https://abhitronix.github.io/vidgear/latest/gears/netgear_async/advanced/bidirectional_mode/ +[rtvc]: https://abhitronix.github.io/vidgear/latest/gears/netgear_async/advanced/bidirectional_mode/#using-bidirectional-mode-for-video-frames-transfer [test-4k]:https://github.com/abhiTronix/vidgear/blob/e0843720202b0921d1c26e2ce5b11fadefbec892/vidgear/tests/benchmark_tests/test_benchmark_playback.py#L65 [bs_script_dataset]:https://github.com/abhiTronix/vidgear/blob/testing/scripts/bash/prepare_dataset.sh diff --git a/docs/bonus/reference/helper.md b/docs/bonus/reference/helper.md index a161db312..20c94b625 100644 --- a/docs/bonus/reference/helper.md +++ b/docs/bonus/reference/helper.md @@ -100,4 +100,33 @@ limitations under the License. ::: vidgear.gears.helper.get_video_bitrate -  \ No newline at end of file +  + +::: vidgear.gears.helper.check_WriteAccess + +  + +::: vidgear.gears.helper.check_open_port + +  + +::: vidgear.gears.helper.delete_file_safe + +  + +::: vidgear.gears.helper.get_supported_demuxers + +  + +::: vidgear.gears.helper.get_supported_vencoders + +  + +::: vidgear.gears.helper.youtube_url_validator + +  + + +::: vidgear.gears.helper.validate_auth_keys + +  diff --git a/docs/bonus/reference/helper_async.md b/docs/bonus/reference/helper_async.md index 79fa7ab6b..cfc329656 100644 --- a/docs/bonus/reference/helper_async.md +++ b/docs/bonus/reference/helper_async.md @@ -40,4 +40,8 @@ limitations under the License. ::: vidgear.gears.asyncio.helper.download_webdata -  \ No newline at end of file +  + +::: vidgear.gears.asyncio.helper.validate_webdata + +  diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index b7aef1a2a..6ea1df99a 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -291,6 +291,7 @@ Now, Open the terminal on another Server System _(a Raspberry Pi with Camera Mod ```python # import required libraries from vidgear.gears import VideoGear +from vidgear.gears import NetGear from vidgear.gears import PiGear # add various Picamera tweak parameters to dictionary diff --git a/docs/gears/netgear_async/advanced/bidirectional_mode.md b/docs/gears/netgear_async/advanced/bidirectional_mode.md new file mode 100644 index 000000000..923372156 --- /dev/null +++ b/docs/gears/netgear_async/advanced/bidirectional_mode.md @@ -0,0 +1,726 @@ + + +# Bidirectional Mode for NetGear_Async API + +

+ Bidirectional Mode +
NetGear_Async's Bidirectional Mode
+

+ +## Overview + +!!! new "New in v0.2.2" + This document was added in `v0.2.2`. + +Bidirectional Mode enables seamless support for Bidirectional data transmission between Client and Sender along with video-frames through its synchronous messaging patterns such as `zmq.PAIR` (ZMQ Pair Pattern) & `zmq.REQ/zmq.REP` (ZMQ Request/Reply Pattern) in NetGear_Async API. + +In Bidirectional Mode, we utilizes the NetGear_Async API's [`transceive_data`](../../../../bonus/reference/NetGear_Async/#vidgear.gears.asyncio.netgear_async.NetGear_Async.transceive_data) method for transmitting data _(at Client's end)_ and receiving data _(in Server's end)_ all while transferring frames in real-time. + +This mode can be easily activated in NetGear_Async through `bidirectional_mode` attribute of its [`options`](../../params/#options) dictionary parameter during initialization. + +  + + +!!! danger "Important" + + * In Bidirectional Mode, `zmq.PAIR`(ZMQ Pair) & `zmq.REQ/zmq.REP`(ZMQ Request/Reply) are **ONLY** Supported messaging patterns. Accessing this mode with any other messaging pattern, will result in `ValueError`. + + * Bidirectional Mode ==only works with [**User-defined Custom Source**](../../usage/#using-netgear_async-with-a-custom-sourceopencv) on Server end==. Otherwise, NetGear_Async API will throw `ValueError`. + + * Bidirectional Mode enables you to send data of **ANY**[^1] Data-type along with frame bidirectionally. + + * NetGear_Async API will throw `RuntimeError` if Bidirectional Mode is disabled at Server end or Client end but not both. + + * Bidirectional Mode may lead to additional **LATENCY** depending upon the size of data being transfer bidirectionally. User discretion is advised! + + +  + +  + +## Exclusive Method and Parameter + +To send data bidirectionally, NetGear_Async API provides following exclusive method and parameter: + +!!! alert "`transceive_data` only works when Bidirectional Mode is enabled." + +* [`transceive_data`](../../../../bonus/reference/NetGear_Async/#vidgear.gears.asyncio.netgear_async.NetGear_Async.transceive_data): It's a bidirectional mode exclusive method to transmit data _(in Receive mode)_ and receive data _(in Send mode)_, all while transferring frames in real-time. + + * `data`: In `transceive_data` method, this parameter enables user to inputs data _(of **ANY**[^1] datatype)_ for sending back to Server at Client's end. + +  + +  + + +## Usage Examples + + +!!! warning "For Bidirectional Mode, NetGear_Async must need [User-defined Custom Source](../../usage/#using-netgear_async-with-a-custom-sourceopencv) at its Server end otherwise it will throw ValueError." + + +### Bare-Minimum Usage with OpenCV + +Following is the bare-minimum code you need to get started with Bidirectional Mode over Custom Source Server built using OpenCV and NetGear_Async API: + +#### Server End + +Open your favorite terminal and execute the following python code: + +!!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import library +from vidgear.gears.asyncio import NetGear_Async +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# initialize Server without any source +server = NetGear_Async(source=None, logging=True, **options) + +# Create a async frame generator as custom source +async def my_frame_generator(): + + # !!! define your own video source here !!! + # Open any valid video stream(for e.g `foo.mp4` file) + stream = cv2.VideoCapture("foo.mp4") + + # loop over stream until its terminated + while True: + # read frames + (grabbed, frame) = stream.read() + + # check for empty frame + if not grabbed: + break + + # {do something with the frame to be sent here} + + # prepare data to be sent(a simple text in our case) + target_data = "Hello, I am a Server." + + # receive data from Client + recv_data = await server.transceive_data() + + # print data just received from Client + if not (recv_data is None): + print(recv_data) + + # send our frame & data + yield (target_data, frame) + + # sleep for sometime + await asyncio.sleep(0) + + # safely close video stream + stream.release() + + +if __name__ == "__main__": + # set event loop + asyncio.set_event_loop(server.loop) + # Add your custom source generator to Server configuration + server.config["generator"] = my_frame_generator() + # Launch the Server + server.launch() + try: + # run your main function task until it is complete + server.loop.run_until_complete(server.task) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + finally: + # finally close the server + server.close() +``` + +#### Client 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 +# import libraries +from vidgear.gears.asyncio import NetGear_Async +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# define and launch Client with `receive_mode=True` +client = NetGear_Async(receive_mode=True, logging=True, **options).launch() + + +# Create a async function where you want to show/manipulate your received frames +async def main(): + # loop over Client's Asynchronous Frame Generator + async for (data, frame) in client.recv_generator(): + + # do something with receive data from server + if not (data is None): + # let's print it + print(data) + + # {do something with received frames here} + + # Show output window(comment these lines if not required) + cv2.imshow("Output Frame", frame) + cv2.waitKey(1) & 0xFF + + # prepare data to be sent + target_data = "Hi, I am a Client here." + # send our data to server + await client.transceive_data(data=target_data) + + # await before continuing + await asyncio.sleep(0) + + +if __name__ == "__main__": + # Set event loop to client's + asyncio.set_event_loop(client.loop) + try: + # run your main function task until it is complete + client.loop.run_until_complete(main()) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + + # close all output window + cv2.destroyAllWindows() + + # safely close client + client.close() +``` + +  + +  + + +### Bare-Minimum Usage with VideoGear + +Following is another comparatively faster Bidirectional Mode bare-minimum example over Custom Source Server built using multi-threaded [VideoGear](../../../videogear/overview/) _(instead of OpenCV)_ and NetGear_Async API: + +#### Server End + +Open your favorite terminal and execute the following python code: + +!!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import library +from vidgear.gears.asyncio import NetGear_Async +from vidgear.gears import VideoGear +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# initialize Server without any source +server = NetGear_Async(source=None, logging=True, **options) + +# Create a async frame generator as custom source +async def my_frame_generator(): + + # !!! define your own video source here !!! + # Open any valid video stream(for e.g `foo.mp4` file) + stream = VideoGear(source="foo.mp4").start() + + # loop over stream until its terminated + while True: + # read frames + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame to be sent here} + + # prepare data to be sent(a simple text in our case) + target_data = "Hello, I am a Server." + + # receive data from Client + recv_data = await server.transceive_data() + + # print data just received from Client + if not (recv_data is None): + print(recv_data) + + # send our frame & data + yield (target_data, frame) + + # sleep for sometime + await asyncio.sleep(0) + + # safely close video stream + stream.stop() + + +if __name__ == "__main__": + # set event loop + asyncio.set_event_loop(server.loop) + # Add your custom source generator to Server configuration + server.config["generator"] = my_frame_generator() + # Launch the Server + server.launch() + try: + # run your main function task until it is complete + server.loop.run_until_complete(server.task) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + finally: + # finally close the server + server.close() +``` + +#### Client 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 +# import libraries +from vidgear.gears.asyncio import NetGear_Async +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# define and launch Client with `receive_mode=True` +client = NetGear_Async(receive_mode=True, logging=True, **options).launch() + +# Create a async function where you want to show/manipulate your received frames +async def main(): + # loop over Client's Asynchronous Frame Generator + async for (data, frame) in client.recv_generator(): + + # do something with receive data from server + if not (data is None): + # let's print it + print(data) + + # {do something with received frames here} + + # Show output window(comment these lines if not required) + cv2.imshow("Output Frame", frame) + cv2.waitKey(1) & 0xFF + + # prepare data to be sent + target_data = "Hi, I am a Client here." + + # send our data to server + await client.transceive_data(data=target_data) + + # await before continuing + await asyncio.sleep(0) + + +if __name__ == "__main__": + # Set event loop to client's + asyncio.set_event_loop(client.loop) + try: + # run your main function task until it is complete + client.loop.run_until_complete(main()) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + + # close all output window + cv2.destroyAllWindows() + + # safely close client + client.close() +``` + +  + +  + + + +### Using Bidirectional Mode with Variable Parameters + + +#### 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: + +!!! info "Note down the IP-address of this system(required at Server's end) by executing the command: `hostname -I` and also replace it in the following code." + +!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import libraries +from vidgear.gears.asyncio import NetGear_Async +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# Define NetGear_Async Client at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with yours !!! +client = NetGear_Async( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + receive_mode=True, + logging=True, + **options +) + +# Create a async function where you want to show/manipulate your received frames +async def main(): + # loop over Client's Asynchronous Frame Generator + async for (data, frame) in client.recv_generator(): + + # do something with receive data from server + if not (data is None): + # let's print it + print(data) + + # {do something with received frames here} + + # Show output window(comment these lines if not required) + cv2.imshow("Output Frame", frame) + cv2.waitKey(1) & 0xFF + + # prepare data to be sent + target_data = "Hi, I am a Client here." + # send our data to server + await client.transceive_data(data=target_data) + + # await before continuing + await asyncio.sleep(0) + + +if __name__ == "__main__": + # Set event loop to client's + asyncio.set_event_loop(client.loop) + try: + # run your main function task until it is complete + client.loop.run_until_complete(main()) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + + # close all output window + cv2.destroyAllWindows() + + # safely close client + client.close() +``` + +  + +#### Server End + +Now, Open the terminal on another Server System _(a Raspberry Pi with Camera Module)_, and execute the following python code: + +!!! info "Replace the IP address in the following code with Client's IP address you noted earlier." + +!!! tip "You can terminate stream on both side anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import library +from vidgear.gears.asyncio import NetGear_Async +from vidgear.gears import VideoGear +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# initialize Server without any source at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with client's IP address !!! +server = NetGear_Async( + source=None, + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options +) + +# Create a async frame generator as custom source +async def my_frame_generator(): + + # !!! define your own video source here !!! + # Open any video stream such as live webcam + # video stream on first index(i.e. 0) device + # add various Picamera tweak parameters to dictionary + options = { + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, + } + + # open pi video stream with defined parameters + stream = PiGear(resolution=(640, 480), framerate=60, logging=True, **options).start() + + # loop over stream until its terminated + while True: + # read frames + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame to be sent here} + + # prepare data to be sent(a simple text in our case) + target_data = "Hello, I am a Server." + + # receive data from Client + recv_data = await server.transceive_data() + + # print data just received from Client + if not (recv_data is None): + print(recv_data) + + # send our frame & data + yield (target_data, frame) + + # sleep for sometime + await asyncio.sleep(0) + + # safely close video stream + stream.stop() + + +if __name__ == "__main__": + # set event loop + asyncio.set_event_loop(server.loop) + # Add your custom source generator to Server configuration + server.config["generator"] = my_frame_generator() + # Launch the Server + server.launch() + try: + # run your main function task until it is complete + server.loop.run_until_complete(server.task) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + finally: + # finally close the server + server.close() +``` + +  + +  + + +### Using Bidirectional Mode for Video-Frames Transfer + + +In this example we are going to implement a bare-minimum example, where we will be sending video-frames _(3-Dimensional numpy arrays)_ of the same Video bidirectionally at the same time, for testing the real-time performance and synchronization between the Server and the Client using this(Bidirectional) Mode. + +!!! tip "This feature is great for building applications like Real-Time Video Chat." + +!!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#vidgear.gears.helper.reducer--reducer) method for reducing frame-size on-the-go for additional performance." + +!!! warning "Remember, Sending large HQ video-frames may required more network bandwidth and packet size which may lead to video latency!" + +#### Server End + +Open your favorite terminal and execute the following python code: + +!!! tip "You can terminate both side anytime by pressing ++ctrl+"C"++ on your keyboard!" + +!!! alert "Server end can only send [numpy.ndarray](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) datatype as frame but not as data." + +```python +# import library +from vidgear.gears.asyncio import NetGear_Async +from vidgear.gears.asyncio.helper import reducer +import cv2, asyncio +import numpy as np + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# Define NetGear Server without any source and with defined parameters +server = NetGear_Async(source=None, pattern=1, logging=True, **options) + +# Create a async frame generator as custom source +async def my_frame_generator(): + # !!! define your own video source here !!! + # Open any valid video stream(for e.g `foo.mp4` file) + stream = cv2.VideoCapture("foo.mp4") + # loop over stream until its terminated + while True: + + # read frames + (grabbed, frame) = stream.read() + + # check for empty frame + if not grabbed: + break + + # reducer frames size if you want more performance, otherwise comment this line + frame = await reducer(frame, percentage=30) # reduce frame by 30% + + # {do something with the frame to be sent here} + + # send frame & data and also receive data from Client + recv_data = await server.transceive_data() + + # receive data from Client + if not (recv_data is None): + # check data is a numpy frame + if isinstance(recv_data, np.ndarray): + + # {do something with received numpy frame here} + + # Let's show it on output window + cv2.imshow("Received Frame", recv_data) + cv2.waitKey(1) & 0xFF + else: + # otherwise just print data + print(recv_data) + + # prepare data to be sent(a simple text in our case) + target_data = "Hello, I am a Server." + + # send our frame & data to client + yield (target_data, frame) + + # sleep for sometime + await asyncio.sleep(0) + + # safely close video stream + stream.release() + + +if __name__ == "__main__": + # set event loop + asyncio.set_event_loop(server.loop) + # Add your custom source generator to Server configuration + server.config["generator"] = my_frame_generator() + # Launch the Server + server.launch() + try: + # run your main function task until it is complete + server.loop.run_until_complete(server.task) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + finally: + # finally close the server + server.close() +``` + +  + +#### Client 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 +# import libraries +from vidgear.gears.asyncio import NetGear_Async +from vidgear.gears.asyncio.helper import reducer +import cv2, asyncio + +# activate Bidirectional mode +options = {"bidirectional_mode": True} + +# define and launch Client with `receive_mode=True` +client = NetGear_Async(pattern=1, receive_mode=True, logging=True, **options).launch() + +# Create a async function where you want to show/manipulate your received frames +async def main(): + # !!! define your own video source here !!! + # again open the same video stream for comparison + stream = cv2.VideoCapture("foo.mp4") + # loop over Client's Asynchronous Frame Generator + async for (server_data, frame) in client.recv_generator(): + + # check for server data + if not (server_data is None): + + # {do something with the server data here} + + # lets print extracted server data + print(server_data) + + # {do something with received frames here} + + # Show output window + cv2.imshow("Output Frame", frame) + key = cv2.waitKey(1) & 0xFF + + # read frame target data from stream to be sent to server + (grabbed, target_data) = stream.read() + # check for frame + if grabbed: + # reducer frames size if you want more performance, otherwise comment this line + target_data = await reducer( + target_data, percentage=30 + ) # reduce frame by 30% + # send our frame data + await client.transceive_data(data=target_data) + + # await before continuing + await asyncio.sleep(0) + + # safely close video stream + stream.release() + + +if __name__ == "__main__": + # Set event loop to client's + asyncio.set_event_loop(client.loop) + try: + # run your main function task until it is complete + client.loop.run_until_complete(main()) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + # close all output window + cv2.destroyAllWindows() + # safely close client + client.close() +``` + +  + + +[^1]: + + !!! warning "Additional data of [numpy.ndarray](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) datatype is **ONLY SUPPORTED** at Client's end with [`transceive_data`](../../../../bonus/reference/NetGear_Async/#vidgear.gears.asyncio.netgear_async.NetGear_Async.transceive_data) method using its `data` parameter. Whereas Server end can only send [numpy.ndarray](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) datatype as frame but not as data." + + +  \ No newline at end of file diff --git a/docs/gears/netgear_async/overview.md b/docs/gears/netgear_async/overview.md index caf43f195..162ff784a 100644 --- a/docs/gears/netgear_async/overview.md +++ b/docs/gears/netgear_async/overview.md @@ -26,11 +26,13 @@ limitations under the License. ## Overview -> _NetGear_Async can generate the same performance as [NetGear API](../../netgear/overview/) at about one-third the memory consumption, and also provide complete server-client handling with various options to use variable protocols/patterns similar to NetGear, but it doesn't support any [NetGear's Exclusive Modes](../../netgear/overview/#exclusive-modes) yet._ +> _NetGear_Async can generate the same performance as [NetGear API](../../netgear/overview/) at about one-third the memory consumption, and also provide complete server-client handling with various options to use variable protocols/patterns similar to NetGear, but lacks in term of flexibility as it supports only a few [NetGear's Exclusive Modes](../../netgear/overview/#exclusive-modes)._ NetGear_Async is built on [`zmq.asyncio`](https://pyzmq.readthedocs.io/en/latest/api/zmq.asyncio.html), and powered by a high-performance asyncio event loop called [**`uvloop`**](https://github.com/MagicStack/uvloop) to achieve unmatchable high-speed and lag-free video streaming over the network with minimal resource constraints. NetGear_Async can transfer thousands of frames in just a few seconds without causing any significant load on your system. -NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](../../netgear/overview/) but doesn't support any [NetGear's Exclusive Modes](../../netgear/overview/#exclusive-modes) yet. Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc](../usage/#using-netgear_async-with-a-custom-sourceopencv) example). +NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](../../netgear/overview/). Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc](../usage/#using-netgear_async-with-a-custom-sourceopencv) example). + +NetGear_Async now supports additional [**bidirectional data transmission**](../advanced/bidirectional_mode) between receiver(client) and sender(server) while transferring frames. Users can easily build complex applications such as like [Real-Time Video Chat](../advanced/bidirectional_mode/#using-bidirectional-mode-for-video-frames-transfer) in just few lines of code. In addition to all this, NetGear_Async API also provides internal wrapper around [VideoGear](../../videogear/overview/), which itself provides internal access to both [CamGear](../../camgear/overview/) and [PiGear](../../pigear/overview/) APIs, thereby granting it exclusive power for transferring frames incoming from any source to the network. diff --git a/docs/gears/netgear_async/params.md b/docs/gears/netgear_async/params.md index 820a3ff37..4b1180ba4 100644 --- a/docs/gears/netgear_async/params.md +++ b/docs/gears/netgear_async/params.md @@ -150,6 +150,34 @@ In NetGear_Async, the Receiver-end keeps tracks if frames are received from Serv NetGear_Async(timeout=5.0) # sets 5secs timeout ``` +## **`options`** + +This parameter provides the flexibility to alter various NetGear_Async API's internal properties and modes. + +**Data-Type:** Dictionary + +**Default Value:** Its default value is `{}` + +**Usage:** + + +!!! abstract "Supported dictionary attributes for NetGear_Async API" + + * **`bidirectional_mode`** (_boolean_) : This internal attribute activates the exclusive [**Bidirectional Mode**](../advanced/bidirectional_mode/), if enabled(`True`). + + +The desired attributes can be passed to NetGear_Async API as follows: + +```python +# formatting parameters as dictionary attributes +options = { + "bidirectional_mode": True, +} +# assigning it +NetGear_Async(logging=True, **options) +``` + +     diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index a1102d1b7..f0a123657 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -223,7 +223,9 @@ if __name__ == "__main__": ## Using NetGear_Async with a Custom Source(OpenCV) -NetGear_Async allows you to easily define your own custom Source at Server-end that you want to use to manipulate your frames before sending them onto the network. Let's implement a bare-minimum example with a Custom Source using NetGear_Async API and OpenCV: +NetGear_Async allows you to easily define your own custom Source at Server-end that you want to use to manipulate your frames before sending them onto the network. + +Let's implement a bare-minimum example with a Custom Source using NetGear_Async API and OpenCV: ### Server's End @@ -237,7 +239,7 @@ from vidgear.gears.asyncio import NetGear_Async import cv2, asyncio # initialize Server without any source -server = NetGear_Async(logging=True) +server = NetGear_Async(source=None, logging=True) # Create a async frame generator as custom source async def my_frame_generator(): diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 164a4c429..8b0d34a3f 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -1021,8 +1021,8 @@ The complete example is as follows: { "-resolution": "1280x720", "-video_bitrate": "4000k", - }, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps + }, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! "-audio": [ @@ -1086,8 +1086,8 @@ The complete example is as follows: { "-resolution": "1280x720", "-video_bitrate": "4000k", - }, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps + }, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! "-audio": [ diff --git a/docs/overrides/assets/images/bidir_async.png b/docs/overrides/assets/images/bidir_async.png new file mode 100644 index 0000000000000000000000000000000000000000..8f158c65b603931c62e8a6446649a58a45c33876 GIT binary patch literal 51563 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 k0|_5g@!~`kUdQyG|7xF 0 and isinstance(timeout, (int, float)): + if timeout and isinstance(timeout, (int, float)): self.__timeout = float(timeout) else: self.__timeout = 15.0 @@ -662,7 +665,7 @@ async def __frame_generator(self): async def transceive_data(self, data=None): """ - Bidirectional Mode exclusive method to Transmit _(in Receive mode)_ and Receive _(in Send mode)_ data in real-time. + Bidirectional Mode exclusive method to Transmit data _(in Receive mode)_ and Receive data _(in Send mode)_. Parameters: data (any): inputs data _(of any datatype)_ for sending back to Server. @@ -672,11 +675,10 @@ async def transceive_data(self, data=None): if self.__bi_mode: if self.__receive_mode: await self.__queue.put(data) - elif not self.__receive_mode and not self.__queue.empty(): - recvd_data = await self.__queue.get() - self.__queue.task_done() else: - pass + if not self.__queue.empty(): + recvd_data = await self.__queue.get() + self.__queue.task_done() else: logger.error( "`transceive_data()` function cannot be used when Bidirectional Mode is disabled." @@ -754,4 +756,6 @@ def close(self, skip_loop=False): self.loop.close() else: # otherwise create a task - asyncio.ensure_future(self.__terminate_connection(disable_confirmation=True)) + asyncio.ensure_future( + self.__terminate_connection(disable_confirmation=True) + ) diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index a52cdb220..1e18dd2f2 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -57,7 +57,7 @@ def logger_handler(): """ - ### logger_handler + ## logger_handler Returns the logger handler @@ -131,7 +131,7 @@ def send(self, request, **kwargs): def restore_levelnames(): """ - ### restore_levelnames + ## restore_levelnames Auxiliary method to restore logger levelnames. """ @@ -148,7 +148,7 @@ def restore_levelnames(): def check_CV_version(): """ - ### check_CV_version + ## check_CV_version **Returns:** OpenCV's version first bit """ @@ -160,7 +160,7 @@ def check_CV_version(): def check_open_port(address, port=22): """ - ### check_open_port + ## check_open_port Checks whether specified port open at given IP address. @@ -181,7 +181,7 @@ def check_open_port(address, port=22): def check_WriteAccess(path, is_windows=False): """ - ### check_WriteAccess + ## check_WriteAccess Checks whether given path directory has Write-Access. @@ -212,7 +212,7 @@ def check_WriteAccess(path, is_windows=False): def check_gstreamer_support(logging=False): """ - ### check_gstreamer_support + ## check_gstreamer_support Checks whether OpenCV is compiled with Gstreamer(`>=1.0.0`) support. @@ -239,7 +239,7 @@ def check_gstreamer_support(logging=False): def get_supported_resolution(value, logging=False): """ - ### get_supported_resolution + ## get_supported_resolution Parameters: value (string): value to be validated @@ -285,7 +285,7 @@ def get_supported_resolution(value, logging=False): def dimensions_to_resolutions(value): """ - ### dimensions_to_resolutions + ## dimensions_to_resolutions Parameters: value (list): list of dimensions (e.g. `640x360`) @@ -312,7 +312,7 @@ def dimensions_to_resolutions(value): def get_supported_vencoders(path): """ - ### get_supported_vencoders + ## get_supported_vencoders Find and returns FFmpeg's supported video encoders @@ -339,7 +339,7 @@ def get_supported_vencoders(path): def get_supported_demuxers(path): """ - ### get_supported_demuxers + ## get_supported_demuxers Find and returns FFmpeg's supported demuxers @@ -361,7 +361,7 @@ def get_supported_demuxers(path): def is_valid_url(path, url=None, logging=False): """ - ### is_valid_url + ## is_valid_url Checks URL validity by testing its scheme against FFmpeg's supported protocols @@ -400,7 +400,7 @@ def is_valid_url(path, url=None, logging=False): def validate_video(path, video_path=None, logging=False): """ - ### validate_video + ## validate_video Validates video by retrieving resolution/size and framerate from file. @@ -439,7 +439,7 @@ def validate_video(path, video_path=None, logging=False): def create_blank_frame(frame=None, text="", logging=False): """ - ### create_blank_frame + ## create_blank_frame Create blank frames of given frame size with text @@ -478,7 +478,7 @@ def create_blank_frame(frame=None, text="", logging=False): def extract_time(value): """ - ### extract_time + ## extract_time Extract time from give string value. @@ -507,7 +507,7 @@ def extract_time(value): def validate_audio(path, source=None): """ - ### validate_audio + ## validate_audio Validates audio by retrieving audio-bitrate from file. @@ -556,7 +556,7 @@ def validate_audio(path, source=None): def get_video_bitrate(width, height, fps, bpp): """ - ### get_video_bitrate + ## get_video_bitrate Calculate optimum Bitrate from resolution, framerate, bits-per-pixels values @@ -573,7 +573,7 @@ def get_video_bitrate(width, height, fps, bpp): def delete_file_safe(file_path): """ - ### delete_ext_safe + ## delete_ext_safe Safely deletes files at given path. @@ -593,7 +593,7 @@ def delete_file_safe(file_path): def mkdir_safe(dir_path, logging=False): """ - ### mkdir_safe + ## mkdir_safe Safely creates directory at given path. @@ -615,7 +615,7 @@ def mkdir_safe(dir_path, logging=False): def delete_ext_safe(dir_path, extensions=[], logging=False): """ - ### delete_ext_safe + ## delete_ext_safe Safely deletes files with given extensions at given path. @@ -652,7 +652,7 @@ def delete_ext_safe(dir_path, extensions=[], logging=False): def capPropId(property, logging=True): """ - ### capPropId + ## capPropId Retrieves the OpenCV property's Integer(Actual) value from string. @@ -675,7 +675,7 @@ def capPropId(property, logging=True): def retrieve_best_interpolation(interpolations): """ - ### retrieve_best_interpolation + ## retrieve_best_interpolation Retrieves best interpolation for resizing Parameters: @@ -692,7 +692,7 @@ def retrieve_best_interpolation(interpolations): def youtube_url_validator(url): """ - ### youtube_url_validator + ## youtube_url_validator Validates & extracts Youtube video ID from URL. @@ -715,7 +715,7 @@ def youtube_url_validator(url): def reducer(frame=None, percentage=0, interpolation=cv2.INTER_LANCZOS4): """ - ### reducer + ## reducer Reduces frame size by given percentage @@ -756,7 +756,7 @@ def reducer(frame=None, percentage=0, interpolation=cv2.INTER_LANCZOS4): def dict2Args(param_dict): """ - ### dict2Args + ## dict2Args Converts dictionary attributes to list(args) @@ -787,7 +787,7 @@ def get_valid_ffmpeg_path( custom_ffmpeg="", is_windows=False, ffmpeg_download_path="", logging=False ): """ - ### get_valid_ffmpeg_path + ## get_valid_ffmpeg_path Validate the given FFmpeg path/binaries, and returns a valid FFmpeg executable path. @@ -880,7 +880,7 @@ def get_valid_ffmpeg_path( def download_ffmpeg_binaries(path, os_windows=False, os_bit=""): """ - ### download_ffmpeg_binaries + ## download_ffmpeg_binaries Generates FFmpeg Static Binaries for windows(if not available) @@ -963,7 +963,7 @@ def download_ffmpeg_binaries(path, os_windows=False, os_bit=""): def validate_ffmpeg(path, logging=False): """ - ### validate_ffmpeg + ## validate_ffmpeg Validate FFmeg Binaries. returns `True` if tests are passed. @@ -997,7 +997,7 @@ def validate_ffmpeg(path, logging=False): def check_output(*args, **kwargs): """ - ### check_output + ## check_output Returns stdin output from subprocess module """ @@ -1036,7 +1036,7 @@ def check_output(*args, **kwargs): def generate_auth_certificates(path, overwrite=False, logging=False): """ - ### generate_auth_certificates + ## generate_auth_certificates Auto-Generates, and Auto-validates CURVE ZMQ key-pairs for NetGear API's Secure Mode. @@ -1148,7 +1148,7 @@ def generate_auth_certificates(path, overwrite=False, logging=False): def validate_auth_keys(path, extension): """ - ### validate_auth_keys + ## validate_auth_keys Validates, and also maintains generated ZMQ CURVE Key-pairs. 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 3d0e77d68..0a8b2c311 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -122,11 +122,14 @@ async def custom_dataframe_generator(self): # Create a async function where you want to show/manipulate your received frames -async def client_iterator(client): +async def client_iterator(client, data=False): # loop over Client's Asynchronous Frame Generator async for frame in client.recv_generator(): # test frame validity assert not (frame is None or np.shape(frame) == ()), "Failed Test" + if data: + # send data + await client.transceive_data(data="invalid") # await before continuing await asyncio.sleep(0) @@ -152,7 +155,7 @@ async def client_dataframe_iterator(client, data=""): @pytest.mark.asyncio @pytest.mark.parametrize( "pattern", - [0, 2, 3, 4], + [1, 2, 3, 4], ) async def test_netgear_async_playback(pattern): try: @@ -164,12 +167,15 @@ async def test_netgear_async_playback(pattern): server = NetGear_Async( source=return_testvideo_path(), pattern=pattern, - timeout=7.0, + timeout=7.0 if pattern == 4 else 0, logging=True, **options_gear ).launch() # gather and run tasks - input_coroutines = [server.task, client_iterator(client)] + input_coroutines = [ + server.task, + client_iterator(client, data=True if pattern == 4 else False), + ] res = await asyncio.gather(*input_coroutines, return_exceptions=True) except Exception as e: if isinstance(e, queue.Empty): @@ -275,15 +281,19 @@ async def test_netgear_async_bidirectionalmode( @pytest.mark.asyncio -@pytest.mark.parametrize("address, port", [("172.31.11.15.77", "5555"), (None, "5555")]) +@pytest.mark.parametrize( + "address, port", + [("172.31.11.15.77", "5555"), ("172.31.11.33.44", "5555"), (None, "5555")], +) async def test_netgear_async_addresses(address, port): + server = None try: # define and launch Client with `receive_mode = True` client = NetGear_Async( address=address, port=port, logging=True, timeout=5.0, receive_mode=True ).launch() + options_gear = {"THREAD_TIMEOUT": 60} if address is None: - options_gear = {"THREAD_TIMEOUT": 60} server = NetGear_Async( source=return_testvideo_path(), address=address, @@ -295,15 +305,28 @@ async def test_netgear_async_addresses(address, port): # gather and run tasks input_coroutines = [server.task, client_iterator(client)] await asyncio.gather(*input_coroutines, return_exceptions=True) + elif address == "172.31.11.33.44": + options_gear["bidirectional_mode"] = True + server = NetGear_Async( + source=return_testvideo_path(), + address=address, + port=port, + logging=True, + timeout=5.0, + **options_gear + ).launch() + await asyncio.ensure_future(server.task) else: await asyncio.ensure_future(client_iterator(client)) except Exception as e: - if address == "172.31.11.15.77" or isinstance(e, queue.Empty): + if address in ["172.31.11.15.77", "172.31.11.33.44"] or isinstance( + e, queue.Empty + ): logger.exception(str(e)) else: pytest.fail(str(e)) finally: - if address is None: + if (address is None or address == "172.31.11.33.44") and not (server is None): server.close(skip_loop=True) client.close(skip_loop=True) From 6e383e99e1fe784c832a36c8fe6dea1a24dd0725 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Sun, 22 Aug 2021 23:05:14 +0530 Subject: [PATCH 102/112] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Docs:=20Reverted?= =?UTF-8?q?=20UI=20change=20in=20CSS.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/overrides/assets/stylesheets/custom.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 62d2fbac2..af2beccd0 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -189,6 +189,7 @@ th { overflow: hidden; } .md-version__current { + text-transform: uppercase; font-weight: bolder; } .md-typeset .task-list-control .task-list-indicator::before { From d260320755e860294abd106f23f23e90d8918695 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 23 Aug 2021 10:03:40 +0530 Subject: [PATCH 103/112] =?UTF-8?q?=F0=9F=92=84=20Docs:=20New=20assets=20a?= =?UTF-8?q?nd=20typos=20fixed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +++++----- docs/gears/camgear/advanced/source_params.md | 13 +++++++--- docs/gears/camgear/usage.md | 2 +- docs/gears/netgear_async/overview.md | 2 +- docs/gears/netgear_async/usage.md | 2 +- docs/gears/stabilizer/usage.md | 2 +- docs/gears/streamgear/introduction.md | 24 ++++++++---------- docs/gears/streamgear/rtfm/overview.md | 8 +++--- docs/gears/streamgear/ssm/overview.md | 10 +++++--- docs/gears/webgear/advanced.md | 2 +- docs/gears/webgear_rtc/advanced.md | 2 +- docs/gears/webgear_rtc/overview.md | 2 +- docs/gears/writegear/introduction.md | 2 +- docs/overrides/assets/images/stream_tweak.png | Bin 0 -> 44390 bytes docs/switch_from_cv.md | 2 +- vidgear/gears/asyncio/netgear_async.py | 2 +- vidgear/gears/asyncio/webgear_rtc.py | 2 +- 17 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 docs/overrides/assets/images/stream_tweak.png diff --git a/README.md b/README.md index f4eadc415..b45a5dec1 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ In addition to this, WriteGear also provides flexible access to [**OpenCV's Vide * **Compression Mode:** In this mode, WriteGear utilizes powerful [**FFmpeg**][ffmpeg] inbuilt encoders to encode lossless multimedia files. This mode provides us the ability to exploit almost any parameter available within FFmpeg, effortlessly and flexibly, and while doing that it robustly handles all errors/warnings quietly. **You can find more about this mode [here ➶][cm-writegear-doc]** - * **Non-Compression Mode:** In this mode, WriteGear utilizes basic [**OpenCV's inbuilt VideoWriter API**][opencv-vw] tools. This mode also supports all parameters manipulation available within VideoWriter API, but it lacks the ability to manipulate encoding parameters and other important features like video compression, audio encoding, etc. **You can learn about this mode [here ➶][ncm-writegear-doc]** + * **Non-Compression Mode:** In this mode, WriteGear utilizes basic [**OpenCV's inbuilt VideoWriter API**][opencv-vw] tools. This mode also supports all parameter transformations available within OpenCV's VideoWriter API, but it lacks the ability to manipulate encoding parameters and other important features like video compression, audio encoding, etc. **You can learn about this mode [here ➶][ncm-writegear-doc]** ### WriteGear API Guide: @@ -449,10 +449,9 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** - * **Single-Source Mode:** In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated 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 video-frames _(as opposed to a entire file)_ into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. ***Learn more about this mode [here ➶][rtf-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:** 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: @@ -507,7 +506,7 @@ WebGear API works on [**Starlette**](https://www.starlette.io/)'s ASGI applicati WebGear API uses an intraframe-only compression scheme under the hood where the sequence of video-frames are first encoded as JPEG-DIB (JPEG with Device-Independent Bit compression) and then streamed over HTTP using Starlette's Multipart [Streaming Response](https://www.starlette.io/responses/#streamingresponse) and a [Uvicorn](https://www.uvicorn.org/#quickstart) ASGI Server. This method imposes lower processing and memory requirements, but the quality is not the best, since JPEG compression is not very efficient for motion video. -In layman's terms, WebGear acts as a powerful **Video Broadcaster** that transmits live video-frames to any web-browser in the network. Additionally, WebGear API also provides a special internal wrapper around [VideoGear](#videogear), which itself provides internal access to both [CamGear](#camgear) and [PiGear](#pigear) APIs, thereby granting it exclusive power of broadcasting frames from any incoming stream. It also allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc][webgear-cs] example). +In layman's terms, WebGear acts as a powerful **Video Broadcaster** that transmits live video-frames to any web-browser in the network. Additionally, WebGear API also provides a special internal wrapper around [VideoGear](#videogear), which itself provides internal access to both [CamGear](#camgear) and [PiGear](#pigear) APIs, thereby granting it exclusive power of broadcasting frames from any incoming stream. It also allows us to define our custom Server as source to transform frames easily before sending them across the network(see this [doc][webgear-cs] example). **Below is a snapshot of a WebGear Video Server in action on Chrome browser:** @@ -558,7 +557,7 @@ web.shutdown() WebGear_RTC is implemented with the help of [**aiortc**][aiortc] library which is built on top of asynchronous I/O framework for Web Real-Time Communication (WebRTC) and Object Real-Time Communication (ORTC) and supports many features like SDP generation/parsing, Interactive Connectivity Establishment with half-trickle and mDNS support, DTLS key and certificate generation, DTLS handshake, etc. -WebGear_RTC can handle [multiple consumers][webgear_rtc-mc] seamlessly and provides native support for ICE _(Interactive Connectivity Establishment)_ protocol, STUN _(Session Traversal Utilities for NAT)_, and TURN _(Traversal Using Relays around NAT)_ servers that help us to easily establish direct media connection with the remote peers for uninterrupted data flow. It also allows us to define our custom Server as a source to manipulate frames easily before sending them across the network(see this [doc][webgear_rtc-cs] example). +WebGear_RTC can handle [multiple consumers][webgear_rtc-mc] seamlessly and provides native support for ICE _(Interactive Connectivity Establishment)_ protocol, STUN _(Session Traversal Utilities for NAT)_, and TURN _(Traversal Using Relays around NAT)_ servers that help us to easily establish direct media connection with the remote peers for uninterrupted data flow. It also allows us to define our custom Server as a source to transform frames easily before sending them across the network(see this [doc][webgear_rtc-cs] example). WebGear_RTC API works in conjunction with [**Starlette**][starlette]'s ASGI application and provides easy access to its complete framework. WebGear_RTC can also flexibly interact with Starlette's ecosystem of shared middleware, mountable applications, [Response classes](https://www.starlette.io/responses/), [Routing tables](https://www.starlette.io/routing/), [Static Files](https://www.starlette.io/staticfiles/), [Templating engine(with Jinja2)](https://www.starlette.io/templates/), etc. @@ -615,7 +614,7 @@ web.shutdown() NetGear_Async is built on [`zmq.asyncio`][asyncio-zmq], and powered by a high-performance asyncio event loop called [**`uvloop`**][uvloop] to achieve unmatchable high-speed and lag-free video streaming over the network with minimal resource constraints. NetGear_Async can transfer thousands of frames in just a few seconds without causing any significant load on your system. -NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](#netgear). Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc][netgear_Async-cs] example). +NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](#netgear). Furthermore, NetGear_Async allows us to define our custom Server as source to transform frames easily before sending them across the network(see this [doc][netgear_Async-cs] example). NetGear_Async now supports additional [**bidirectional data transmission**][btm_netgear_async] between receiver(client) and sender(server) while transferring video-frames. Users can easily build complex applications such as like [Real-Time Video Chat][rtvc] in just few lines of code. diff --git a/docs/gears/camgear/advanced/source_params.md b/docs/gears/camgear/advanced/source_params.md index 01cb9287d..0c91b9cdb 100644 --- a/docs/gears/camgear/advanced/source_params.md +++ b/docs/gears/camgear/advanced/source_params.md @@ -20,17 +20,22 @@ limitations under the License. # Source Tweak Parameters for CamGear API -  +
+ Source Tweak Parameters +
## Overview -The [`options`](../../params/#options) dictionary parameter in CamGear, gives user the ability to alter various **Source Tweak Parameters** available within [OpenCV's VideoCapture Class](https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80). These tweak parameters can be used to manipulate input source Camera-Device properties _(such as its brightness, saturation, size, iso, gain etc.)_ seamlessly. Thereby, All Source Tweak Parameters supported by CamGear API are disscussed in this document. +The [`options`](../../params/#options) dictionary parameter in CamGear gives user the ability to alter various parameters available within [OpenCV's VideoCapture Class](https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html#a57c0e81e83e60f36c83027dc2a188e80). + +These tweak parameters can be used to transform input Camera-Source properties _(such as its brightness, saturation, size, iso, gain etc.)_ seamlessly. All parameters supported by CamGear API are disscussed in this document.   -!!! quote "" - ### Exclusive CamGear Parameters +### Exclusive CamGear Parameters + +!!! quote "" In addition to Source Tweak Parameters, CamGear also provides some exclusive attributes for its [`options`](../../params/#options) dictionary parameters. These attributes are as follows: diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index f40d4f10f..d2b377c13 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -186,7 +186,7 @@ stream.stop() ## Using CamGear with Variable Camera Properties -CamGear API also flexibly support various **Source Tweak Parameters** available within [OpenCV's VideoCapture API](https://docs.opencv.org/master/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d). These tweak parameters can be used to manipulate input source Camera-Device properties _(such as its brightness, saturation, size, iso, gain etc.)_ seamlessly, and can be easily applied in CamGear API through its `options` dictionary parameter by formatting them as its attributes. The complete usage example is as follows: +CamGear API also flexibly support various **Source Tweak Parameters** available within [OpenCV's VideoCapture API](https://docs.opencv.org/master/d4/d15/group__videoio__flags__base.html#gaeb8dd9c89c10a5c63c139bf7c4f5704d). These tweak parameters can be used to transform input source Camera-Device properties _(such as its brightness, saturation, size, iso, gain etc.)_ seamlessly, and can be easily applied in CamGear API through its `options` dictionary parameter by formatting them as its attributes. The complete usage example is as follows: !!! tip "All the supported Source Tweak Parameters can be found [here ➶](../advanced/source_params/#source-tweak-parameters-for-camgear-api)" diff --git a/docs/gears/netgear_async/overview.md b/docs/gears/netgear_async/overview.md index 162ff784a..fc2e505cf 100644 --- a/docs/gears/netgear_async/overview.md +++ b/docs/gears/netgear_async/overview.md @@ -30,7 +30,7 @@ limitations under the License. NetGear_Async is built on [`zmq.asyncio`](https://pyzmq.readthedocs.io/en/latest/api/zmq.asyncio.html), and powered by a high-performance asyncio event loop called [**`uvloop`**](https://github.com/MagicStack/uvloop) to achieve unmatchable high-speed and lag-free video streaming over the network with minimal resource constraints. NetGear_Async can transfer thousands of frames in just a few seconds without causing any significant load on your system. -NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](../../netgear/overview/). Furthermore, NetGear_Async allows us to define our custom Server as source to manipulate frames easily before sending them across the network(see this [doc](../usage/#using-netgear_async-with-a-custom-sourceopencv) example). +NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to [NetGear API](../../netgear/overview/). Furthermore, NetGear_Async allows us to define our custom Server as source to transform frames easily before sending them across the network(see this [doc](../usage/#using-netgear_async-with-a-custom-sourceopencv) example). NetGear_Async now supports additional [**bidirectional data transmission**](../advanced/bidirectional_mode) between receiver(client) and sender(server) while transferring frames. Users can easily build complex applications such as like [Real-Time Video Chat](../advanced/bidirectional_mode/#using-bidirectional-mode-for-video-frames-transfer) in just few lines of code. diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index f0a123657..0220d1252 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -223,7 +223,7 @@ if __name__ == "__main__": ## Using NetGear_Async with a Custom Source(OpenCV) -NetGear_Async allows you to easily define your own custom Source at Server-end that you want to use to manipulate your frames before sending them onto the network. +NetGear_Async allows you to easily define your own custom Source at Server-end that you want to use to transform your frames before sending them onto the network. Let's implement a bare-minimum example with a Custom Source using NetGear_Async API and OpenCV: diff --git a/docs/gears/stabilizer/usage.md b/docs/gears/stabilizer/usage.md index 65167fe37..fd423ba41 100644 --- a/docs/gears/stabilizer/usage.md +++ b/docs/gears/stabilizer/usage.md @@ -145,7 +145,7 @@ stream.release() ## Using Stabilizer with Variable Parameters -Stabilizer class provide certain [parameters](../params/) which you can use to manipulate its internal properties. The complete usage example is as follows: +Stabilizer class provide certain [parameters](../params/) which you can use to tweak its internal properties. The complete usage example is as follows: ```python # import required libraries diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 205e73f0c..027b1a0d3 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -39,7 +39,7 @@ SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/ 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. -!!! tip "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead." +!!! alert "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead."   @@ -52,10 +52,17 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../ffmpeg_install/) for its installation. - * :warning: StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + * :warning: StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executable on your system. * It is advised to enable logging _([`logging=True`](../params/#logging))_ on the first run for easily identifying any runtime errors. +!!! 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. + +   ## Mode of Operations @@ -68,9 +75,9 @@ StreamGear primarily operates in following independent modes for transcoding: Rather, you can enable live-streaming in Real-time Frames Mode by using using 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**](../ssm/overview): In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated 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 video-frames _(as opposed to a entire file)_, into a sequence of multiple smaller chunks/segments for streaming. In this mode, StreamGear supports real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames, and process them over FFmpeg pipeline. But on the downside, audio has to added manually _(as separate source)_ for streams. +- [**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.   @@ -125,15 +132,6 @@ from vidgear.gears import StreamGear ## Recommended Players -!!! 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 comparsion. - - === "GUI Players" - [x] **[MPV Player](https://mpv.io/):** _(recommended)_ MPV is a free, open source, and cross-platform media player. It supports a wide variety of media file formats, audio and video codecs, and subtitle types. - [x] **[VLC Player](https://www.videolan.org/vlc/releases/3.0.0.html):** VLC is a free and open source cross-platform multimedia player and framework that plays most multimedia files as well as DVDs, Audio CDs, VCDs, and various streaming protocols. diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index d1c07ab82..0f8649233 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -29,13 +29,13 @@ limitations under the License. ## Overview -When no valid input is received on [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, StreamGear API activates this mode where it directly transcodes 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. +When no valid input is received on [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, StreamGear API activates this mode where it directly transcodes real-time [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) video-frames _(as opposed to a entire video file)_ into a sequence of multiple smaller chunks/segments for adaptive streaming. -SteamGear 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)_ with this mode. +This mode works exceptionally well when you desire to flexibility manipulate or transform video-frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, StreamGear **DOES NOT** automatically maps video-source's audio to generated streams with this mode. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. -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. +SteamGear 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)_ with this mode. -This mode provide [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function for directly trancoding video-frames into streamable chunks over the FFmpeg pipeline. +For this mode, StreamGear API provides exclusive [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) method for directly trancoding video-frames into streamable chunks.   diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 99d985a04..b71ba4084 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -21,18 +21,20 @@ limitations under the License. # StreamGear API: Single-Source Mode
- StreamGear Flow Diagram -
StreamGear API's generalized workflow
+ Single-Source Mode Flow Diagram +
Single-Source Mode generalized workflow
## Overview -In this mode, StreamGear transcodes entire video/audio file _(as opposed to frames by frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well, when you're transcoding lossless long-duration videos(with audio) for streaming and required no extra efforts or interruptions. But on the downside, the provided source cannot be changed or manipulated before sending onto FFmpeg Pipeline for processing. +In this mode, StreamGear transcodes entire audio-video file _(as opposed to frames-by-frame)_ into a sequence of multiple smaller chunks/segments for adaptive streaming. + +This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) files for streaming that requires no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. SteamGear 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)_ with this mode. -This mode provide [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function to process audio-video files into streamable chunks. +For this mode, StreamGear API provides exclusive [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) method to easily process audio-video files into streamable chunks. This mode can be easily activated by assigning suitable video path as input to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#stream_params) dictionary parameter, during StreamGear initialization. diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index a29751622..ff7aea9dd 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -74,7 +74,7 @@ web.shutdown() !!! new "New in v0.2.1" This example was added in `v0.2.1`. -WebGear allows you to easily define your own custom Source that you want to use to manipulate your frames before sending them onto the browser. +WebGear allows you to easily define your own custom Source that you want to use to transform your frames before sending them onto the browser. !!! warning "JPEG Frame-Compression and all of its [performance enhancing attributes](../usage/#performance-enhancements) are disabled with a Custom Source!" diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index 99e094c61..5ca6f1624 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -64,7 +64,7 @@ web.shutdown() ## Using WebGear_RTC with a Custom Source(OpenCV) -WebGear_RTC allows you to easily define your own Custom Media Server with a custom source that you want to use to manipulate your frames before sending them onto the browser. +WebGear_RTC allows you to easily define your own Custom Media Server with a custom source that you want to use to transform your frames before sending them onto the browser. Let's implement a bare-minimum example with a Custom Source using WebGear_RTC API and OpenCV: diff --git a/docs/gears/webgear_rtc/overview.md b/docs/gears/webgear_rtc/overview.md index df6d7cb39..d7087f9aa 100644 --- a/docs/gears/webgear_rtc/overview.md +++ b/docs/gears/webgear_rtc/overview.md @@ -34,7 +34,7 @@ limitations under the License. WebGear_RTC is implemented with the help of [**aiortc**](https://aiortc.readthedocs.io/en/latest/) library which is built on top of asynchronous I/O framework for Web Real-Time Communication (WebRTC) and Object Real-Time Communication (ORTC) and supports many features like SDP generation/parsing, Interactive Connectivity Establishment with half-trickle and mDNS support, DTLS key and certificate generation, DTLS handshake, etc. -WebGear_RTC can handle [multiple consumers](../../webgear_rtc/advanced/#using-webgear_rtc-as-real-time-broadcaster) seamlessly and provides native support for ICE _(Interactive Connectivity Establishment)_ protocol, STUN _(Session Traversal Utilities for NAT)_, and TURN _(Traversal Using Relays around NAT)_ servers that help us to easily establish direct media connection with the remote peers for uninterrupted data flow. It also allows us to define our custom Server as a source to manipulate frames easily before sending them across the network(see this [doc](../../webgear_rtc/advanced/#using-webgear_rtc-with-a-custom-sourceopencv) example). +WebGear_RTC can handle [multiple consumers](../../webgear_rtc/advanced/#using-webgear_rtc-as-real-time-broadcaster) seamlessly and provides native support for ICE _(Interactive Connectivity Establishment)_ protocol, STUN _(Session Traversal Utilities for NAT)_, and TURN _(Traversal Using Relays around NAT)_ servers that help us to easily establish direct media connection with the remote peers for uninterrupted data flow. It also allows us to define our custom Server as a source to transform frames easily before sending them across the network(see this [doc](../../webgear_rtc/advanced/#using-webgear_rtc-with-a-custom-sourceopencv) example). WebGear_RTC API works in conjunction with [**Starlette**](https://www.starlette.io/) ASGI application and can also flexibly interact with Starlette's ecosystem of shared middleware, mountable applications, [Response classes](https://www.starlette.io/responses/), [Routing tables](https://www.starlette.io/routing/), [Static Files](https://www.starlette.io/staticfiles/), [Templating engine(with Jinja2)](https://www.starlette.io/templates/), etc. diff --git a/docs/gears/writegear/introduction.md b/docs/gears/writegear/introduction.md index 5deb1010e..2e5d7e609 100644 --- a/docs/gears/writegear/introduction.md +++ b/docs/gears/writegear/introduction.md @@ -45,7 +45,7 @@ WriteGear primarily operates in following modes: * [**Compression Mode**](../compression/overview/): In this mode, WriteGear utilizes powerful **FFmpeg** inbuilt encoders to encode lossless multimedia files. This mode provides us the ability to exploit almost any parameter available within FFmpeg, effortlessly and flexibly, and while doing that it robustly handles all errors/warnings quietly. -* [**Non-Compression Mode**](../non_compression/overview/): In this mode, WriteGear utilizes basic **OpenCV's inbuilt VideoWriter API** tools. This mode also supports all parameters manipulation available within VideoWriter API, but it lacks the ability to manipulate encoding parameters and other important features like video compression, audio encoding, etc. +* [**Non-Compression Mode**](../non_compression/overview/): In this mode, WriteGear utilizes basic **OpenCV's inbuilt VideoWriter API** tools. This mode also supports all parameter transformations available within OpenCV's VideoWriter API, but it lacks the ability to manipulate encoding parameters and other important features like video compression, audio encoding, etc.   diff --git a/docs/overrides/assets/images/stream_tweak.png b/docs/overrides/assets/images/stream_tweak.png new file mode 100644 index 0000000000000000000000000000000000000000..6a956fd32cfdd318f61ac68ecbaa9617c5b0cd88 GIT binary patch literal 44390 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 Xn2|?2v{t+X0UsrKb-8L8^RWK|nl$TR literal 0 HcmV?d00001 diff --git a/docs/switch_from_cv.md b/docs/switch_from_cv.md index 3bda0e224..e01aa7b2c 100644 --- a/docs/switch_from_cv.md +++ b/docs/switch_from_cv.md @@ -43,7 +43,7 @@ Switching OpenCV with VidGear APIs is fairly painless process, and will just req VidGear employs OpenCV at its backend and enhances its existing capabilities even further by introducing many new state-of-the-art functionalities such as: - [x] Accelerated [Multi-Threaded](../bonus/TQM/#what-does-threaded-queue-mode-exactly-do) Performance. -- [x] Out-of-the-box support for OpenCV API. +- [x] Out-of-the-box support for OpenCV APIs. - [x] Real-time [Stabilization](../gears/stabilizer/overview/) ready. - [x] Lossless hardware enabled video [encoding](../gears/writegear/compression/usage/#using-compression-mode-with-hardware-encoders) and [transcoding](../gears/streamgear/rtfm/usage/#usage-with-hardware-video-encoder). - [x] Inherited multi-backend support for various video sources and devices. diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index 3547d3d44..e6abce228 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -54,7 +54,7 @@ class NetGear_Async: system. NetGear_Async provides complete server-client handling and options to use variable protocols/patterns similar to NetGear API. Furthermore, NetGear_Async allows us to define - our custom Server as source to manipulate frames easily before sending them across the network. + our custom Server as source to transform frames easily before sending them across the network. NetGear_Async now supports additional [**bidirectional data transmission**](../advanced/bidirectional_mode) between receiver(client) and sender(server) while transferring frames. Users can easily build complex applications such as like [Real-Time Video Chat](../advanced/bidirectional_mode/#using-bidirectional-mode-for-video-frames-transfer) in just few lines of code. diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index c8b6efc72..bdb59a688 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -287,7 +287,7 @@ class WebGear_RTC: WebGear_RTC can handle multiple consumers seamlessly and provides native support for ICE (Interactive Connectivity Establishment) protocol, STUN (Session Traversal Utilities for NAT), and TURN (Traversal Using Relays around NAT) servers that help us to easily establish direct media connection with the remote peers for uninterrupted data flow. It also allows us to define our custom Server - as a source to manipulate frames easily before sending them across the network(see this doc example). + as a source to transform frames easily before sending them across the network(see this doc example). WebGear_RTC API works in conjunction with Starlette ASGI application and can also flexibly interact with Starlette's ecosystem of shared middleware, mountable applications, Response classes, Routing tables, Static Files, Templating engine(with Jinja2), etc. From a82d5af0848978b1f84fc128551c78648867d6b5 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Mon, 23 Aug 2021 22:51:23 +0530 Subject: [PATCH 104/112] =?UTF-8?q?=F0=9F=9A=B8=20Docs:=20Added=20`pip`=20?= =?UTF-8?q?update=20instructions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/installation/pip_install.md | 29 +++++++++++++++++++++++++++++ docs/installation/source_install.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 8d6af3436..1b9b66663 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -29,6 +29,32 @@ limitations under the License. When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), you need to check manually if following dependencies are installed: +??? alert "Latest `pip` Recommended" + + It advised to install latest `pip` version before installing vidgear to avoid any undesired errors. Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. + + === "Linux" + + ```sh + python -m ensurepip --upgrade + + ``` + + === "MacOS" + + ```sh + python -m ensurepip --upgrade + + ``` + + === "Windows" + + ```sh + py -m ensurepip --upgrade + + ``` + + ### Core Prerequisites * #### OpenCV @@ -50,6 +76,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y pip install opencv-python ``` + ### API Specific Prerequisites * #### FFmpeg @@ -162,3 +189,5 @@ pip install vidgear-0.2.2-py3-none-any.whl[asyncio] ```   + +[^1]: The `ensurepip` module was added to the Python standard library in Python 3.4. \ No newline at end of file diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index f71a97e09..d8b299be4 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -31,6 +31,32 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec !!! question "What about rest of the dependencies?" Any other python dependencies _(Core/API specific)_ will be automatically installed based on your OS specifications. + + +??? alert "Latest `pip` Recommended" + + It advised to install latest `pip` version before installing vidgear to avoid any undesired errors. Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. + + === "Linux" + + ```sh + python -m ensurepip --upgrade + + ``` + + === "MacOS" + + ```sh + python -m ensurepip --upgrade + + ``` + + === "Windows" + + ```sh + py -m ensurepip --upgrade + + ``` ### API Specific Prerequisites @@ -123,3 +149,6 @@ pip install git+git://github.com/abhiTronix/vidgear@testing#egg=vidgear[asyncio] ```   + + +[^1]: The `ensurepip` module was added to the Python standard library in Python 3.4. From 3ab426f015656082a3150291d8b129565d4de187 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 31 Aug 2021 10:24:02 +0530 Subject: [PATCH 105/112] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20VidGear=20Core:=20?= =?UTF-8?q?Virtually=20isolated=20API=20specific=20dependencies=20(Fixes?= =?UTF-8?q?=20#242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ⚡️ New behavior to virtually isolate optional API specific dependencies by silencing `ImportError` on all VidGear's APIs import. - 🎨 Implemented algorithm to cache all imports on startup but silence any `ImportError` on missing optional dependency. - ⚠️ Now `ImportError` will be raised only when certain API specific dependency is missing during given API's initialization. - ✨ New `import_dependency_safe` to imports specified dependency safely with `importlib` module. - ⚡️Replaced all APIs imports with `import_dependency_safe`. - ⚡️ Added support for relative imports in `import_dependency_safe`. - ✨ Implemented `error` parameter to by default `ImportError` with a meaningful message if a dependency is missing, Otherwise if `error = log` a warning will be logged and on `error = silent` everything will be quit. But If a dependency is present, but older than specified, an error is raised if specified. - ✨ Implemented behavior that if a dependency is present, but older than `min_version` specified, an error is raised always. - ✨ Implemented `custom_message` to display custom message on error instead of default one. - 🔥 Removed redundant `logger_handler`, `mkdir_safe`, `retrieve_best_interpolation`, `capPropId` helper functions from asyncio package. - 🎨 Relatively imported helper functions from non-asyncio package. - ⚡️ Implemented separate `import_core_dependency` function to import and check for specified core dependency. `ImportError` will be raised immediately if core dependency not found. - Setup.py: - 🔥 Removed version check on certain dependencies. - ⏪️ Re-added `aiortc` to auto-install latest version. - Docs: - 📝 Updated URL and context for CamGear example. - ✏️ Fixed typos in usage examples. - 💡 Updated code comments. - 📝 Other Minor updates. - CI: 👷 Imported correct `logger_handler` for asyncio tests. - Codecov: ☂️ Added `__init__.py` to ignore. --- codecov.yml | 1 + docs/gears/camgear/usage.md | 6 +- .../netgear/advanced/bidirectional_mode.md | 6 +- docs/gears/netgear/advanced/compression.md | 4 +- docs/gears/netgear/advanced/multi_client.md | 12 +- docs/gears/netgear/advanced/multi_server.md | 15 +- docs/gears/netgear/advanced/secure_mode.md | 6 +- docs/gears/netgear/advanced/ssh_tunnel.md | 6 +- docs/gears/screengear/usage.md | 4 +- docs/gears/webgear/advanced.md | 8 +- docs/gears/webgear_rtc/advanced.md | 2 +- setup.py | 12 +- vidgear/gears/__init__.py | 164 +++++++++++++++++- vidgear/gears/asyncio/__init__.py | 1 + vidgear/gears/asyncio/__main__.py | 2 +- vidgear/gears/asyncio/helper.py | 109 +----------- vidgear/gears/asyncio/netgear_async.py | 34 ++-- vidgear/gears/asyncio/webgear.py | 33 ++-- vidgear/gears/asyncio/webgear_rtc.py | 54 +++--- vidgear/gears/camgear.py | 14 +- vidgear/gears/helper.py | 150 ++++++++++++++-- vidgear/gears/netgear.py | 56 +++--- vidgear/gears/pigear.py | 33 ++-- vidgear/gears/screengear.py | 20 ++- vidgear/gears/stabilizer.py | 4 +- vidgear/gears/streamgear.py | 4 +- vidgear/gears/videogear.py | 3 + vidgear/gears/writegear.py | 10 +- .../asyncio_tests/test_helper.py | 2 +- .../asyncio_tests/test_netgear_async.py | 2 +- .../asyncio_tests/test_webgear.py | 2 +- .../asyncio_tests/test_webgear_rtc.py | 2 +- 32 files changed, 494 insertions(+), 287 deletions(-) diff --git a/codecov.yml b/codecov.yml index 544e750c8..7cef6987d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -30,5 +30,6 @@ ignore: - "vidgear/tests" - "docs" - "scripts" + - "vidgear/gears/__init__.py" #trivial - "vidgear/gears/asyncio/__main__.py" #trivial - "setup.py" \ No newline at end of file diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index d2b377c13..0583fe944 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -66,7 +66,7 @@ stream.stop() ## Using Camgear with Streaming Websites -CamGear API provides direct support for piping video streams from various popular streaming services like [Twitch](https://www.twitch.tv/), [Livestream](https://livestream.com/), [Dailymotion](https://www.dailymotion.com/live), and [many more ➶](https://streamlink.github.io/plugin_matrix.html#plugins). All you have to do is to provide the desired Video's URL to its `source` parameter, and enable the [`stream_mode`](../params/#stream_mode) parameter. The complete usage example is as follows: +CamGear API provides direct support for piping video streams from various popular streaming services like [Twitch](https://www.twitch.tv/), [Vimeo](https://vimeo.com/), [Dailymotion](https://www.dailymotion.com), and [many more ➶](https://streamlink.github.io/plugin_matrix.html#plugins). All you have to do is to provide the desired Video's URL to its `source` parameter, and enable the [`stream_mode`](../params/#stream_mode) parameter. The complete usage example is as follows: !!! bug "Bug in OpenCV's FFmpeg" To workaround a [**FFmpeg bug**](https://github.com/abhiTronix/vidgear/issues/133#issuecomment-638263225) that causes video to freeze frequently, You must always use [GStreamer backend](../params/#backend) for Livestreams _(such as Twitch URLs)_. @@ -90,10 +90,10 @@ import cv2 options = {"STREAM_RESOLUTION": "720p"} # Add any desire Video URL as input source -# for e.g https://www.dailymotion.com/video/x7xsoud +# for e.g https://vimeo.com/151666798 # and enable Stream Mode (`stream_mode = True`) stream = CamGear( - source="https://www.dailymotion.com/video/x7xsoud", + source="https://vimeo.com/151666798", stream_mode=True, logging=True, **options diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index 6ea1df99a..3f8ac47ac 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -36,7 +36,7 @@ This mode can be easily activated in NetGear through `bidirectional_mode` attrib   -!!! danger "Important" +!!! danger "Important Information regarding Bidirectional Mode" * In Bidirectional Mode, `zmq.PAIR`(ZMQ Pair) & `zmq.REQ/zmq.REP`(ZMQ Request/Reply) are **ONLY** Supported messaging patterns. Accessing this mode with any other messaging pattern, will result in `ValueError`. @@ -69,7 +69,7 @@ This mode can be easily activated in NetGear through `bidirectional_mode` attrib   -## Method Parameters +## Exclusive Parameters To send data bidirectionally, NetGear API provides two exclusive parameters for its methods: @@ -364,7 +364,7 @@ server.close() In this example we are going to implement a bare-minimum example, where we will be sending video-frames _(3-Dimensional numpy arrays)_ of the same Video bidirectionally at the same time, for testing the real-time performance and synchronization between the Server and the Client using this(Bidirectional) Mode. -!!! tip "This feature is great for building applications like Real-Time Video Chat." +!!! tip "This example is useful for building applications like Real-Time Video Chat." !!! info "We're also using [`reducer()`](../../../../../bonus/reference/helper/#vidgear.gears.helper.reducer--reducer) method for reducing frame-size on-the-go for additional performance." diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index d09bf2070..21e6c00c8 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -49,9 +49,9 @@ Frame Compression is enabled by default in NetGear, and can be easily controlled   -## Supported Attributes +## Exclusive Attributes -For implementing Frame Compression, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter to leverage performance with Frame Compression: +For implementing Frame Compression, NetGear API currently provide following exclusive attribute for its [`options`](../../params/#options) dictionary parameter to leverage performance with Frame Compression: * `jpeg_compression`: _(bool/str)_ This internal attribute is used to activate/deactivate JPEG Frame Compression as well as to specify incoming frames colorspace with compression. Its usage is as follows: diff --git a/docs/gears/netgear/advanced/multi_client.md b/docs/gears/netgear/advanced/multi_client.md index b8ab39be1..9c7e6016f 100644 --- a/docs/gears/netgear/advanced/multi_client.md +++ b/docs/gears/netgear/advanced/multi_client.md @@ -37,7 +37,7 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a   -!!! danger "Multi-Clients Mode Requirements" +!!! danger "Important Information regarding Multi-Clients Mode" * A unique PORT address **MUST** be assigned to each Client on the network using its [`port`](../../params/#port) parameter. @@ -45,6 +45,8 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a * Patterns `1` _(i.e. Request/Reply `zmq.REQ/zmq.REP`)_ and `2` _(i.e. Publish/Subscribe `zmq.PUB/zmq.SUB`)_ are the only supported pattern values for this Mode. Therefore, calling any other pattern value with is mode will result in `ValueError`. + * Multi-Clients and Multi-Servers exclusive modes **CANNOT** be enabled simultaneously, Otherwise NetGear API will throw `ValueError`. + * The [`address`](../../params/#address) parameter value of each Client **MUST** exactly match the Server.   @@ -71,12 +73,10 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a ## Usage Examples -!!! alert "Important Information" +!!! alert "Important" * ==Frame/Data transmission will **NOT START** until all given Client(s) are connected to the Server.== - * Multi-Clients and Multi-Servers exclusive modes **CANNOT** be enabled simultaneously, Otherwise NetGear API will throw `ValueError`. - * For sake of simplicity, in these examples we will use only two unique Clients, but the number of these Clients can be extended to **SEVERAL** numbers depending upon your Network bandwidth and System Capabilities. @@ -85,7 +85,9 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a ### Bare-Minimum Usage -In this example, we will capturing live video-frames from a source _(a.k.a Servers)_ with a webcam connected to it. Afterwards, those captured frame will be transferred over the network to a two independent system _(a.k.a Client)_ at the same time, and will be displayed in Output Window at real-time. All this by using this Multi-Clients Mode in NetGear API. +In this example, we will capturing live video-frames from a source _(a.k.a Server)_ with a webcam connected to it. Afterwards, those captured frame will be sent over the network to two independent system _(a.k.a Clients)_ using this Multi-Clients Mode in NetGear API. Finally, both Clients will be displaying recieved frames in Output Windows in real time. + +!!! tip "This example is useful for building applications like Real-Time Video Broadcasting to multiple clients in local network." #### Server's End diff --git a/docs/gears/netgear/advanced/multi_server.md b/docs/gears/netgear/advanced/multi_server.md index 8d6dc61c1..d0fa4caaa 100644 --- a/docs/gears/netgear/advanced/multi_server.md +++ b/docs/gears/netgear/advanced/multi_server.md @@ -35,7 +35,7 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a   -!!! danger "Multi-Servers Mode Requirements" +!!! danger "Important Information regarding Multi-Servers Mode" * A unique PORT address **MUST** be assigned to each Server on the network using its [`port`](../../params/#port) parameter. @@ -43,6 +43,8 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a * Patterns `1` _(i.e. Request/Reply `zmq.REQ/zmq.REP`)_ and `2` _(i.e. Publish/Subscribe `zmq.PUB/zmq.SUB`)_ are the only supported values for this Mode. Therefore, calling any other pattern value with is mode will result in `ValueError`. + * Multi-Servers and Multi-Clients exclusive modes **CANNOT** be enabled simultaneously, Otherwise NetGear API will throw `ValueError`. + * The [`address`](../../params/#address) parameter value of each Server **MUST** exactly match the Client.   @@ -68,23 +70,24 @@ The supported patterns for this mode are Publish/Subscribe (`zmq.PUB/zmq.SUB`) a ## Usage Examples -!!! alert "Important Information" +!!! alert "Example Assumptions" * For sake of simplicity, in these examples we will use only two unique Servers, but, the number of these Servers can be extended to several numbers depending upon your system hardware limits. - * All of Servers will be transferring frames to a single Client system at the same time, which will be displaying received frames as a montage _(multiple frames concatenated together)_. + * All of Servers will be transferring frames to a single Client system at the same time, which will be displaying received frames as a live montage _(multiple frames concatenated together)_. * For building Frames Montage at Client's end, We are going to use `imutils` python library function to build montages, by concatenating together frames recieved from different servers. Therefore, Kindly install this library with `pip install imutils` terminal command. - * Multi-Servers and Multi-Clients exclusive modes **CANNOT** be enabled simultaneously, Otherwise NetGear API will throw `ValueError`. -   ### Bare-Minimum Usage -In this example, we will capturing live video-frames on two independent sources _(a.k.a Servers)_, each with a webcam connected to it. Then, those frames will be transferred over the network to a single system _(a.k.a Client)_ at the same time, and will be displayed as a real-time montage. All this by using this Multi-Servers Mode in NetGear API. +In this example, we will capturing live video-frames on two independent sources _(a.k.a Servers)_, each with a webcam connected to it. Afterwards, these frames will be sent over the network to a single system _(a.k.a Client)_ using this Multi-Servers Mode in NetGear API in real time, and will be displayed as a live montage. + + +!!! tip "This example is useful for building applications like Real-Time Security System with multiple cameras." #### Client's End diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 1fee3c798..200ebd92a 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -48,7 +48,7 @@ Secure mode supports the two most powerful ZMQ security layers:   -!!! danger "Secure Mode Requirements" +!!! 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**)_. @@ -83,9 +83,9 @@ Secure mode supports the two most powerful ZMQ security layers:   -## Supported Attributes +## Exclusive Attributes -For implementing Secure Mode, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter: +For implementing Secure Mode, NetGear API currently provide following exclusive attribute for its [`options`](../../params/#options) dictionary parameter: * `secure_mode` (_integer_) : This attribute activates and sets the ZMQ security Mechanism. Its possible values are: `1`(_StoneHouse_) & `2`(_IronHouse_), and its default value is `0`(_Grassland(no security)_). Its usage is as follows: diff --git a/docs/gears/netgear/advanced/ssh_tunnel.md b/docs/gears/netgear/advanced/ssh_tunnel.md index 150d79fa8..8b0d41a5c 100644 --- a/docs/gears/netgear/advanced/ssh_tunnel.md +++ b/docs/gears/netgear/advanced/ssh_tunnel.md @@ -80,12 +80,12 @@ SSH Tunnel Mode requires [`pexpect`](http://www.noah.org/wiki/pexpect) or [`para   -## Supported Attributes +## Exclusive Attributes !!! warning "All these attributes will work on Server end only whereas Client end will simply discard them." -For implementing SSH Tunneling Mode, NetGear API currently provide following attribute for its [`options`](../../params/#options) dictionary parameter: +For implementing SSH Tunneling Mode, NetGear API currently provide following exclusive attribute for its [`options`](../../params/#options) dictionary parameter: * **`ssh_tunnel_mode`** (_string_) : This attribute activates SSH Tunneling Mode and sets the fully specified `"@:"` SSH URL for tunneling at Server end. Its usage is as follows: @@ -138,7 +138,7 @@ For implementing SSH Tunneling Mode, NetGear API currently provide following att ## Usage Example -??? alert "Assumptions for this Example" +???+ alert "Assumptions for this Example" In this particular example, we assume that: diff --git a/docs/gears/screengear/usage.md b/docs/gears/screengear/usage.md index c8ccfa8ec..dea324021 100644 --- a/docs/gears/screengear/usage.md +++ b/docs/gears/screengear/usage.md @@ -122,7 +122,7 @@ from vidgear.gears import ScreenGear import cv2 # open video stream with defined parameters with monitor at index `1` selected -stream = ScreenGear(monitor=1, logging=True, **options).start() +stream = ScreenGear(monitor=1, logging=True).start() # loop over while True: @@ -167,7 +167,7 @@ from vidgear.gears import ScreenGear import cv2 # open video stream with defined parameters and `mss` backend for extracting frames. -stream = ScreenGear(backend="mss", logging=True, **options).start() +stream = ScreenGear(backend="mss", logging=True).start() # loop over while True: diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index ff7aea9dd..507f42fc5 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -42,7 +42,7 @@ Let's implement a bare-minimum example using WebGear, where we will be sending [ ```python # import required libraries import uvicorn -from vidgear.gears.asyncio import WebGear_RTC +from vidgear.gears.asyncio import WebGear # various performance tweaks and enable grayscale input options = { @@ -53,8 +53,8 @@ options = { "jpeg_compression_fastupsample": True, } -# initialize WebGear_RTC app and change its colorspace to grayscale -web = WebGear_RTC( +# initialize WebGear app and change its colorspace to grayscale +web = WebGear( source="foo.mp4", colorspace="COLOR_BGR2GRAY", logging=True, **options ) @@ -248,7 +248,7 @@ WebGear natively supports ASGI middleware classes with Starlette for implementin !!! new "New in v0.2.2" This example was added in `v0.2.2`. -!!! info "All supported middlewares can be [here ➶](https://www.starlette.io/middleware/)" +!!! info "All supported middlewares can be found [here ➶](https://www.starlette.io/middleware/)" For this example, let's use [`CORSMiddleware`](https://www.starlette.io/middleware/#corsmiddleware) for implementing appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to outgoing responses in our application in order to allow cross-origin requests from browsers, as follows: diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index 5ca6f1624..da0887954 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -262,7 +262,7 @@ WebGear_RTC also natively supports ASGI middleware classes with Starlette for im !!! new "New in v0.2.2" This example was added in `v0.2.2`. -!!! info "All supported middlewares can be [here ➶](https://www.starlette.io/middleware/)" +!!! info "All supported middlewares can be found [here ➶](https://www.starlette.io/middleware/)" For this example, let's use [`CORSMiddleware`](https://www.starlette.io/middleware/#corsmiddleware) for implementing appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to outgoing responses in our application in order to allow cross-origin requests from browsers, as follows: diff --git a/setup.py b/setup.py index 9185b83db..af187226f 100644 --- a/setup.py +++ b/setup.py @@ -91,11 +91,9 @@ def latest_version(package_name): install_requires=[ "pafy{}".format(latest_version("pafy")), "mss{}".format(latest_version("mss")), - "numpy{}".format( - "<=1.19.5" if sys.version_info[:2] < (3, 7) else "" - ), # dropped support for 3.6.x legacies + "numpy", "youtube-dl{}".format(latest_version("youtube-dl")), - "streamlink{}".format(latest_version("streamlink")), + "streamlink", "requests", "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format(latest_version("simplejpeg")), @@ -119,12 +117,8 @@ def latest_version(package_name): "aiohttp", "uvicorn{}".format(latest_version("uvicorn")), "msgpack_numpy", + "aiortc{}".format(latest_version("aiortc")), ] - + ( - ["aiortc{}".format(latest_version("aiortc"))] - if (platform.system() != "Windows") - else [] - ) + ( ( ["uvloop{}".format(latest_version("uvloop"))] diff --git a/vidgear/gears/__init__.py b/vidgear/gears/__init__.py index 766af43ce..5fb7f212a 100644 --- a/vidgear/gears/__init__.py +++ b/vidgear/gears/__init__.py @@ -1,8 +1,168 @@ # import the necessary packages -from .pigear import PiGear +import sys +import types +import logging +import importlib +from distutils.version import LooseVersion + +# define custom logger +FORMAT = "%(name)s :: %(levelname)s :: %(message)s" +logging.basicConfig(format=FORMAT) +logger = logging.getLogger("VidGear CORE") +logger.propagate = False + + +def get_module_version(module=None): + """ + ## get_module_version + + Retrieves version of specified module + + Parameters: + name (ModuleType): module of datatype `ModuleType`. + + **Returns:** version of specified module as string + """ + # check if module type is valid + assert not (module is None) and isinstance( + module, types.ModuleType + ), "[VidGear CORE:ERROR] :: Invalid module!" + + # get version from attribute + version = getattr(module, "__version__", None) + # retry if failed + if version is None: + # some modules uses a capitalized attribute name + version = getattr(module, "__VERSION__", None) + # raise if still failed + if version is None: + raise ImportError( + "[VidGear CORE:ERROR] :: Can't determine version for module: `{}`!".format( + module.__name__ + ) + ) + return str(version) + + +def import_core_dependency( + name, pkg_name=None, custom_message=None, version=None, mode="gte" +): + """ + ## import_core_dependency + + Imports specified core dependency. By default(`error = raise`), if a dependency is missing, + an ImportError with a meaningful message will be raised. Also, If a dependency is present, + but version is different than specified, an error is raised. + + Parameters: + name (string): name of dependency to be imported. + pkg_name (string): (Optional) package name of dependency(if different `pip` name). Otherwise `name` will be used. + custom_message (string): (Optional) custom Import error message to be raised or logged. + version(string): (Optional) required minimum/maximum version of the dependency to be imported. + mode(boolean): (Optional) Possible values "gte"(greater then equal), "lte"(less then equal), "exact"(exact). Default is "gte". + + **Returns:** `None` + """ + # check specified parameters + assert name and isinstance( + name, str + ), "[VidGear CORE:ERROR] :: Kindly provide name of the dependency." + + # extract name in case of relative import + sub_class = "" + name = name.strip() + if name.startswith("from"): + name = name.split(" ") + name, sub_class = (name[1].strip(), name[-1].strip()) + + # check mode of operation + assert mode in ["gte", "lte", "exact"], "[VidGear CORE:ERROR] :: Invalid mode!" + + # specify package name of dependency(if defined). Otherwise use name + install_name = pkg_name if not (pkg_name is None) else name + + # create message + msg = ( + custom_message + if not (custom_message is None) + else "Failed to find its core dependency '{}'. Install it with `pip install {}` command.".format( + name, install_name + ) + ) + # 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 + + # check if minimum required version + if not (version) is None: + # Handle submodules + parent_module = name.split(".")[0] + if parent_module != name: + # grab parent module + module_to_get = sys.modules[parent_module] + else: + module_to_get = module + + # extract version + module_version = get_module_version(module_to_get) + # verify + if mode == "exact": + if LooseVersion(module_version) != LooseVersion(version): + # create message + msg = "Unsupported version '{}' found. Vidgear requires '{}' dependency with exact version '{}' installed!".format( + module_version, parent_module, version + ) + # raise + raise ImportError(msg) + elif mode == "lte": + if LooseVersion(module_version) > LooseVersion(version): + # create message + msg = "Unsupported version '{}' found. Vidgear requires '{}' dependency installed with older version '{}' or smaller!".format( + module_version, parent_module, version + ) + # raise + raise ImportError(msg) + else: + if LooseVersion(module_version) < LooseVersion(version): + # create message + msg = "Unsupported version '{}' found. Vidgear requires '{}' dependency installed with newer version '{}' or greater!".format( + module_version, parent_module, version + ) + # raise + raise ImportError(msg) + return module + + +# import core dependencies +import_core_dependency( + "cv2", + pkg_name="opencv-python", + version="3", + custom_message="Failed to find core dependency '{}'. Install it with `pip install opencv-python` command.", +) +import_core_dependency( + "numpy", + version="1.19.5" + if sys.version_info[:2] < (3, 7) + else None, # dropped support for 3.6.x legacies + mode="lte", +) +import_core_dependency( + "colorlog", +) +import_core_dependency("requests") +import_core_dependency("from tqdm import tqdm", pkg_name="tqdm") + +# import all APIs from .camgear import CamGear -from .netgear import NetGear +from .pigear import PiGear from .videogear import VideoGear +from .netgear import NetGear from .writegear import WriteGear from .screengear import ScreenGear from .streamgear import StreamGear diff --git a/vidgear/gears/asyncio/__init__.py b/vidgear/gears/asyncio/__init__.py index 2b6a0e845..0febd8916 100644 --- a/vidgear/gears/asyncio/__init__.py +++ b/vidgear/gears/asyncio/__init__.py @@ -1,3 +1,4 @@ +# import all APIs from .webgear import WebGear from .webgear_rtc import WebGear_RTC from .netgear_async import NetGear_Async diff --git a/vidgear/gears/asyncio/__main__.py b/vidgear/gears/asyncio/__main__.py index 67b07704e..cd8bcef52 100644 --- a/vidgear/gears/asyncio/__main__.py +++ b/vidgear/gears/asyncio/__main__.py @@ -19,7 +19,7 @@ """ if __name__ == "__main__": - # import libs + # import neccessary libs import yaml import argparse diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index a88611b02..ab6abcad6 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -21,7 +21,6 @@ # Contains all the support functions/modules required by Vidgear Asyncio packages # import the necessary packages - import os import cv2 import sys @@ -37,51 +36,8 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry - -def logger_handler(): - """ - ## logger_handler - - Returns the logger handler - - **Returns:** A logger handler - """ - # logging formatter - formatter = ColoredFormatter( - "%(bold_cyan)s%(asctime)s :: %(bold_blue)s%(name)s%(reset)s :: %(log_color)s%(levelname)s%(reset)s :: %(message)s", - datefmt="%H:%M:%S", - reset=False, - log_colors={ - "INFO": "bold_green", - "DEBUG": "bold_yellow", - "WARNING": "bold_purple", - "ERROR": "bold_red", - "CRITICAL": "bold_red,bg_white", - }, - ) - # check if VIDGEAR_LOGFILE defined - file_mode = os.environ.get("VIDGEAR_LOGFILE", False) - # define handler - handler = log.StreamHandler() - if file_mode and isinstance(file_mode, str): - file_path = os.path.abspath(file_mode) - if (os.name == "nt" or os.access in os.supports_effective_ids) and os.access( - os.path.dirname(file_path), os.W_OK - ): - file_path = ( - os.path.join(file_path, "vidgear.log") - if os.path.isdir(file_path) - else file_path - ) - handler = log.FileHandler(file_path, mode="a") - formatter = log.Formatter( - "%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s", - datefmt="%H:%M:%S", - ) - - handler.setFormatter(formatter) - return handler - +# import helper packages +from ..helper import logger_handler, mkdir_safe # define logger logger = log.getLogger("Helper Asyncio") @@ -113,67 +69,6 @@ def send(self, request, **kwargs): return super().send(request, **kwargs) -def mkdir_safe(dir, logging=False): - """ - ## mkdir_safe - - Safely creates directory at given path. - - Parameters: - logging (bool): enables logging for its operations - - """ - try: - os.makedirs(dir) - if logging: - logger.debug("Created directory at `{}`".format(dir)) - except OSError as e: - if e.errno != errno.EEXIST: - raise - if logging: - logger.debug("Directory already exists at `{}`".format(dir)) - - -def capPropId(property, logging=True): - """ - ## capPropId - - Retrieves the OpenCV property's Integer(Actual) value from string. - - Parameters: - property (string): inputs OpenCV property as string. - logging (bool): enables logging for its operations - - **Returns:** Resultant integer value. - """ - integer_value = 0 - 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)) - return None - return integer_value - - -def retrieve_best_interpolation(interpolations): - """ - ## retrieve_best_interpolation - Retrieves best interpolation for resizing - - Parameters: - interpolations (list): list of interpolations as string. - **Returns:** Resultant integer value of found interpolation. - """ - if isinstance(interpolations, list): - for intp in interpolations: - interpolation = capPropId(intp, logging=False) - if not (interpolation is None): - return interpolation - return None - - def create_blank_frame(frame=None, text="", logging=False): """ ## create_blank_frame diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index e6abce228..ddb10b993 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -18,25 +18,31 @@ =============================================== """ # import the necessary packages - import cv2 import sys -import zmq import numpy as np import asyncio import inspect import logging as log -import msgpack import string import secrets import platform -import zmq.asyncio -import msgpack_numpy as m from collections import deque -from .helper import logger_handler +# import helper packages +from ..helper import logger_handler, import_dependency_safe + +# import additional API(s) from ..videogear import VideoGear +# safe import critical Class modules +zmq = import_dependency_safe("zmq", error="silent", min_version="4.0") +if not (zmq is None): + import zmq.asyncio +msgpack = import_dependency_safe("msgpack", error="silent") +m = import_dependency_safe("msgpack_numpy", error="silent") +uvloop = import_dependency_safe("uvloop", error="silent") + # define logger logger = log.getLogger("NetGear_Async") logger.propagate = False @@ -120,6 +126,10 @@ def __init__( time_delay (int): time delay (in sec) before start reading the frames. options (dict): provides ability to alter Tweak Parameters of NetGear_Async, CamGear, PiGear & Stabilizer. """ + # raise error(s) for critical Class imports + import_dependency_safe("zmq" if zmq is None else "", min_version="4.0") + import_dependency_safe("msgpack" if msgpack is None else "") + import_dependency_safe("msgpack_numpy" if m is None else "") # enable logging if specified self.__logging = logging @@ -284,14 +294,12 @@ def __init__( if sys.version_info[:2] >= (3, 8): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) else: - try: - # import library - import uvloop - - # Latest uvloop eventloop is only available for UNIX machines & python>=3.7. + if not (uvloop is None): + # Latest uvloop eventloop is only available for UNIX machines. asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - except ImportError: - pass + else: + # log if not present + import_dependency_safe("uvloop", error="log") # Retrieve event loop and assign it self.loop = asyncio.get_event_loop() diff --git a/vidgear/gears/asyncio/webgear.py b/vidgear/gears/asyncio/webgear.py index bf7f2b2a6..be6e3f634 100755 --- a/vidgear/gears/asyncio/webgear.py +++ b/vidgear/gears/asyncio/webgear.py @@ -18,32 +18,38 @@ =============================================== """ # import the necessary packages - import os import cv2 import sys import asyncio import inspect -import simplejpeg import numpy as np import logging as log from collections import deque -from starlette.routing import Mount, Route -from starlette.responses import StreamingResponse -from starlette.templating import Jinja2Templates -from starlette.staticfiles import StaticFiles -from starlette.applications import Starlette -from starlette.middleware import Middleware +from os.path import expanduser +# import helper packages from .helper import ( reducer, - logger_handler, generate_webdata, create_blank_frame, - retrieve_best_interpolation, ) +from ..helper import logger_handler, retrieve_best_interpolation, import_dependency_safe + +# import additional API(s) from ..videogear import VideoGear +# safe import critical Class modules +starlette = import_dependency_safe("starlette", error="silent") +if not (starlette is None): + from starlette.routing import Mount, Route + from starlette.responses import StreamingResponse + from starlette.templating import Jinja2Templates + from starlette.staticfiles import StaticFiles + from starlette.applications import Starlette + from starlette.middleware import Middleware +simplejpeg = import_dependency_safe("simplejpeg", error="silent", min_version="1.6.1") + # define logger logger = log.getLogger("WebGear") logger.propagate = False @@ -100,6 +106,11 @@ def __init__( time_delay (int): time delay (in sec) before start reading the frames. options (dict): provides ability to alter Tweak Parameters of WebGear, CamGear, PiGear & Stabilizer. """ + # raise error(s) for critical Class imports + import_dependency_safe("starlette" if starlette is None else "") + import_dependency_safe( + "simplejpeg" if simplejpeg is None else "", min_version="1.6.1" + ) # initialize global params # define frame-compression handler @@ -227,8 +238,6 @@ def __init__( ) else: # otherwise generate suitable path - from os.path import expanduser - data_path = generate_webdata( os.path.join(expanduser("~"), ".vidgear"), c_name="webgear", diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index bdb59a688..3ae39b931 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -18,7 +18,6 @@ =============================================== """ # import the necessary packages - import os import cv2 import sys @@ -27,33 +26,40 @@ import asyncio import logging as log from collections import deque -from starlette.routing import Mount, Route -from starlette.templating import Jinja2Templates -from starlette.staticfiles import StaticFiles -from starlette.applications import Starlette -from starlette.middleware import Middleware -from starlette.responses import JSONResponse, PlainTextResponse - -from aiortc.rtcrtpsender import RTCRtpSender -from aiortc import ( - RTCPeerConnection, - RTCSessionDescription, - VideoStreamTrack, -) -from aiortc.contrib.media import MediaRelay -from aiortc.mediastreams import MediaStreamError -from av import VideoFrame - +from os.path import expanduser +# import helper packages from .helper import ( reducer, - logger_handler, generate_webdata, create_blank_frame, - retrieve_best_interpolation, ) +from ..helper import logger_handler, retrieve_best_interpolation, import_dependency_safe + +# import additional API(s) from ..videogear import VideoGear +# safe import critical Class modules +starlette = import_dependency_safe("starlette", error="silent") +if not (starlette is None): + from starlette.routing import Mount, Route + from starlette.templating import Jinja2Templates + from starlette.staticfiles import StaticFiles + from starlette.applications import Starlette + from starlette.middleware import Middleware + from starlette.responses import JSONResponse, PlainTextResponse +aiortc = import_dependency_safe("aiortc", error="silent") +if not (aiortc is None): + from aiortc.rtcrtpsender import RTCRtpSender + from aiortc import ( + RTCPeerConnection, + RTCSessionDescription, + VideoStreamTrack, + ) + from aiortc.contrib.media import MediaRelay + from aiortc.mediastreams import MediaStreamError + from av import VideoFrame # aiortc dependency + # define logger logger = log.getLogger("WebGear_RTC") if logger.hasHandlers(): @@ -109,6 +115,9 @@ def __init__( super().__init__() # don't forget this! + # raise error(s) for critical Class import + import_dependency_safe("aiortc" if aiortc is None else "") + # initialize global params self.__logging = logging self.__enable_inf = False # continue frames even when video ends. @@ -329,6 +338,9 @@ def __init__( time_delay (int): time delay (in sec) before start reading the frames. options (dict): provides ability to alter Tweak Parameters of WebGear_RTC, CamGear, PiGear & Stabilizer. """ + # raise error(s) for critical Class imports + import_dependency_safe("starlette" if starlette is None else "") + import_dependency_safe("aiortc" if aiortc is None else "") # initialize global params self.__logging = logging @@ -394,8 +406,6 @@ def __init__( ) else: # otherwise generate suitable path - from os.path import expanduser - data_path = generate_webdata( os.path.join(expanduser("~"), ".vidgear"), c_name="webgear_rtc", diff --git a/vidgear/gears/camgear.py b/vidgear/gears/camgear.py index 251425fbe..2736a5729 100644 --- a/vidgear/gears/camgear.py +++ b/vidgear/gears/camgear.py @@ -17,14 +17,15 @@ limitations under the License. =============================================== """ -# import the necessary packages +# import the necessary packages import cv2 import time import queue import logging as log from threading import Thread, Event +# import helper packages from .helper import ( capPropId, logger_handler, @@ -34,6 +35,7 @@ get_supported_resolution, check_gstreamer_support, dimensions_to_resolutions, + import_dependency_safe, ) # define logger @@ -104,8 +106,7 @@ def __init__( video_url = youtube_url_validator(source) if video_url: # import backend library - import pafy - + pafy = import_dependency_safe("pafy") logger.info("Using Youtube-dl Backend") # create new pafy object source_object = pafy.new(video_url, ydl_opts=stream_params) @@ -183,8 +184,9 @@ def __init__( ) else: # import backend library - from streamlink import Streamlink - + Streamlink = import_dependency_safe( + "from streamlink import Streamlink" + ) restore_levelnames() logger.info("Using Streamlink Backend") # check session @@ -442,7 +444,7 @@ def stop(self): if self.__threaded_queue_mode: self.__threaded_queue_mode = False - # indicate that the thread + # indicate that the thread # should be terminated immediately self.__terminate.set() self.__stream_read.set() diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index 1e18dd2f2..f57bb905b 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -21,16 +21,18 @@ # Contains all the support functions/modules required by Vidgear packages # import the necessary packages - import os import re import sys +import cv2 +import types import errno import shutil +import importlib +import requests import numpy as np import logging as log import platform -import requests import socket from tqdm import tqdm from contextlib import closing @@ -40,20 +42,6 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry -try: - # import OpenCV Binaries - import cv2 - - # check whether OpenCV Binaries are 3.x+ - if LooseVersion(cv2.__version__) < LooseVersion("3"): - raise ImportError( - "[Vidgear:ERROR] :: Installed OpenCV API version(< 3.0) is not supported!" - ) -except ImportError: - raise ImportError( - "[Vidgear:ERROR] :: Failed to detect correct OpenCV executables, install it with `pip install opencv-python` command." - ) - def logger_handler(): """ @@ -106,6 +94,134 @@ def logger_handler(): logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) + +def get_module_version(module=None): + """ + ## get_module_version + + Retrieves version of specified module + + Parameters: + name (ModuleType): module of datatype `ModuleType`. + + **Returns:** version of specified module as string + """ + # check if module type is valid + assert not (module is None) and isinstance( + module, types.ModuleType + ), "[Vidgear:ERROR] :: Invalid module!" + + # get version from attribute + version = getattr(module, "__version__", None) + # retry if failed + if version is None: + # some modules uses a capitalized attribute name + version = getattr(module, "__VERSION__", None) + # raise if still failed + if version is None: + raise ImportError( + "[Vidgear:ERROR] :: Can't determine version for module: `{}`!".format( + module.__name__ + ) + ) + return str(version) + + +def import_dependency_safe( + name, + error="raise", + pkg_name=None, + min_version=None, + custom_message=None, +): + """ + ## import_dependency_safe + + Imports specified dependency safely. By default(`error = raise`), if a dependency is missing, + an ImportError with a meaningful message will be raised. Otherwise if `error = log` a warning + will be logged and on `error = silent` everything will be quit. But If a dependency is present, + but older than specified, an error is raised if specified. + + Parameters: + name (string): name of dependency to be imported. + error (string): raise or Log or silence ImportError. Possible values are `"raise"`, `"log"` and `silent`. Default is `"raise"`. + pkg_name (string): (Optional) package name of dependency(if different `pip` name). Otherwise `name` will be used. + min_version(string): (Optional) required minimum version of the dependency to be imported. + custom_message (string): (Optional) custom Import error message to be raised or logged. + + **Returns:** The imported module, when found and the version is correct(if specified). Otherwise `None`. + """ + # check specified parameters + sub_class = "" + if not name or not isinstance(name, str): + return None + else: + # extract name in case of relative import + name = name.strip() + if name.startswith("from"): + name = name.split(" ") + name, sub_class = (name[1].strip(), name[-1].strip()) + + assert error in [ + "raise", + "log", + "silent", + ], "[Vidgear:ERROR] :: Invalid value at `error` parameter." + + # specify package name of dependency(if defined). Otherwise use name + install_name = pkg_name if not (pkg_name is None) else name + + # create message + msg = ( + custom_message + if not (custom_message is None) + else "Failed to find required dependency '{}'. Install it with `pip install {}` command.".format( + name, install_name + ) + ) + # try importing dependency + try: + module = importlib.import_module(name) + if sub_class: + module = getattr(module, sub_class) + except Exception: + # handle errors. + if error == "raise": + raise ImportError(msg) from None + elif error == "log": + logger.error(msg) + return None + else: + return None + + # check if minimum required version + if not (min_version) is None: + # Handle submodules + parent_module = name.split(".")[0] + if parent_module != name: + # grab parent module + module_to_get = sys.modules[parent_module] + else: + module_to_get = module + # extract version + version = get_module_version(module_to_get) + # verify + if LooseVersion(version) < LooseVersion(min_version): + # create message + msg = """Unsupported version '{}' found. Vidgear requires '{}' dependency installed with version '{}' or greater. + Update it with `pip install -U {}` command.""".format( + parent_module, min_version, version, install_name + ) + # handle errors. + if error == "silent": + return None + else: + # raise + raise ImportError(msg) + + return module + + # set default timer for download requests DEFAULT_TIMEOUT = 3 @@ -1017,7 +1133,7 @@ def check_output(*args, **kwargs): stdout=sp.PIPE, stderr=sp.DEVNULL if not (retrieve_stderr) else sp.PIPE, *args, - **kwargs + **kwargs, ) output, stderr = process.communicate() retcode = process.poll() diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index aacad21b0..cba317bac 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -18,31 +18,36 @@ =============================================== """ # import the necessary packages - import os import cv2 -import zmq import time import string import secrets -import simplejpeg - import numpy as np import logging as log - -from zmq import ssh -from zmq import auth -from zmq.auth.thread import ThreadAuthenticator -from zmq.error import ZMQError from threading import Thread from collections import deque +from os.path import expanduser + +# import helper packages from .helper import ( logger_handler, generate_auth_certificates, check_WriteAccess, check_open_port, + import_dependency_safe, ) +# safe import critical Class modules +zmq = import_dependency_safe("zmq", error="silent", min_version="4.0") +if not (zmq is None): + from zmq import ssh + from zmq import auth + from zmq.auth.thread import ThreadAuthenticator + from zmq.error import ZMQError +simplejpeg = import_dependency_safe("simplejpeg", error="silent", min_version="1.6.1") +paramiko = import_dependency_safe("paramiko", error="silent") + # define logger logger = log.getLogger("NetGear") logger.propagate = False @@ -127,6 +132,12 @@ def __init__( logging (bool): enables/disables logging. options (dict): provides the flexibility to alter various NetGear internal properties. """ + # raise error(s) for critical Class imports + import_dependency_safe("zmq" if zmq is None else "", min_version="4.0") + import_dependency_safe( + "simplejpeg" if simplejpeg is None else "", error="log", min_version="1.6.1" + ) + # enable logging if specified self.__logging = True if logging else False @@ -176,7 +187,7 @@ def __init__( self.__ssh_tunnel_mode = None # handles ssh_tunneling mode state self.__ssh_tunnel_pwd = None self.__ssh_tunnel_keyfile = None - self.__paramiko_present = False + self.__paramiko_present = False if paramiko is None else True # define Multi-Server mode self.__multiserver_mode = False # handles multi-server mode state @@ -197,7 +208,9 @@ def __init__( custom_cert_location = "" # handles custom ZMQ certificates path # define frame-compression handler - self.__jpeg_compression = True # enabled by default for all connections + self.__jpeg_compression = ( + True if not (simplejpeg is None) else False + ) # enabled by default for all connections if simplejpeg is installed self.__jpeg_compression_quality = 90 # 90% quality self.__jpeg_compression_fastdct = True # fastest DCT on by default self.__jpeg_compression_fastupsample = False # fastupsample off by default @@ -286,12 +299,6 @@ def __init__( and isinstance(value, int) and (value in valid_security_mech) ): - # check if installed libzmq version is valid - assert zmq.zmq_version_info() >= ( - 4, - 0, - ), "[NetGear:ERROR] :: ZMQ Security feature is not supported in libzmq version < 4.0." - # assign valid mode self.__secure_mode = value elif key == "custom_cert_location" and isinstance(value, str): @@ -328,7 +335,11 @@ def __init__( ) # handle jpeg compression - elif key == "jpeg_compression" and isinstance(value, (bool, str)): + elif ( + key == "jpeg_compression" + and not (simplejpeg is None) + and isinstance(value, (bool, str)) + ): if isinstance(value, str) and value.strip().upper() in [ "RGB", "BGR", @@ -416,8 +427,6 @@ def __init__( ) else: # otherwise auto-generate suitable path - from os.path import expanduser - ( auth_cert_dir, self.__auth_secretkeys_dir, @@ -473,13 +482,6 @@ def __init__( ssh_address, ssh_port ) - # import packages - import importlib - - self.__paramiko_present = ( - True if bool(importlib.util.find_spec("paramiko")) else False - ) - # Handle multiple exclusive modes if enabled if self.__multiclient_mode and self.__multiserver_mode: raise ValueError( diff --git a/vidgear/gears/pigear.py b/vidgear/gears/pigear.py index c322fff8e..975284406 100644 --- a/vidgear/gears/pigear.py +++ b/vidgear/gears/pigear.py @@ -17,16 +17,21 @@ limitations under the License. =============================================== """ - -# import the packages - +# import the necessary packages import cv2 import sys import time import logging as log from threading import Thread -from .helper import capPropId, logger_handler +# import helper packages +from .helper import capPropId, logger_handler, import_dependency_safe + +# safe import critical Class modules +picamera = import_dependency_safe("picamera", error="silent") +if not (picamera is None): + from picamera import PiCamera + from picamera.array import PiRGBArray # define logger logger = log.getLogger("PiGear") @@ -69,22 +74,10 @@ def __init__( time_delay (int): time delay (in sec) before start reading the frames. options (dict): provides ability to alter Source Tweak Parameters. """ - - try: - import picamera - from picamera import PiCamera - from picamera.array import PiRGBArray - except Exception as error: - if isinstance(error, ImportError): - # Output expected ImportErrors. - raise ImportError( - '[PiGear:ERROR] :: Failed to detect Picamera executables, install it with "pip3 install picamera" command.' - ) - else: - # Handle any API errors - raise RuntimeError( - "[PiGear:ERROR] :: Picamera API failure: {}".format(error) - ) + # raise error(s) for critical Class imports + import_dependency_safe( + "picamera" if picamera is None else "", + ) # enable logging if specified self.__logging = False diff --git a/vidgear/gears/screengear.py b/vidgear/gears/screengear.py index 004186be3..094dd1d0b 100644 --- a/vidgear/gears/screengear.py +++ b/vidgear/gears/screengear.py @@ -18,20 +18,24 @@ =============================================== """ # import the necessary packages - import cv2 import time import queue import numpy as np import logging as log -from mss import mss -import pyscreenshot as pysct from threading import Thread, Event from collections import deque, OrderedDict -from mss.exception import ScreenShotError -from pyscreenshot.err import FailedBackendError -from .helper import capPropId, logger_handler +# import helper packages +from .helper import import_dependency_safe, capPropId, logger_handler + +# safe import critical Class modules +mss = import_dependency_safe("from mss import mss", error="silent") +if not (mss is None): + from mss.exception import ScreenShotError +pysct = import_dependency_safe("pyscreenshot", error="silent") +if not (pysct is None): + from pyscreenshot.err import FailedBackendError # define logger logger = log.getLogger("ScreenGear") @@ -61,6 +65,10 @@ def __init__( logging (bool): enables/disables logging. options (dict): provides the flexibility to manually set the dimensions of capture screen area. """ + # raise error(s) for critical Class imports + import_dependency_safe("mss.mss" if mss is None else "") + import_dependency_safe("pyscreenshot" if pysct is None else "") + # enable logging if specified: self.__logging = logging if isinstance(logging, bool) else False diff --git a/vidgear/gears/stabilizer.py b/vidgear/gears/stabilizer.py index 91746de1b..2bf34e6eb 100644 --- a/vidgear/gears/stabilizer.py +++ b/vidgear/gears/stabilizer.py @@ -21,12 +21,12 @@ =============================================== """ # import the necessary packages - import cv2 import numpy as np import logging as log from collections import deque +# import helper packages from .helper import logger_handler, check_CV_version, retrieve_best_interpolation # define logger @@ -140,7 +140,7 @@ def __init__( # retrieve best interpolation self.__interpolation = retrieve_best_interpolation( - ["INTER_LINEAR_EXACT", "INTER_LINEAR", "INTER_CUBIC"] + ["INTER_LINEAR_EXACT", "INTER_LINEAR", "INTER_AREA"] ) # define normalized box filter diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index dfd550901..e1ef5cb33 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -18,7 +18,6 @@ =============================================== """ # import the necessary packages - import os import cv2 import sys @@ -31,6 +30,7 @@ from fractions import Fraction from collections import OrderedDict +# import helper packages from .helper import ( capPropId, dict2Args, @@ -47,6 +47,7 @@ # define logger logger = log.getLogger("StreamGear") +logger.propagate = False logger.addHandler(logger_handler()) logger.setLevel(log.DEBUG) @@ -79,7 +80,6 @@ def __init__( logging (bool): enables/disables logging. stream_params (dict): provides the flexibility to control supported internal parameters and FFmpeg properities. """ - # checks if machine in-use is running windows os or not self.__os_windows = True if os.name == "nt" else False # enable logging if specified diff --git a/vidgear/gears/videogear.py b/vidgear/gears/videogear.py index 681a9abdd..bef898dc5 100644 --- a/vidgear/gears/videogear.py +++ b/vidgear/gears/videogear.py @@ -21,7 +21,10 @@ # import the necessary packages import logging as log +# import helper packages from .helper import logger_handler + +# import additional API(s) from .camgear import CamGear # define logger diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 0e26af04c..a6d8e1ba9 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -18,7 +18,6 @@ =============================================== """ # import the necessary packages - import os import cv2 import sys @@ -26,6 +25,7 @@ import logging as log import subprocess as sp +# import helper packages from .helper import ( capPropId, dict2Args, @@ -46,13 +46,13 @@ class WriteGear: """ - WriteGear handles various powerful Video-Writer Tools that provide us the freedom to do almost anything imaginable with multimedia data. + WriteGear handles various powerful Video-Writer Tools that provide us the freedom to do almost anything imaginable with multimedia data. - WriteGear API provides a complete, flexible, and robust wrapper around FFmpeg, a leading multimedia framework. WriteGear can process real-time frames into a lossless - compressed video-file with any suitable specification (such asbitrate, codec, framerate, resolution, subtitles, etc.). It is powerful enough to perform complex tasks such as + WriteGear API provides a complete, flexible, and robust wrapper around FFmpeg, a leading multimedia framework. WriteGear can process real-time frames into a lossless + compressed video-file with any suitable specification (such asbitrate, codec, framerate, resolution, subtitles, etc.). It is powerful enough to perform complex tasks such as Live-Streaming (such as for Twitch) and Multiplexing Video-Audio with real-time frames in way fewer lines of code. - Best of all, WriteGear grants users the complete freedom to play with any FFmpeg parameter with its exclusive Custom Commands function without relying on any + Best of all, WriteGear grants users the complete freedom to play with any FFmpeg parameter with its exclusive Custom Commands function without relying on any third-party API. In addition to this, WriteGear also provides flexible access to OpenCV's VideoWriter API tools for video-frames encoding without compression. diff --git a/vidgear/tests/network_tests/asyncio_tests/test_helper.py b/vidgear/tests/network_tests/asyncio_tests/test_helper.py index 5741146c0..3ff1630e7 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_helper.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_helper.py @@ -29,9 +29,9 @@ from vidgear.gears.asyncio.helper import ( reducer, create_blank_frame, - logger_handler, retrieve_best_interpolation, ) +from vidgear.gears.helper import logger_handler # define test logger logger = log.getLogger("Test_Asyncio_Helper") 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 0a8b2c311..3d7a8c6a3 100644 --- a/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py +++ b/vidgear/tests/network_tests/asyncio_tests/test_netgear_async.py @@ -32,7 +32,7 @@ import tempfile from vidgear.gears.asyncio import NetGear_Async -from vidgear.gears.asyncio.helper import logger_handler +from vidgear.gears.helper import logger_handler # define test logger logger = log.getLogger("Test_NetGear_Async") diff --git a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py index de46afbc4..e05d5d426 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear.py @@ -33,7 +33,7 @@ from starlette.testclient import TestClient from vidgear.gears.asyncio import WebGear -from vidgear.gears.asyncio.helper import logger_handler +from vidgear.gears.helper import logger_handler # define test logger logger = log.getLogger("Test_webgear") 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 a1a188810..cec35673f 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -44,7 +44,7 @@ ) from av import VideoFrame from vidgear.gears.asyncio import WebGear_RTC -from vidgear.gears.asyncio.helper import logger_handler +from vidgear.gears.helper import logger_handler # define test logger From 3d45f5d61c5ea7c27e172bd6510f9c70f1a75b3d Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Tue, 31 Aug 2021 19:14:08 +0530 Subject: [PATCH 106/112] =?UTF-8?q?=F0=9F=93=9D=20Docs:=20Added=20docs=20a?= =?UTF-8?q?nd=20other=20related=20tweaks.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 📝 Added docs for installing vidgear with only selective dependencies - 💄 Added new `advance`/`experiment` admonition with new background color. - 🍱 Added new icons svg for `advance` and `warning` admonition. - ⚰️ Removed redundant data table tweaks from `custom.css`. - 🎨 Beautify `custom.css`. - ✏️ Fixed typos in URL links. - Setup.py: - ➖ Removed all redundant dependencies like `colorama`, `aiofiles`, `aiohttp`. - ➕ Added new `cython` and `msgpack` dependency. - 🎨 Added `msgpack` and `msgpack_numpy` to auto-install latest. - Helper: - ⚰️ Removed unused `aiohttp` dependency. - 🔊 Removed `asctime` from logging. --- docs/installation/pip_install.md | 72 +++++++++++++++--- docs/installation/source_install.md | 78 ++++++++++++++++++-- docs/overrides/assets/stylesheets/custom.css | 32 ++++---- setup.py | 7 +- vidgear/gears/asyncio/helper.py | 1 - vidgear/gears/asyncio/netgear_async.py | 2 +- vidgear/gears/helper.py | 2 +- vidgear/gears/netgear.py | 2 +- 8 files changed, 159 insertions(+), 37 deletions(-) diff --git a/docs/installation/pip_install.md b/docs/installation/pip_install.md index 1b9b66663..69106f7f5 100644 --- a/docs/installation/pip_install.md +++ b/docs/installation/pip_install.md @@ -29,9 +29,9 @@ limitations under the License. When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), you need to check manually if following dependencies are installed: -??? alert "Latest `pip` Recommended" +!!! alert "Upgrade your `pip`" - It advised to install latest `pip` version before installing vidgear to avoid any undesired errors. Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. + It strongly advised to upgrade to latest `pip` before installing vidgear to avoid any undesired installation error(s). Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. === "Linux" @@ -81,7 +81,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y * #### FFmpeg - Require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). + Require only for the video compression and encoding compatibility within [**StreamGear API**](../../gears/streamgear/overview/) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). !!! tip "FFmpeg Installation" @@ -104,7 +104,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y ??? error "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: + Installing `aiortc` on windows may sometimes require Microsoft Build Tools for Visual C++ libraries installed. You can easily fix this error by installing any **ONE** of these choices: !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." @@ -153,7 +153,7 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y python -m pip install vidgear[asyncio] ``` - If you don't have the privileges to the directory you're installing package. Then use `--user` flag, that makes pip install packages in your home directory instead: + And, If you don't have the privileges to the directory you're installing package. Then use `--user` flag, that makes pip install packages in your home directory instead: ``` sh python -m pip install --user vidgear @@ -162,12 +162,66 @@ When installing VidGear with [pip](https://pip.pypa.io/en/stable/installing/), y python -m pip install --user vidgear[asyncio] ``` + Or, If you're using `py` as alias for installed python, then: + + ``` sh + py -m pip install --user vidgear + + # or with asyncio support + py -m pip install --user vidgear[asyncio] + ``` + +??? experiment "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). + + This is useful when you want to manually review, select and install minimal API-specific dependencies on bare-minimum vidgear from scratch on your system: + + - To install bare-minimum vidgear without any dependencies, use [`--no-deps`](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-no-deps) pip flag as follows: + + ```sh + # Install stable release without any dependencies + pip install --no-deps --upgrade vidgear + ``` + + - Then, you must install all **Core dependencies**: + + ```sh + # Install core dependencies + pip install cython, numpy, requests, tqdm, colorlog + + # Install opencv(only if not installed previously) + pip install opencv-python + ``` + + - Finally, manually install your **API-specific dependencies** as required by your API(in use): + + + | APIs | Dependencies | + |:---:|:---| + | CamGear | `pafy`, `youtube-dl`, `streamlink` | + | PiGear | `picamera` | + | VideoGear | - | + | ScreenGear | `mss`, `pyscreenshot`, `Pillow` | + | WriteGear | **FFmpeg:** See [this doc ➶](https://abhitronix.github.io/vidgear/v0.2.2-dev/gears/writegear/compression/advanced/ffmpeg_install/#ffmpeg-installation-instructions) | + | StreamGear | **FFmpeg:** See [this doc ➶](https://abhitronix.github.io/vidgear/v0.2.2-dev/gears/streamgear/ffmpeg_install/#ffmpeg-installation-instructions) | + | NetGear | `pyzmq`, `simplejpeg` | + | WebGear | `starlette`, `jinja2`, `uvicorn`, `simplejpeg` | + | WebGear_RTC | `aiortc`, `starlette`, `jinja2`, `uvicorn` | + | NetGear_Async | `pyzmq`, `msgpack`, `msgpack_numpy`, `uvloop` | + + ```sh + # Just copy-&-paste from above table + pip install + ``` + + ```sh -# Install stable release -pip install vidgear +# Install latest stable release +pip install -U vidgear -# Or Install stable release with Asyncio support -pip install vidgear[asyncio] +# Or Install latest stable release with Asyncio support +pip install -U vidgear[asyncio] ``` **And if you prefer to install VidGear directly from the repository:** diff --git a/docs/installation/source_install.md b/docs/installation/source_install.md index d8b299be4..243f497a6 100644 --- a/docs/installation/source_install.md +++ b/docs/installation/source_install.md @@ -33,9 +33,9 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec Any other python dependencies _(Core/API specific)_ will be automatically installed based on your OS specifications. -??? alert "Latest `pip` Recommended" +!!! alert "Upgrade your `pip`" - It advised to install latest `pip` version before installing vidgear to avoid any undesired errors. Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. + It strongly advised to upgrade to latest `pip` before installing vidgear to avoid any undesired installation error(s). Python comes with an [`ensurepip`](https://docs.python.org/3/library/ensurepip.html#module-ensurepip) module[^1], which can easily install `pip` in any Python environment. === "Linux" @@ -63,7 +63,7 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec * #### FFmpeg - Require for the video compression and encoding compatibilities within [**StreamGear**](#streamgear) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). + Require only for the video compression and encoding compatibility within [**StreamGear API**](../../gears/streamgear/overview/) API and [**WriteGear API's Compression Mode**](../../gears/writegear/compression/overview/). !!! tip "FFmpeg Installation" @@ -76,7 +76,7 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec ??? error "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: + Installing `aiortc` on windows may sometimes requires Microsoft Build Tools for Visual C++ libraries installed. You can easily fix this error by installing any **ONE** of these choices: !!! info "While the error is calling for VC++ 14.0 - but newer versions of Visual C++ libraries works as well." @@ -111,7 +111,7 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec * Use following commands to clone and install VidGear: - ```sh + ```sh # clone the repository and get inside git clone https://github.com/abhiTronix/vidgear.git && cd vidgear @@ -123,7 +123,73 @@ When installing VidGear from source, FFmpeg and Aiortc are the only two API spec # OR install with asyncio support python - m pip install .[asyncio] - ``` + ``` + + * If you're using `py` as alias for installed python, then: + + ``` sh + # clone the repository and get inside + git clone https://github.com/abhiTronix/vidgear.git && cd vidgear + + # checkout the latest testing branch + git checkout testing + + # install normally + python -m pip install . + + # OR install with asyncio support + python - m pip install .[asyncio] + ``` + +??? experiment "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). + + This is useful when you want to manually review, select and install minimal API-specific dependencies on bare-minimum vidgear from scratch on your system: + + - To install bare-minimum vidgear without any dependencies, use [`--no-deps`](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-no-deps) pip flag as follows: + + ```sh + # clone the repository and get inside + git clone https://github.com/abhiTronix/vidgear.git && cd vidgear + + # checkout the latest testing branch + git checkout testing + + # Install without any dependencies + pip install --no-deps . + ``` + + - Then, you must install all **Core dependencies**: + + ```sh + # Install core dependencies + pip install cython, numpy, requests, tqdm, colorlog + + # Install opencv(only if not installed previously) + pip install opencv-python + ``` + + - Finally, manually install your **API-specific dependencies** as required by your API(in use): + + + | APIs | Dependencies | + |:---:|:---| + | CamGear | `pafy`, `youtube-dl`, `streamlink` | + | PiGear | `picamera` | + | VideoGear | - | + | ScreenGear | `mss`, `pyscreenshot`, `Pillow` | + | WriteGear | **FFmpeg:** See [this doc ➶](https://abhitronix.github.io/vidgear/v0.2.2-dev/gears/writegear/compression/advanced/ffmpeg_install/#ffmpeg-installation-instructions) | + | StreamGear | **FFmpeg:** See [this doc ➶](https://abhitronix.github.io/vidgear/v0.2.2-dev/gears/streamgear/ffmpeg_install/#ffmpeg-installation-instructions) | + | NetGear | `pyzmq`, `simplejpeg` | + | WebGear | `starlette`, `jinja2`, `uvicorn`, `simplejpeg` | + | WebGear_RTC | `aiortc`, `starlette`, `jinja2`, `uvicorn` | + | NetGear_Async | `pyzmq`, `msgpack`, `msgpack_numpy`, `uvloop` | + + ```sh + # Just copy-&-paste from above table + pip install + ``` ```sh # clone the repository and get inside diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index af2beccd0..411c08edd 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -22,7 +22,7 @@ limitations under the License. --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,%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='M6,6.9L3.87,4.78L5.28,3.37L7.4,5.5L6,6.9M13,1V4H11V1H13M20.13,4.78L18,6.9L16.6,5.5L18.72,3.37L20.13,4.78M4.5,10.5V12.5H1.5V10.5H4.5M19.5,10.5H22.5V12.5H19.5V10.5M6,20H18A2,2 0 0,1 20,22H4A2,2 0 0,1 6,20M12,5A6,6 0 0,1 18,11V19H6V11A6,6 0 0,1 12,5Z' /%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 fill='%23000000' d='M13 13H11V7H13M11 15H13V17H11M15.73 3H8.27L3 8.27V15.73L8.27 21H15.73L21 15.73V8.27L15.73 3Z' /%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"); @@ -33,6 +33,11 @@ limitations under the License. --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' 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='M7,2V4H8V18A4,4 0 0,0 12,22A4,4 0 0,0 16,18V4H17V2H7M11,16C10.4,16 10,15.6 10,15C10,14.4 10.4,14 11,14C11.6,14 12,14.4 12,15C12,15.6 11.6,16 11,16M13,12C12.4,12 12,11.6 12,11C12,10.4 12.4,10 13,10C13.6,10 14,10.4 14,11C14,11.6 13.6,12 13,12M14,7H10V4H14V7Z' /%3E%3C/svg%3E"); +} +.md-typeset .admonition.advance, +.md-typeset details.advance { + border-color: rgb(27,77,62); } .md-typeset .admonition.new, .md-typeset details.new { @@ -45,7 +50,7 @@ limitations under the License. } .md-typeset .new > .admonition-title::before, .md-typeset .new > summary::before { - background-color: rgb(43, 155, 70); + background-color: rgb(228,24,30); -webkit-mask-image: var(--md-admonition-icon--new); mask-image: var(--md-admonition-icon--new); } @@ -55,12 +60,12 @@ limitations under the License. } .md-typeset .alert > .admonition-title, .md-typeset .alert > summary { - background-color: rgba(255, 0, 255), 0.1); - border-color: rgb(255, 0, 255)); + 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 { - background-color: rgb(255, 0, 255)); + background-color: rgb(255, 0, 255); -webkit-mask-image: var(--md-admonition-icon--alert); mask-image: var(--md-admonition-icon--alert); } @@ -154,16 +159,15 @@ limitations under the License. -webkit-mask-image: var(--md-admonition-icon--xquote); mask-image: var(--md-admonition-icon--xquote); } - - -td { - vertical-align: middle !important; - text-align: center !important; -} -th { - font-weight: bold !important; - text-align: center !important; +.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); } + .md-nav__item--active > .md-nav__link { font-weight: bold; } diff --git a/setup.py b/setup.py index af187226f..f0202c5fe 100644 --- a/setup.py +++ b/setup.py @@ -91,6 +91,7 @@ def latest_version(package_name): install_requires=[ "pafy{}".format(latest_version("pafy")), "mss{}".format(latest_version("mss")), + "cython", "numpy", "youtube-dl{}".format(latest_version("youtube-dl")), "streamlink", @@ -98,7 +99,6 @@ def latest_version(package_name): "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format(latest_version("simplejpeg")), "colorlog", - "colorama", "tqdm", "Pillow", "pyscreenshot{}".format(latest_version("pyscreenshot")), @@ -112,11 +112,10 @@ def latest_version(package_name): extras_require={ "asyncio": [ "starlette{}".format(latest_version("starlette")), - "aiofiles", "jinja2", - "aiohttp", "uvicorn{}".format(latest_version("uvicorn")), - "msgpack_numpy", + "msgpack{}".format(latest_version("msgpack")), + "msgpack_numpy{}".format(latest_version("msgpack_numpy")), "aiortc{}".format(latest_version("aiortc")), ] + ( diff --git a/vidgear/gears/asyncio/helper.py b/vidgear/gears/asyncio/helper.py index ab6abcad6..ebe3ce722 100755 --- a/vidgear/gears/asyncio/helper.py +++ b/vidgear/gears/asyncio/helper.py @@ -26,7 +26,6 @@ import sys import errno import numpy as np -import aiohttp import asyncio import logging as log import platform diff --git a/vidgear/gears/asyncio/netgear_async.py b/vidgear/gears/asyncio/netgear_async.py index ddb10b993..d4564e03a 100755 --- a/vidgear/gears/asyncio/netgear_async.py +++ b/vidgear/gears/asyncio/netgear_async.py @@ -36,7 +36,7 @@ from ..videogear import VideoGear # safe import critical Class modules -zmq = import_dependency_safe("zmq", error="silent", min_version="4.0") +zmq = import_dependency_safe("zmq", pkg_name="pyzmq", error="silent", min_version="4.0") if not (zmq is None): import zmq.asyncio msgpack = import_dependency_safe("msgpack", error="silent") diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index f57bb905b..afd6c1c62 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -53,7 +53,7 @@ def logger_handler(): """ # logging formatter formatter = ColoredFormatter( - "%(bold_cyan)s%(asctime)s :: %(bold_blue)s%(name)s%(reset)s :: %(log_color)s%(levelname)s%(reset)s :: %(message)s", + "%(bold_blue)s%(name)s%(reset)s :: %(log_color)s%(levelname)s%(reset)s :: %(message)s", datefmt="%H:%M:%S", reset=True, log_colors={ diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index cba317bac..53be57f89 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -39,7 +39,7 @@ ) # safe import critical Class modules -zmq = import_dependency_safe("zmq", error="silent", min_version="4.0") +zmq = import_dependency_safe("zmq", pkg_name="pyzmq", error="silent", min_version="4.0") if not (zmq is None): from zmq import ssh from zmq import auth From 114969e999d4577dec8e8bff7f0500878de45637 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 1 Sep 2021 11:27:24 +0530 Subject: [PATCH 107/112] =?UTF-8?q?=F0=9F=9A=B8=20Docs:=20Added=20bonus=20?= =?UTF-8?q?examples=20to=20help=20section.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🚸 Implemented a curated list of more advanced examples with unusual configuration for each API. - 💄 Updated admonitions colors and beautified `custom.css`. - ⚡️ Replaced VideoGear & CamGear with OpenCV in CPU intensive examples. - 📝 Updated `mkdocs.yml` with new changes and URLs. - 🚚 Moved FAQ examples to bonus. - 📝 Added several new contents and updated context. - ✏️ Fixed typos and updated links. --- docs/gears/camgear/usage.md | 6 + .../netgear/advanced/bidirectional_mode.md | 22 +- docs/gears/netgear/advanced/compression.md | 22 +- docs/gears/netgear/usage.md | 8 +- .../advanced/bidirectional_mode.md | 144 ------- docs/gears/netgear_async/usage.md | 24 +- docs/gears/pigear/usage.md | 8 +- docs/gears/screengear/usage.md | 8 +- docs/gears/stabilizer/usage.md | 16 +- docs/gears/streamgear/introduction.md | 8 +- docs/gears/streamgear/rtfm/usage.md | 2 +- docs/gears/streamgear/ssm/usage.md | 2 +- docs/gears/videogear/usage.md | 6 + docs/gears/webgear/advanced.md | 73 +--- docs/gears/webgear_rtc/advanced.md | 67 +--- docs/gears/writegear/compression/usage.md | 4 +- docs/gears/writegear/introduction.md | 8 +- docs/help/camgear_ex.md | 243 ++++++++++++ docs/help/camgear_faqs.md | 93 +---- docs/help/get_help.md | 22 +- docs/help/netgear_async_ex.md | 169 ++++++++ docs/help/netgear_ex.md | 368 ++++++++++++++++++ docs/help/pigear_ex.md | 75 ++++ docs/help/pigear_faqs.md | 49 +-- docs/help/screengear_ex.md | 149 +++++++ docs/help/stabilizer_ex.md | 236 +++++++++++ docs/help/streamgear_ex.md | 161 ++++++++ docs/help/videogear_ex.md | 220 +++++++++++ docs/help/webgear_ex.md | 233 +++++++++++ docs/help/webgear_faqs.md | 2 +- docs/help/webgear_rtc_ex.md | 213 ++++++++++ docs/help/writegear_ex.md | 306 +++++++++++++++ docs/help/writegear_faqs.md | 192 +-------- docs/overrides/assets/stylesheets/custom.css | 220 +++++++---- mkdocs.yml | 12 + 35 files changed, 2661 insertions(+), 730 deletions(-) create mode 100644 docs/help/camgear_ex.md create mode 100644 docs/help/netgear_async_ex.md create mode 100644 docs/help/netgear_ex.md create mode 100644 docs/help/pigear_ex.md create mode 100644 docs/help/screengear_ex.md create mode 100644 docs/help/stabilizer_ex.md create mode 100644 docs/help/streamgear_ex.md create mode 100644 docs/help/videogear_ex.md create mode 100644 docs/help/webgear_ex.md create mode 100644 docs/help/webgear_rtc_ex.md create mode 100644 docs/help/writegear_ex.md diff --git a/docs/gears/camgear/usage.md b/docs/gears/camgear/usage.md index 0583fe944..67f8e9b05 100644 --- a/docs/gears/camgear/usage.md +++ b/docs/gears/camgear/usage.md @@ -301,4 +301,10 @@ cv2.destroyAllWindows() stream.stop() ``` +  + +## Bonus Examples + +!!! example "Checkout more advanced CamGear examples with unusual configuration [here ➶](../../../help/camgear_ex/)" +   \ No newline at end of file diff --git a/docs/gears/netgear/advanced/bidirectional_mode.md b/docs/gears/netgear/advanced/bidirectional_mode.md index 3f8ac47ac..cfea17b39 100644 --- a/docs/gears/netgear/advanced/bidirectional_mode.md +++ b/docs/gears/netgear/advanced/bidirectional_mode.md @@ -378,14 +378,13 @@ Open your favorite terminal and execute the following python code: ```python # import required libraries -from vidgear.gears import VideoGear from vidgear.gears import NetGear from vidgear.gears.helper import reducer import numpy as np import cv2 # open any valid video stream(for e.g `test.mp4` file) -stream = VideoGear(source="test.mp4").start() +stream = cv2.VideoCapture("test.mp4") # activate Bidirectional mode options = {"bidirectional_mode": True} @@ -398,10 +397,10 @@ while True: try: # read frames from stream - frame = stream.read() + (grabbed, frame) = stream.read() - # check for frame if Nonetype - if frame is None: + # check for frame if not grabbed + if not grabbed: break # reducer frames size if you want more performance, otherwise comment this line @@ -428,7 +427,7 @@ while True: break # safely close video stream -stream.stop() +stream.release() # safely close server server.close() @@ -445,7 +444,6 @@ Then open another terminal on the same system and execute the following python c ```python # import required libraries from vidgear.gears import NetGear -from vidgear.gears import VideoGear from vidgear.gears.helper import reducer import cv2 @@ -453,7 +451,7 @@ import cv2 options = {"bidirectional_mode": True} # again open the same video stream -stream = VideoGear(source="test.mp4").start() +stream = cv2.VideoCapture("test.mp4") # define NetGear Client with `receive_mode = True` and defined parameter client = NetGear(receive_mode=True, pattern=1, logging=True, **options) @@ -462,10 +460,10 @@ client = NetGear(receive_mode=True, pattern=1, logging=True, **options) while True: # read frames from stream - frame = stream.read() + (grabbed, frame) = stream.read() - # check for frame if Nonetype - if frame is None: + # check for frame if not grabbed + if not grabbed: break # reducer frames size if you want more performance, otherwise comment this line @@ -503,7 +501,7 @@ while True: cv2.destroyAllWindows() # safely close video stream -stream.stop() +stream.release() # safely close client client.close() diff --git a/docs/gears/netgear/advanced/compression.md b/docs/gears/netgear/advanced/compression.md index 21e6c00c8..6187ddef9 100644 --- a/docs/gears/netgear/advanced/compression.md +++ b/docs/gears/netgear/advanced/compression.md @@ -475,14 +475,13 @@ Open your favorite terminal and execute the following python code: ```python # import required libraries -from vidgear.gears import VideoGear from vidgear.gears import NetGear from vidgear.gears.helper import reducer import numpy as np import cv2 # open any valid video stream(for e.g `test.mp4` file) -stream = VideoGear(source="test.mp4").start() +stream = cv2.VideoCapture("test.mp4") # activate Bidirectional mode and Frame Compression options = { @@ -501,10 +500,10 @@ while True: try: # read frames from stream - frame = stream.read() + (grabbed, frame) = stream.read() - # check for frame if Nonetype - if frame is None: + # check for frame if not grabbed + if not grabbed: break # reducer frames size if you want even more performance, otherwise comment this line @@ -531,7 +530,7 @@ while True: break # safely close video stream -stream.stop() +stream.release() # safely close server server.close() @@ -548,7 +547,6 @@ Then open another terminal on the same system and execute the following python c ```python # import required libraries from vidgear.gears import NetGear -from vidgear.gears import VideoGear from vidgear.gears.helper import reducer import cv2 @@ -562,7 +560,7 @@ options = { } # again open the same video stream -stream = VideoGear(source="test.mp4").start() +stream = cv2.VideoCapture("test.mp4") # define NetGear Client with `receive_mode = True` and defined parameter client = NetGear(receive_mode=True, pattern=1, logging=True, **options) @@ -571,10 +569,10 @@ client = NetGear(receive_mode=True, pattern=1, logging=True, **options) while True: # read frames from stream - frame = stream.read() + (grabbed, frame) = stream.read() - # check for frame if Nonetype - if frame is None: + # check for frame if not grabbed + if not grabbed: break # reducer frames size if you want even more performance, otherwise comment this line @@ -612,7 +610,7 @@ while True: cv2.destroyAllWindows() # safely close video stream -stream.stop() +stream.release() # safely close client client.close() diff --git a/docs/gears/netgear/usage.md b/docs/gears/netgear/usage.md index 95d310bd0..e2aaa2d14 100644 --- a/docs/gears/netgear/usage.md +++ b/docs/gears/netgear/usage.md @@ -471,4 +471,10 @@ stream.stop() server.close() ``` -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced NetGear examples with unusual configuration [here ➶](../../../help/netgear_ex/)" + +  \ No newline at end of file diff --git a/docs/gears/netgear_async/advanced/bidirectional_mode.md b/docs/gears/netgear_async/advanced/bidirectional_mode.md index 923372156..0341f372b 100644 --- a/docs/gears/netgear_async/advanced/bidirectional_mode.md +++ b/docs/gears/netgear_async/advanced/bidirectional_mode.md @@ -219,150 +219,6 @@ if __name__ == "__main__":   -### Bare-Minimum Usage with VideoGear - -Following is another comparatively faster Bidirectional Mode bare-minimum example over Custom Source Server built using multi-threaded [VideoGear](../../../videogear/overview/) _(instead of OpenCV)_ and NetGear_Async API: - -#### Server End - -Open your favorite terminal and execute the following python code: - -!!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" - -```python -# import library -from vidgear.gears.asyncio import NetGear_Async -from vidgear.gears import VideoGear -import cv2, asyncio - -# activate Bidirectional mode -options = {"bidirectional_mode": True} - -# initialize Server without any source -server = NetGear_Async(source=None, logging=True, **options) - -# Create a async frame generator as custom source -async def my_frame_generator(): - - # !!! define your own video source here !!! - # Open any valid video stream(for e.g `foo.mp4` file) - stream = VideoGear(source="foo.mp4").start() - - # loop over stream until its terminated - while True: - # read frames - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame to be sent here} - - # prepare data to be sent(a simple text in our case) - target_data = "Hello, I am a Server." - - # receive data from Client - recv_data = await server.transceive_data() - - # print data just received from Client - if not (recv_data is None): - print(recv_data) - - # send our frame & data - yield (target_data, frame) - - # sleep for sometime - await asyncio.sleep(0) - - # safely close video stream - stream.stop() - - -if __name__ == "__main__": - # set event loop - asyncio.set_event_loop(server.loop) - # Add your custom source generator to Server configuration - server.config["generator"] = my_frame_generator() - # Launch the Server - server.launch() - try: - # run your main function task until it is complete - server.loop.run_until_complete(server.task) - except (KeyboardInterrupt, SystemExit): - # wait for interrupts - pass - finally: - # finally close the server - server.close() -``` - -#### Client 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 -# import libraries -from vidgear.gears.asyncio import NetGear_Async -import cv2, asyncio - -# activate Bidirectional mode -options = {"bidirectional_mode": True} - -# define and launch Client with `receive_mode=True` -client = NetGear_Async(receive_mode=True, logging=True, **options).launch() - -# Create a async function where you want to show/manipulate your received frames -async def main(): - # loop over Client's Asynchronous Frame Generator - async for (data, frame) in client.recv_generator(): - - # do something with receive data from server - if not (data is None): - # let's print it - print(data) - - # {do something with received frames here} - - # Show output window(comment these lines if not required) - cv2.imshow("Output Frame", frame) - cv2.waitKey(1) & 0xFF - - # prepare data to be sent - target_data = "Hi, I am a Client here." - - # send our data to server - await client.transceive_data(data=target_data) - - # await before continuing - await asyncio.sleep(0) - - -if __name__ == "__main__": - # Set event loop to client's - asyncio.set_event_loop(client.loop) - try: - # run your main function task until it is complete - client.loop.run_until_complete(main()) - except (KeyboardInterrupt, SystemExit): - # wait for interrupts - pass - - # close all output window - cv2.destroyAllWindows() - - # safely close client - client.close() -``` - -  - -  - - - ### Using Bidirectional Mode with Variable Parameters diff --git a/docs/gears/netgear_async/usage.md b/docs/gears/netgear_async/usage.md index 0220d1252..63323b049 100644 --- a/docs/gears/netgear_async/usage.md +++ b/docs/gears/netgear_async/usage.md @@ -241,14 +241,14 @@ import cv2, asyncio # initialize Server without any source server = NetGear_Async(source=None, logging=True) +# !!! define your own video source here !!! +# Open any video stream such as live webcam +# video stream on first index(i.e. 0) device +stream = cv2.VideoCapture(0) + # Create a async frame generator as custom source async def my_frame_generator(): - # !!! define your own video source here !!! - # Open any video stream such as live webcam - # video stream on first index(i.e. 0) device - stream = cv2.VideoCapture(0) - # loop over stream until its terminated while True: @@ -265,9 +265,6 @@ async def my_frame_generator(): yield frame # sleep for sometime await asyncio.sleep(0) - - # close stream - stream.release() if __name__ == "__main__": @@ -284,6 +281,8 @@ if __name__ == "__main__": # wait for interrupts pass finally: + # close stream + stream.release() # finally close the server server.close() ``` @@ -375,6 +374,7 @@ if __name__ == "__main__": ``` ### Client's End + Then open another terminal on the same system and execute the following python code and see the output: !!! warning "Client will throw TimeoutError if it fails to connect to the Server in given [`timeout`](../params/#timeout) value!" @@ -429,4 +429,10 @@ if __name__ == "__main__": writer.close() ``` -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced NetGear_Async examples with unusual configuration [here ➶](../../../help/netgear_async_ex/)" + +  \ No newline at end of file diff --git a/docs/gears/pigear/usage.md b/docs/gears/pigear/usage.md index 7b9827685..78ec04348 100644 --- a/docs/gears/pigear/usage.md +++ b/docs/gears/pigear/usage.md @@ -270,4 +270,10 @@ stream.stop() writer.close() ``` -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced PiGear examples with unusual configuration [here ➶](../../../help/pigear_ex/)" + +  \ No newline at end of file diff --git a/docs/gears/screengear/usage.md b/docs/gears/screengear/usage.md index dea324021..9dd7c6ce3 100644 --- a/docs/gears/screengear/usage.md +++ b/docs/gears/screengear/usage.md @@ -321,4 +321,10 @@ stream.stop() writer.close() ``` -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced NetGear examples with unusual configuration [here ➶](../../../help/screengear_ex/)" + +  \ No newline at end of file diff --git a/docs/gears/stabilizer/usage.md b/docs/gears/stabilizer/usage.md index fd423ba41..acd7ca2ae 100644 --- a/docs/gears/stabilizer/usage.md +++ b/docs/gears/stabilizer/usage.md @@ -67,7 +67,7 @@ while True: if stabilized_frame is None: continue - # {do something with the stabilized_frame frame here} + # {do something with the stabilized frame here} # Show output window cv2.imshow("Output Stabilized Frame", stabilized_frame) @@ -121,7 +121,7 @@ while True: if stabilized_frame is None: continue - # {do something with the frame here} + # {do something with the stabilized frame here} # Show output window cv2.imshow("Stabilized Frame", stabilized_frame) @@ -176,7 +176,7 @@ while True: if stabilized_frame is None: continue - # {do something with the stabilized_frame frame here} + # {do something with the stabilized frame here} # Show output window cv2.imshow("Output Stabilized Frame", stabilized_frame) @@ -203,6 +203,8 @@ 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)" + ```python # import required libraries from vidgear.gears.stabilizer import Stabilizer @@ -236,7 +238,7 @@ while True: if stabilized_frame is None: continue - # {do something with the frame here} + # {do something with the stabilized frame here} # write stabilized frame to writer writer.write(stabilized_frame) @@ -271,4 +273,10 @@ writer.close() !!! example "The complete usage example can be found [here ➶](../../videogear/usage/#using-videogear-with-video-stabilizer-backend)" +  + +## Bonus Examples + +!!! example "Checkout more advanced Stabilizer examples with unusual configuration [here ➶](../../../help/stabilizer_ex/)" +   \ No newline at end of file diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 027b1a0d3..b460a41ee 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -170,4 +170,10 @@ from vidgear.gears import StreamGear See here 🚀 -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced StreamGear examples with unusual configuration [here ➶](../../../help/streamgear_ex/)" + +  \ No newline at end of file diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index 8b0d34a3f..6008a65a7 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -155,7 +155,7 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, !!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." -!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." +!!! 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/playlist will contain NO information of any older ones, and therefore resultant DASH/HLS stream will play only the most recent frames." !!! 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." diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 4756f41c4..1db663992 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -82,7 +82,7 @@ You can easily activate ==Low-latency Livestreaming in Single-Source Mode==, whe !!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. Less these value, less will be latency." -!!! warning "All Chunks will be overwritten in this mode after every few Chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, Hence Newer Chunks and Manifest contains NO information of any older video-frames." +!!! 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/playlist will contain NO information of any older ones, and therefore resultant DASH/HLS stream will play only the most recent frames." !!! 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." diff --git a/docs/gears/videogear/usage.md b/docs/gears/videogear/usage.md index b02541e34..3f41d1ab8 100644 --- a/docs/gears/videogear/usage.md +++ b/docs/gears/videogear/usage.md @@ -274,4 +274,10 @@ cv2.destroyAllWindows() stream.stop() ``` +  + +## Bonus Examples + +!!! example "Checkout more advanced VideoGear examples with unusual configuration [here ➶](../../../help/videogear_ex/)" +   \ No newline at end of file diff --git a/docs/gears/webgear/advanced.md b/docs/gears/webgear/advanced.md index 507f42fc5..c0a735366 100644 --- a/docs/gears/webgear/advanced.md +++ b/docs/gears/webgear/advanced.md @@ -108,7 +108,7 @@ async def my_frame_producer(): # do something with your OpenCV frame here # reducer frames size if you want more performance otherwise comment this line - frame = await reducer(frame, percentage=30, interpolation=cv2.INTER_LINEAR) # reduce frame by 30% + frame = await reducer(frame, percentage=30, interpolation=cv2.INTER_AREA) # reduce frame by 30% # handle JPEG encoding encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() # yield frame in byte format @@ -314,75 +314,8 @@ WebGear gives us complete freedom of altering data files generated in [**Auto-Ge   -## Bonus Usage Examples +## Bonus Examples -Because of WebGear API's flexible internal wapper around [VideoGear](../../videogear/overview/), it can easily access any parameter of [CamGear](#camgear) and [PiGear](#pigear) videocapture APIs. - -!!! info "Following usage examples are just an idea of what can be done with WebGear API, you can try various [VideoGear](../../videogear/params/), [CamGear](../../camgear/params/) and [PiGear](../../pigear/params/) parameters directly in WebGear API in the similar manner." - -### Using WebGear with Pi Camera Module - -Here's a bare-minimum example of using WebGear API with the Raspberry Pi camera module while tweaking its various properties in just one-liner: - -```python -# import libs -import uvicorn -from vidgear.gears.asyncio import WebGear - -# various webgear performance and Raspberry Pi camera tweaks -options = { - "frame_size_reduction": 40, - "jpeg_compression_quality": 80, - "jpeg_compression_fastdct": True, - "jpeg_compression_fastupsample": False, - "hflip": True, - "exposure_mode": "auto", - "iso": 800, - "exposure_compensation": 15, - "awb_mode": "horizon", - "sensor_mode": 0, -} - -# initialize WebGear app -web = WebGear( - enablePiCamera=True, resolution=(640, 480), framerate=60, logging=True, **options -) - -# run this app on Uvicorn server at address http://localhost:8000/ -uvicorn.run(web(), host="localhost", port=8000) - -# close app safely -web.shutdown() -``` - -  - -### Using WebGear with real-time Video Stabilization enabled - -Here's an example of using WebGear API with real-time Video Stabilization enabled: - -```python -# import libs -import uvicorn -from vidgear.gears.asyncio import WebGear - -# various webgear performance tweaks -options = { - "frame_size_reduction": 40, - "jpeg_compression_quality": 80, - "jpeg_compression_fastdct": True, - "jpeg_compression_fastupsample": False, -} - -# initialize WebGear app with a raw source and enable video stabilization(`stabilize=True`) -web = WebGear(source="foo.mp4", stabilize=True, logging=True, **options) - -# run this app on Uvicorn server at address http://localhost:8000/ -uvicorn.run(web(), host="localhost", port=8000) - -# close app safely -web.shutdown() -``` +!!! example "Checkout more advanced WebGear examples with unusual configuration [here ➶](../../../help/webgear_ex/)"   - \ No newline at end of file diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index da0887954..1f4646d7a 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -326,69 +326,8 @@ WebGear_RTC gives us complete freedom of altering data files generated in [**Aut   -## Bonus Usage Examples +## Bonus Examples -Because of WebGear_RTC API's flexible internal wapper around [VideoGear](../../videogear/overview/), it can easily access any parameter of [CamGear](#camgear) and [PiGear](#pigear) videocapture APIs. +!!! example "Checkout more advanced WebGear_RTC examples with unusual configuration [here ➶](../../../help/webgear_rtc_ex/)" -!!! info "Following usage examples are just an idea of what can be done with WebGear_RTC API, you can try various [VideoGear](../../videogear/params/), [CamGear](../../camgear/params/) and [PiGear](../../pigear/params/) parameters directly in WebGear_RTC API in the similar manner." - -### Using WebGear_RTC with Pi Camera Module - -Here's a bare-minimum example of using WebGear_RTC API with the Raspberry Pi camera module while tweaking its various properties in just one-liner: - -```python -# import libs -import uvicorn -from vidgear.gears.asyncio import WebGear_RTC - -# various webgear_rtc performance and Raspberry Pi camera tweaks -options = { - "frame_size_reduction": 25, - "hflip": True, - "exposure_mode": "auto", - "iso": 800, - "exposure_compensation": 15, - "awb_mode": "horizon", - "sensor_mode": 0, -} - -# initialize WebGear_RTC app -web = WebGear_RTC( - enablePiCamera=True, resolution=(640, 480), framerate=60, logging=True, **options -) - -# run this app on Uvicorn server at address http://localhost:8000/ -uvicorn.run(web(), host="localhost", port=8000) - -# close app safely -web.shutdown() -``` - -  - -### Using WebGear_RTC with real-time Video Stabilization enabled - -Here's an example of using WebGear_RTC API with real-time Video Stabilization enabled: - -```python -# import libs -import uvicorn -from vidgear.gears.asyncio import WebGear_RTC - -# various webgear_rtc performance tweaks -options = { - "frame_size_reduction": 25, -} - -# initialize WebGear_RTC app with a raw source and enable video stabilization(`stabilize=True`) -web = WebGear_RTC(source="foo.mp4", stabilize=True, logging=True, **options) - -# run this app on Uvicorn server at address http://localhost:8000/ -uvicorn.run(web(), host="localhost", port=8000) - -# close app safely -web.shutdown() -``` - -  - \ No newline at end of file +  \ No newline at end of file diff --git a/docs/gears/writegear/compression/usage.md b/docs/gears/writegear/compression/usage.md index 97d1bd259..bc3b0c0ea 100644 --- a/docs/gears/writegear/compression/usage.md +++ b/docs/gears/writegear/compression/usage.md @@ -221,7 +221,7 @@ In Compression Mode, WriteGear also allows URL strings _(as output)_ for network In this example, we will stream live camera feed directly to Twitch: -!!! info "YouTube-Live Streaming example code also available in [WriteGear FAQs ➶](../../../../help/writegear_faqs/#is-youtube-live-streaming-possibe-with-writegear)" +!!! info "YouTube-Live Streaming example code also available in [WriteGear FAQs ➶](../../../../help/writegear_ex/#using-writegears-compression-mode-for-youtube-live-streaming)" !!! warning "This example assume you already have a [**Twitch Account**](https://www.twitch.tv/) for publishing video." @@ -576,7 +576,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" -!!! danger "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you encounter multiple errors or no output at all." +!!! danger "Make sure this `-i` audio-source it compatible with provided video-source, otherwise you 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." diff --git a/docs/gears/writegear/introduction.md b/docs/gears/writegear/introduction.md index 2e5d7e609..0149e6376 100644 --- a/docs/gears/writegear/introduction.md +++ b/docs/gears/writegear/introduction.md @@ -75,4 +75,10 @@ from vidgear.gears import WriteGear See here 🚀 -  \ No newline at end of file +  + +## Bonus Examples + +!!! example "Checkout more advanced WriteGear examples with unusual configuration [here ➶](../../../help/writegear_ex/)" + +  \ No newline at end of file diff --git a/docs/help/camgear_ex.md b/docs/help/camgear_ex.md new file mode 100644 index 000000000..5a0522d3a --- /dev/null +++ b/docs/help/camgear_ex.md @@ -0,0 +1,243 @@ + + +# CamGear Examples + +  + +## Synchronizing Two Sources in CamGear + +In this example both streams and corresponding frames will be processed synchronously i.e. with no delay: + +!!! danger "Using same source with more than one instances of CamGear can lead to [Global Interpreter Lock (GIL)](https://wiki.python.org/moin/GlobalInterpreterLock#:~:text=In%20CPython%2C%20the%20global%20interpreter,conditions%20and%20ensures%20thread%20safety.&text=The%20GIL%20can%20degrade%20performance%20even%20when%20it%20is%20not%20a%20bottleneck.) that degrades performance even when it is not a bottleneck." + +```python +# import required libraries +from vidgear.gears import CamGear +import cv2 +import time + +# define and start the stream on first source ( For e.g #0 index device) +stream1 = CamGear(source=0, logging=True).start() + +# define and start the stream on second source ( For e.g #1 index device) +stream2 = CamGear(source=1, logging=True).start() + +# infinite loop +while True: + + frameA = stream1.read() + # read frames from stream1 + + frameB = stream2.read() + # read frames from stream2 + + # check if any of two frame is None + if frameA is None or frameB is None: + #if True break the infinite loop + break + + # do something with both frameA and frameB here + cv2.imshow("Output Frame1", frameA) + cv2.imshow("Output Frame2", frameB) + # Show output window of stream1 and stream 2 separately + + key = cv2.waitKey(1) & 0xFF + # check for 'q' key-press + if key == ord("q"): + #if 'q' key-pressed break out + break + + if key == ord("w"): + #if 'w' key-pressed save both frameA and frameB at same time + cv2.imwrite("Image-1.jpg", frameA) + cv2.imwrite("Image-2.jpg", frameB) + #break #uncomment this line to break out after taking images + +cv2.destroyAllWindows() +# close output window + +# safely close both video streams +stream1.stop() +stream2.stop() +``` + +  + +## Using variable Youtube-DL parameters in CamGear + +CamGear provides exclusive attributes `STREAM_RESOLUTION` _(for specifying stream resolution)_ & `STREAM_PARAMS` _(for specifying underlying API(e.g. `youtube-dl`) parameters)_ with its [`options`](../../gears/camgear/params/#options) dictionary parameter. + +The complete usage example is as follows: + +!!! tip "More information on `STREAM_RESOLUTION` & `STREAM_PARAMS` attributes can be found [here ➶](../../gears/camgear/advanced/source_params/#exclusive-camgear-parameters)" + +```python +# import required libraries +from vidgear.gears import CamGear +import cv2 + +# specify attributes +options = {"STREAM_RESOLUTION": "720p", "STREAM_PARAMS": {"nocheckcertificate": True}} + +# Add YouTube Video URL as input source (for e.g https://youtu.be/bvetuLwJIkA) +# and enable Stream Mode (`stream_mode = True`) +stream = CamGear( + source="https://youtu.be/bvetuLwJIkA", stream_mode=True, logging=True, **options +).start() + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # Show output window + cv2.imshow("Output", 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() +``` + + +  + + +## Using CamGear for capturing RSTP/RTMP URLs + +You can open any network stream _(such as RTSP/RTMP)_ just by providing its URL directly to CamGear's [`source`](../../gears/camgear/params/#source) parameter. + +Here's a high-level wrapper code around CamGear API to enable auto-reconnection during capturing: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +??? tip "Enforcing UDP stream" + + You can easily enforce UDP for RSTP streams inplace of default TCP, by putting following lines of code on the top of your existing code: + + ```python + # import required libraries + import os + + # enforce UDP + os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" + ``` + + Finally, use [`backend`](../../gears/camgear/params/#backend) parameter value as `backend=cv2.CAP_FFMPEG` in CamGear. + + +```python +from vidgear.gears import CamGear +import cv2 +import datetime +import time + + +class Reconnecting_CamGear: + def __init__(self, cam_address, reset_attempts=50, reset_delay=5): + self.cam_address = cam_address + self.reset_attempts = reset_attempts + self.reset_delay = reset_delay + self.source = CamGear(source=self.cam_address).start() + self.running = True + + def read(self): + if self.source is None: + return None + if self.running and self.reset_attempts > 0: + frame = self.source.read() + if frame is None: + self.source.stop() + self.reset_attempts -= 1 + print( + "Re-connection Attempt-{} occured at time:{}".format( + str(self.reset_attempts), + datetime.datetime.now().strftime("%m-%d-%Y %I:%M:%S%p"), + ) + ) + time.sleep(self.reset_delay) + self.source = CamGear(source=self.cam_address).start() + # return previous frame + return self.frame + else: + self.frame = frame + return frame + else: + return None + + def stop(self): + self.running = False + self.reset_attempts = 0 + self.frame = None + if not self.source is None: + self.source.stop() + + +if __name__ == "__main__": + # open any valid video stream + stream = Reconnecting_CamGear( + cam_address="rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov", + reset_attempts=20, + reset_delay=5, + ) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if None-type + if frame is None: + break + + # {do something with the frame here} + + # Show output window + cv2.imshow("Output", 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() +``` + +  \ No newline at end of file diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index a55cdc848..a2b394105 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -74,48 +74,7 @@ limitations under the License. CamGear provides exclusive attributes `STREAM_RESOLUTION` _(for specifying stream resolution)_ & `STREAM_PARAMS` _(for specifying underlying API(e.g. `youtube-dl`) parameters)_ with its [`options`](../../gears/camgear/params/#options) dictionary parameter. The complete usage example is as follows: -!!! tip "More information on `STREAM_RESOLUTION` & `STREAM_PARAMS` attributes can be found [here ➶](../../gears/camgear/advanced/source_params/#exclusive-camgear-parameters)" - -```python -# import required libraries -from vidgear.gears import CamGear -import cv2 - -# specify attributes -options = {"STREAM_RESOLUTION": "720p", "STREAM_PARAMS": {"nocheckcertificate": True}} - -# Add YouTube Video URL as input source (for e.g https://youtu.be/bvetuLwJIkA) -# and enable Stream Mode (`stream_mode = True`) -stream = CamGear( - source="https://youtu.be/bvetuLwJIkA", stream_mode=True, logging=True, **options -).start() - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # Show output window - cv2.imshow("Output", 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() -``` +**Answer:** See [this bonus example ➶](../camgear_ex/#using-variable-youtube-dl-parameters-in-camgear).   @@ -125,55 +84,7 @@ stream.stop() You can open any local network stream _(such as RTSP)_ just by providing its URL directly to CamGear's [`source`](../../gears/camgear/params/#source) parameter. The complete usage example is as follows: -??? tip "Enforcing UDP stream" - - You can easily enforce UDP for RSTP streams inplace of default TCP, by putting following lines of code on the top of your existing code: - - ```python - # import required libraries - import os - - # enforce UDP - os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" - ``` - - Finally, use [`backend`](../../gears/camgear/params/#backend) parameter value as `backend=cv2.CAP_FFMPEG` in CamGear. - - -```python -# import required libraries -from vidgear.gears import CamGear -import cv2 - -# open valid network video-stream -stream = CamGear(source="rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov").start() - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # Show output window - cv2.imshow("Output", 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() -``` +**Answer:** See [this bonus example ➶](../camgear_ex/#using-camgear-for-capturing-rstprtmp-urls).   diff --git a/docs/help/get_help.md b/docs/help/get_help.md index 619e2d56a..b01b34706 100644 --- a/docs/help/get_help.md +++ b/docs/help/get_help.md @@ -37,7 +37,7 @@ There are several ways to get help with VidGear: > Got a question related to VidGear Working? -Checkout our Frequently Asked Questions, a curated list of all the questions with adequate answer that we commonly receive, for quickly troubleshooting your problems: +Checkout the Frequently Asked Questions, a curated list of all the questions with adequate answer that we commonly receive, for quickly troubleshooting your problems: - [General FAQs ➶](general_faqs.md) - [CamGear FAQs ➶](camgear_faqs.md) @@ -56,6 +56,26 @@ Checkout our Frequently Asked Questions, a curated list of all the questions wit   +## Bonus Examples + +> How we do this with that API? + +Checkout the Bonus Examples, a curated list of all advanced examples with unusual configuration, which isn't available in Vidgear API's usage examples: + +- [CamGear FAQs ➶](camgear_ex.md) +- [PiGear FAQs ➶](pigear_ex.md) +- [ScreenGear FAQs ➶](screengear_ex.md) +- [StreamGear FAQs ➶](streamgear_ex.md) +- [WriteGear FAQs ➶](writegear_ex.md) +- [NetGear FAQs ➶](netgear_ex.md) +- [WebGear FAQs ➶](webgear_ex.md) +- [WebGear_RTC FAQs ➶](webgear_rtc_ex.md) +- [VideoGear FAQs ➶](videogear_ex.md) +- [Stabilizer Class FAQs ➶](stabilizer_ex.md) +- [NetGear_Async FAQs ➶](netgear_async_ex.md) + +  + ## Join our Gitter Community channel > Have you come up with some new idea 💡 or looking for the fastest way troubleshoot your problems diff --git a/docs/help/netgear_async_ex.md b/docs/help/netgear_async_ex.md new file mode 100644 index 000000000..a46c17c7e --- /dev/null +++ b/docs/help/netgear_async_ex.md @@ -0,0 +1,169 @@ + + +# NetGear_Async Examples + +  + +## Using NetGear_Async with WebGear + +The complete usage example is as follows: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +### Client + WebGear Server + +Open a terminal on Client System where you want to display the input frames _(and setup WebGear server)_ received from the Server and execute the following python code: + +!!! danger "After running this code, Make sure to open Browser immediately otherwise NetGear_Async will soon exit with `TimeoutError`. You can also try setting [`timeout`](../../gears/netgear_async/params/#timeout) parameter to a higher value to extend this timeout." + +!!! warning "Make sure you use different `port` value for NetGear_Async and WebGear API." + +!!! alert "High CPU utilization may occur on Client's end. User discretion is advised." + +!!! note "Note down the IP-address of this system _(required at Server's end)_ by executing the `hostname -I` command and also replace it in the following code."" + +```python +# import libraries +from vidgear.gears.asyncio import NetGear_Async +from vidgear.gears.asyncio import WebGear +from vidgear.gears.asyncio.helper import reducer +import uvicorn, asyncio, cv2 + +# Define NetGear_Async Client at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with yours !!! +client = NetGear_Async( + receive_mode=True, + pattern=1, + logging=True, +).launch() + +# create your own custom frame producer +async def my_frame_producer(): + + # loop over Client's Asynchronous Frame Generator + async for frame in client.recv_generator(): + + # {do something with received frames here} + + # reducer frames size if you want more performance otherwise comment this line + frame = await reducer( + frame, percentage=30, interpolation=cv2.INTER_AREA + ) # reduce frame by 30% + + # handle JPEG encoding + encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() + # yield frame in byte format + yield (b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n") + await asyncio.sleep(0) + + +if __name__ == "__main__": + # Set event loop to client's + asyncio.set_event_loop(client.loop) + + # initialize WebGear app without any source + web = WebGear(logging=True) + + # add your custom frame producer to config with adequate IP address + web.config["generator"] = my_frame_producer + + # run this app on Uvicorn server at address http://localhost:8000/ + uvicorn.run(web(), host="localhost", port=8000) + + # safely close client + client.close() + + # close app safely + web.shutdown() +``` + +!!! success "On successfully running this code, the output stream will be displayed at address http://localhost:8000/ in your Client's Browser." + +### Server + +Now, Open the terminal on another Server System _(with a webcam connected to it at index 0)_, and execute the following python code: + +!!! note "Replace the IP address in the following code with Client's IP address you noted earlier." + +```python +# import library +from vidgear.gears.asyncio import NetGear_Async +import cv2, asyncio + +# initialize Server without any source +server = NetGear_Async( + source=None, + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, +) + +# Create a async frame generator as custom source +async def my_frame_generator(): + + # !!! define your own video source here !!! + # Open any video stream such as live webcam + # video stream on first index(i.e. 0) device + stream = cv2.VideoCapture(0) + + # loop over stream until its terminated + while True: + + # read frames + (grabbed, frame) = stream.read() + + # check if frame empty + if not grabbed: + break + + # do something with the frame to be sent here + + # yield frame + yield frame + # sleep for sometime + await asyncio.sleep(0) + + # close stream + stream.release() + + +if __name__ == "__main__": + # set event loop + asyncio.set_event_loop(server.loop) + # Add your custom source generator to Server configuration + server.config["generator"] = my_frame_generator() + # Launch the Server + server.launch() + try: + # run your main function task until it is complete + server.loop.run_until_complete(server.task) + except (KeyboardInterrupt, SystemExit): + # wait for interrupts + pass + finally: + # finally close the server + server.close() +``` + +  diff --git a/docs/help/netgear_ex.md b/docs/help/netgear_ex.md new file mode 100644 index 000000000..ef43baaa8 --- /dev/null +++ b/docs/help/netgear_ex.md @@ -0,0 +1,368 @@ + + +# NetGear Examples + +  + +## Using NetGear with WebGear + +The complete usage example is as follows: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +### Client + WebGear Server + +Open a terminal on Client System where you want to display the input frames _(and setup WebGear server)_ received from the Server and execute the following python code: + +!!! danger "After running this code, Make sure to open Browser immediately otherwise NetGear will soon exit with `RuntimeError`. You can also try setting [`max_retries`](../../gears/netgear/params/#options) and [`request_timeout`](../../gears/netgear/params/#options) like attributes to a higher value to avoid this." + +!!! warning "Make sure you use different `port` value for NetGear and WebGear API." + +!!! alert "High CPU utilization may occur on Client's end. User discretion is advised." + +!!! note "Note down the IP-address of this system _(required at Server's end)_ by executing the `hostname -I` command and also replace it in the following code."" + +```python +# import necessary libs +import uvicorn, asyncio, cv2 +from vidgear.gears.asyncio import WebGear +from vidgear.gears.asyncio.helper import reducer + +# initialize WebGear app without any source +web = WebGear(logging=True) + + +# activate jpeg encoding and specify other related parameters +options = { + "jpeg_compression": True, + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + +# create your own custom frame producer +async def my_frame_producer(): + # initialize global params + # Define NetGear Client at given IP address and define parameters + # !!! change following IP address '192.168.x.xxx' with yours !!! + client = NetGear( + receive_mode=True, + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options, + ) + + # loop over frames + while True: + # receive frames from network + frame = self.client.recv() + + # if NoneType + if frame is None: + return None + + # do something with your OpenCV frame here + + # reducer frames size if you want more performance otherwise comment this line + frame = await reducer( + frame, percentage=30, interpolation=cv2.INTER_AREA + ) # reduce frame by 30% + + # handle JPEG encoding + encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() + # yield frame in byte format + yield (b"--frame\r\nContent-Type:image/jpeg\r\n\r\n" + encodedImage + b"\r\n") + await asyncio.sleep(0) + # close stream + client.close() + + +# add your custom frame producer to config with adequate IP address +web.config["generator"] = my_frame_producer + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +!!! success "On successfully running this code, the output stream will be displayed at address http://localhost:8000/ in your Client's Browser." + + +### Server + +Now, Open the terminal on another Server System _(with a webcam connected to it at index 0)_, and execute the following python code: + +!!! note "Replace the IP address in the following code with Client's IP address you noted earlier." + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import NetGear +import cv2 + +# activate jpeg encoding and specify other related parameters +options = { + "jpeg_compression": True, + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + +# Open live video stream on webcam at first index(i.e. 0) device +stream = VideoGear(source=0).start() + +# Define NetGear server at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with client's IP address !!! +server = NetGear( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options +) + +# loop over until KeyBoard Interrupted +while True: + + try: + # read frames from stream + frame = stream.read() + + # check for frame if None-type + if frame is None: + break + + # {do something with the frame here} + + # send frame to server + server.send(frame) + + except KeyboardInterrupt: + break + +# safely close video stream +stream.stop() + +# safely close server +server.close() +``` + +  + +## Using NetGear with WebGear_RTC + +The complete usage example is as follows: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +### Client + WebGear_RTC Server + +Open a terminal on Client System where you want to display the input frames _(and setup WebGear_RTC server)_ received from the Server and execute the following python code: + +!!! danger "After running this code, Make sure to open Browser immediately otherwise NetGear will soon exit with `RuntimeError`. You can also try setting [`max_retries`](../../gears/netgear/params/#options) and [`request_timeout`](../../gears/netgear/params/#options) like attributes to a higher value to avoid this." + +!!! warning "Make sure you use different `port` value for NetGear and WebGear_RTC API." + +!!! alert "High CPU utilization may occur on Client's end. User discretion is advised." + +!!! note "Note down the IP-address of this system _required at Server's end)_ by executing the `hostname -I` command and also replace it in the following code."" + +```python +# import required libraries +import uvicorn, asyncio, cv2 +from av import VideoFrame +from aiortc import VideoStreamTrack +from aiortc.mediastreams import MediaStreamError +from vidgear.gears import NetGear +from vidgear.gears.asyncio import WebGear_RTC +from vidgear.gears.asyncio.helper import reducer + +# initialize WebGear_RTC app without any source +web = WebGear_RTC(logging=True) + +# activate jpeg encoding and specify other related parameters +options = { + "jpeg_compression": True, + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + + +# create your own Bare-Minimum Custom Media Server +class Custom_RTCServer(VideoStreamTrack): + """ + Custom Media Server using OpenCV, an inherit-class + to aiortc's VideoStreamTrack. + """ + + def __init__( + self, + address=None, + port="5454", + protocol="tcp", + pattern=1, + logging=True, + options={}, + ): + # don't forget this line! + super().__init__() + + # initialize global params + # Define NetGear Client at given IP address and define parameters + self.client = NetGear( + receive_mode=True, + address=address, + port=protocol, + pattern=pattern, + receive_mode=True, + logging=logging, + **options + ) + + async def recv(self): + """ + A coroutine function that yields `av.frame.Frame`. + """ + # don't forget this function!!! + + # get next timestamp + pts, time_base = await self.next_timestamp() + + # receive frames from network + frame = self.client.recv() + + # if NoneType + if frame is None: + raise MediaStreamError + + # reducer frames size if you want more performance otherwise comment this line + frame = await reducer(frame, percentage=30) # reduce frame by 30% + + # contruct `av.frame.Frame` from `numpy.nd.array` + av_frame = VideoFrame.from_ndarray(frame, format="bgr24") + av_frame.pts = pts + av_frame.time_base = time_base + + # return `av.frame.Frame` + return av_frame + + def terminate(self): + """ + Gracefully terminates VideoGear stream + """ + # don't forget this function!!! + + # terminate + if not (self.client is None): + self.client.close() + self.client = None + + +# assign your custom media server to config with adequate IP address +# !!! change following IP address '192.168.x.xxx' with yours !!! +web.config["server"] = Custom_RTCServer( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options +) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +!!! success "On successfully running this code, the output stream will be displayed at address http://localhost:8000/ in your Client's Browser." + +### Server + +Now, Open the terminal on another Server System _(with a webcam connected to it at index 0)_, and execute the following python code: + +!!! note "Replace the IP address in the following code with Client's IP address you noted earlier." + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import NetGear +import cv2 + +# activate jpeg encoding and specify other related parameters +options = { + "jpeg_compression": True, + "jpeg_compression_quality": 90, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": True, +} + +# Open live video stream on webcam at first index(i.e. 0) device +stream = VideoGear(source=0).start() + +# Define NetGear server at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with client's IP address !!! +server = NetGear( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options +) + +# loop over until KeyBoard Interrupted +while True: + + try: + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to server + server.send(frame) + + except KeyboardInterrupt: + break + +# safely close video stream +stream.stop() + +# safely close server +server.close() +``` + +  \ No newline at end of file diff --git a/docs/help/pigear_ex.md b/docs/help/pigear_ex.md new file mode 100644 index 000000000..03d86f63e --- /dev/null +++ b/docs/help/pigear_ex.md @@ -0,0 +1,75 @@ + + +# 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: + +```python +# import required libraries +from vidgear.gears import PiGear +import cv2 + +# initial parameters +options = {"brightness": 80} # set brightness to 80 + +# open pi video stream with default parameters +stream = PiGear(logging=True, **options).start() + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for 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 + # check for 'z' key if pressed + if key == ord("z"): + # change brightness to 50 + stream.stream.brightness = 50 + +# close output window +cv2.destroyAllWindows() + +# safely close video stream +stream.stop() +``` + +  \ No newline at end of file diff --git a/docs/help/pigear_faqs.md b/docs/help/pigear_faqs.md index 41725348e..30661a518 100644 --- a/docs/help/pigear_faqs.md +++ b/docs/help/pigear_faqs.md @@ -67,53 +67,6 @@ limitations under the License. ## How to change `picamera` settings for Camera Module at runtime? -**Answer:** You can use `stream` global parameter in PiGear to feed any `picamera` setting at runtime. See following sample usage example: - -!!! info "" - 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 -# import required libraries -from vidgear.gears import PiGear -import cv2 - -# initial parameters -options = {"brightness": 80} # set brightness to 80 - -# open pi video stream with default parameters -stream = PiGear(logging=True, **options).start() - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for 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 - # check for 'z' key if pressed - if key == ord("z"): - # change brightness to 50 - stream.stream.brightness = 50 - -# close output window -cv2.destroyAllWindows() - -# safely close video stream -stream.stop() -``` +**Answer:** You can use `stream` global parameter in PiGear to feed any `picamera` setting at runtime. See [this bonus example ➶](../pigear_ex/#setting-variable-picamera-parameters-for-camera-module-at-runtime).   \ No newline at end of file diff --git a/docs/help/screengear_ex.md b/docs/help/screengear_ex.md new file mode 100644 index 000000000..80463ee11 --- /dev/null +++ b/docs/help/screengear_ex.md @@ -0,0 +1,149 @@ + + +# ScreenGear Examples + +  + +## Using ScreenGear with NetGear and WriteGear + +The complete usage example is as follows: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +### Client + WriteGear + +Open a terminal on Client System _(where you want to save the input frames received from the Server)_ and execute the following python code: + +!!! info "Note down the IP-address of this system(required at Server's end) by executing the command: `hostname -I` and also replace it in the following code." + +!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import required libraries +from vidgear.gears import NetGear +from vidgear.gears import WriteGear +import cv2 + +# define various tweak flags +options = {"flag": 0, "copy": False, "track": False} + +# Define Netgear Client at given IP address and define parameters +# !!! change following IP address '192.168.x.xxx' with yours !!! +client = NetGear( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + receive_mode=True, + logging=True, + **options +) + +# Define writer with default parameters and suitable output filename for e.g. `Output.mp4` +writer = WriteGear(output_filename="Output.mp4") + +# 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} + + # write frame to writer + writer.write(frame) + +# close output window +cv2.destroyAllWindows() + +# safely close client +client.close() + +# safely close writer +writer.close() +``` + +### Server + ScreenGear + +Now, Open the terminal on another Server System _(with a montior/display attached to it)_, and execute the following python code: + +!!! info "Replace the IP address in the following code with Client's IP address you noted earlier." + +!!! tip "You can terminate stream on both side anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import NetGear + +# define dimensions of screen w.r.t to given monitor to be captured +options = {"top": 40, "left": 0, "width": 100, "height": 100} + +# open stream with defined parameters +stream = ScreenGear(logging=True, **options).start() + +# define various netgear tweak flags +options = {"flag": 0, "copy": False, "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 !!! +server = NetGear( + address="192.168.x.xxx", + port="5454", + protocol="tcp", + pattern=1, + logging=True, + **options +) + +# loop over until KeyBoard Interrupted +while True: + + try: + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to server + server.send(frame) + + except KeyboardInterrupt: + break + +# safely close video stream +stream.stop() + +# safely close server +server.close() +``` + +  + diff --git a/docs/help/stabilizer_ex.md b/docs/help/stabilizer_ex.md new file mode 100644 index 000000000..8b8636265 --- /dev/null +++ b/docs/help/stabilizer_ex.md @@ -0,0 +1,236 @@ + + +# Stabilizer Class Examples + +  + +## Saving Stabilizer Class output with Live Audio Input + +In this example code, we will merging the audio from a Audio Device _(for e.g. Webcam inbuilt mic input)_ with Stablized frames incoming from the Stabilizer Class _(which is also using same Webcam video input through OpenCV)_, and save the final output as a compressed video file, all in real time: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! alert "Example Assumptions" + + * You're running are Linux machine. + * You already have appropriate audio driver and software installed on your machine. + + +??? tip "Identifying and Specifying sound card on different OS platforms" + + === "On Windows" + + Windows OS users can use the [dshow](https://trac.ffmpeg.org/wiki/DirectShow) (DirectShow) to list audio input device which is the preferred option for Windows users. You can refer following steps to identify and specify your sound card: + + - [x] **[OPTIONAL] Enable sound card(if disabled):** First enable your Stereo Mix by opening the "Sound" window and select the "Recording" tab, then right click on the window and select "Show Disabled Devices" to toggle the Stereo Mix device visibility. **Follow this [post ➶](https://forums.tomshardware.com/threads/no-sound-through-stereo-mix-realtek-hd-audio.1716182/) for more details.** + + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `dshow` as follows: + + ```sh + c:\> ffmpeg -list_devices true -f dshow -i dummy + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [dshow @ 03ACF580] DirectShow video devices + [dshow @ 03ACF580] "Integrated Camera" + [dshow @ 03ACF580] "USB2.0 Camera" + [dshow @ 03ACF580] DirectShow audio devices + [dshow @ 03ACF580] "Microphone (Realtek High Definition Audio)" + [dshow @ 03ACF580] "Microphone (USB2.0 Camera)" + dummy: Immediate exit requested + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + output_params = { + "-i":"audio=Microphone (USB2.0 Camera)", + "-thread_queue_size": "512", + "-f": "dshow", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! 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" + + + === "On Linux" + + Linux OS users can use the [alsa](https://ffmpeg.org/ffmpeg-all.html#alsa) to list input device to capture live audio input such as from a webcam. You can refer following steps to identify and specify your sound card: + + - [x] **Identify Sound Card:** To get the list of all installed cards on your machine, you can type `arecord -l` or `arecord -L` _(longer output)_. + + ```sh + arecord -l + + **** List of CAPTURE Hardware Devices **** + card 0: ICH5 [Intel ICH5], device 0: Intel ICH [Intel ICH5] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 1: Intel ICH - MIC ADC [Intel ICH5 - MIC ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 2: Intel ICH - MIC2 ADC [Intel ICH5 - MIC2 ADC] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 0: ICH5 [Intel ICH5], device 3: Intel ICH - ADC2 [Intel ICH5 - ADC2] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + card 1: U0x46d0x809 [USB Device 0x46d:0x809], device 0: USB Audio [USB Audio] + Subdevices: 1/1 + Subdevice #0: subdevice #0 + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in WriteGear as follows: + + !!! info "The easiest thing to do is to reference sound card directly, namely "card 0" (Intel ICH5) and "card 1" (Microphone on the USB web cam), as `hw:0` or `hw:1`" + + ```python + # assign appropriate input audio-source + output_params = { + "-i": "hw:1", + "-thread_queue_size": "512", + "-f": "alsa", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! fail "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" + + + === "On MacOS" + + MAC OS users can use the [avfoundation](https://ffmpeg.org/ffmpeg-devices.html#avfoundation) to list input devices for grabbing audio from integrated iSight cameras as well as cameras connected via USB or FireWire. You can refer following steps to identify and specify your sound card on MacOS/OSX machines: + + + - [x] **Identify Sound Card:** Then, You can locate your soundcard using `avfoundation` as follows: + + ```sh + ffmpeg -f qtkit -list_devices true -i "" + ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect + libavutil 51. 74.100 / 51. 74.100 + libavcodec 54. 65.100 / 54. 65.100 + libavformat 54. 31.100 / 54. 31.100 + libavdevice 54. 3.100 / 54. 3.100 + libavfilter 3. 19.102 / 3. 19.102 + libswscale 2. 1.101 / 2. 1.101 + libswresample 0. 16.100 / 0. 16.100 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation video devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] FaceTime HD camera (built-in) + [AVFoundation input device @ 0x7f8e2540ef20] [1] Capture screen 0 + [AVFoundation input device @ 0x7f8e2540ef20] AVFoundation audio devices: + [AVFoundation input device @ 0x7f8e2540ef20] [0] Blackmagic Audio + [AVFoundation input device @ 0x7f8e2540ef20] [1] Built-in Microphone + ``` + + + - [x] **Specify Sound Card:** Then, you can specify your located soundcard in StreamGear as follows: + + ```python + # assign appropriate input audio-source + output_params = { + "-audio_device_index": "0", + "-thread_queue_size": "512", + "-f": "avfoundation", + "-ac": "2", + "-acodec": "aac", + "-ar": "44100", + } + ``` + + !!! fail "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 encounter multiple errors or no output at all." + +!!! warning "You **MUST** use [`-input_framerate`](../../gears/writegear/compression/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." + +```python +# import required libraries +from vidgear.gears import WriteGear +from vidgear.gears.stabilizer import Stabilizer +import cv2 + +# Open suitable video stream, such as webcam on first index(i.e. 0) +stream = cv2.VideoCapture(0) + +# initiate stabilizer object with defined parameters +stab = Stabilizer(smoothing_radius=30, crop_n_zoom=True, border_size=5, logging=True) + +# change with your webcam soundcard, plus add additional required FFmpeg parameters for your writer +output_params = { + "-thread_queue_size": "512", + "-f": "alsa", + "-ac": "1", + "-ar": "48000", + "-i": "plughw:CARD=CAMERA,DEV=0", +} + +# Define writer with defined parameters and suitable output filename for e.g. `Output.mp4 +writer = WriteGear(output_filename="Output.mp4", logging=True, **output_params) + +# loop over +while True: + + # read frames from stream + (grabbed, frame) = stream.read() + + # check for frame if not grabbed + if not grabbed: + break + + # send current frame to stabilizer for processing + stabilized_frame = stab.stabilize(frame) + + # wait for stabilizer which still be initializing + if stabilized_frame is None: + continue + + # {do something with the stabilized frame here} + + # write stabilized frame to writer + writer.write(stabilized_frame) + + +# clear stabilizer resources +stab.clean() + +# safely close video stream +stream.release() + +# safely close writer +writer.close() +``` + +  \ No newline at end of file diff --git a/docs/help/streamgear_ex.md b/docs/help/streamgear_ex.md new file mode 100644 index 000000000..d8a83db14 --- /dev/null +++ b/docs/help/streamgear_ex.md @@ -0,0 +1,161 @@ + + +# StreamGear Examples + +  + +## StreamGear Live-Streaming Usage with PiGear + +In this example, we will be Live-Streaming video-frames from Raspberry Pi _(with Camera Module connected)_ using PiGear API and StreamGear API's Real-time Frames Mode: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! tip "Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks. 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/playlist will contain NO information of any older ones, and therefore resultant DASH/HLS stream will play only the most recent frames." + +!!! 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`](../../gears/streamgear/params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." + +=== "DASH" + + ```python + # import required libraries + from vidgear.gears import PiGear + from vidgear.gears import StreamGear + import cv2 + + # add various Picamera tweak parameters to dictionary + options = { + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, + } + + # open pi video stream with defined parameters + stream = PiGear(resolution=(640, 480), framerate=60, logging=True, **options).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", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ``` + +=== "HLS" + + ```python + # import required libraries + from vidgear.gears import PiGear + from vidgear.gears import StreamGear + import cv2 + + # add various Picamera tweak parameters to dictionary + options = { + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, + } + + # open pi video stream with defined parameters + stream = PiGear(resolution=(640, 480), framerate=60, logging=True, **options).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", **stream_params) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # send frame to streamer + streamer.stream(frame) + + # 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.terminate() + ``` + + +  \ No newline at end of file diff --git a/docs/help/videogear_ex.md b/docs/help/videogear_ex.md new file mode 100644 index 000000000..de8a92053 --- /dev/null +++ b/docs/help/videogear_ex.md @@ -0,0 +1,220 @@ + + +# VideoGear Examples + +  + +## Using VideoGear with ROS(Robot Operating System) + +We will be using [`cv_bridge`](http://wiki.ros.org/cv_bridge/Tutorials/ConvertingBetweenROSImagesAndOpenCVImagesPython) to convert OpenCV frames to ROS image messages and vice-versa. + +In this example, we'll create a node that convert OpenCV frames into ROS image messages, and then publishes them over ROS. + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! note "This example is vidgear implementation of this [wiki example](http://wiki.ros.org/cv_bridge/Tutorials/ConvertingBetweenROSImagesAndOpenCVImagesPython)." + +```python +# import roslib +import roslib + +roslib.load_manifest("my_package") + +# import other required libraries +import sys +import rospy +import cv2 +from std_msgs.msg import String +from sensor_msgs.msg import Image +from cv_bridge import CvBridge, CvBridgeError +from vidgear.gears import VideoGear + +# custom publisher class +class image_publisher: + def __init__(self, source=0, logging=False): + # create CV bridge + self.bridge = CvBridge() + # define publisher topic + self.image_pub = rospy.Publisher("image_topic_pub", Image) + # open stream with given parameters + self.stream_stab = VideoGear(source=source, logging=logging).start() + # define publisher topic + rospy.Subscriber("image_topic_sub", Image, self.callback) + + def callback(self, data): + + # {do something with received ROS node data here} + + # read stabilized frames + frame = self.stream.read() + # check for stabilized frame if None-type + if not (frame is None): + + # {do something with the frame here} + + # publish our frame + try: + self.image_pub.publish(self.bridge.cv2_to_imgmsg(frame, "bgr8")) + except CvBridgeError as e: + # catch any errors + print(e) + + def close(self): + # stop stream + self.stream_stab.stop() + + +def main(args): + # !!! define your own video source here !!! + # Open any video stream such as live webcam + # video stream on first index(i.e. 0) device + + # define publisher + ic = image_publisher(source=0, logging=True) + # initiate ROS node on publisher + rospy.init_node("image_publisher", anonymous=True) + try: + # run node + rospy.spin() + except KeyboardInterrupt: + print("Shutting down") + finally: + # close publisher + ic.close() + + +if __name__ == "__main__": + main(sys.argv) +``` + +  + +## Using VideoGear for capturing RSTP/RTMP URLs + +Here's a high-level wrapper code around VideoGear API to enable auto-reconnection during capturing, plus stabilization is enabled _(`stabilize=True`)_ in order to stabilize captured frames on-the-go: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +??? tip "Enforcing UDP stream" + + You can easily enforce UDP for RSTP streams inplace of default TCP, by putting following lines of code on the top of your existing code: + + ```python + # import required libraries + import os + + # enforce UDP + os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;udp" + ``` + + Finally, use [`backend`](../../gears/videogear/params/#backend) parameter value as `backend=cv2.CAP_FFMPEG` in VideoGear. + + +```python +from vidgear.gears import VideoGear +import cv2 +import datetime +import time + + +class Reconnecting_VideoGear: + def __init__(self, cam_address, stabilize=False, reset_attempts=50, reset_delay=5): + self.cam_address = cam_address + self.stabilize = stabilize + self.reset_attempts = reset_attempts + self.reset_delay = reset_delay + self.source = VideoGear( + source=self.cam_address, stabilize=self.stabilize + ).start() + self.running = True + + def read(self): + if self.source is None: + return None + if self.running and self.reset_attempts > 0: + frame = self.source.read() + if frame is None: + self.source.stop() + self.reset_attempts -= 1 + print( + "Re-connection Attempt-{} occured at time:{}".format( + str(self.reset_attempts), + datetime.datetime.now().strftime("%m-%d-%Y %I:%M:%S%p"), + ) + ) + time.sleep(self.reset_delay) + self.source = VideoGear( + source=self.cam_address, stabilize=self.stabilize + ).start() + # return previous frame + return self.frame + else: + self.frame = frame + return frame + else: + return None + + def stop(self): + self.running = False + self.reset_attempts = 0 + self.frame = None + if not self.source is None: + self.source.stop() + + +if __name__ == "__main__": + # open any valid video stream + stream = Reconnecting_VideoGear( + cam_address="rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov", + reset_attempts=20, + reset_delay=5, + ) + + # loop over + while True: + + # read frames from stream + frame = stream.read() + + # check for frame if None-type + if frame is None: + break + + # {do something with the frame here} + + # Show output window + cv2.imshow("Output", 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() +``` + +  \ No newline at end of file diff --git a/docs/help/webgear_ex.md b/docs/help/webgear_ex.md new file mode 100644 index 000000000..05b1dc628 --- /dev/null +++ b/docs/help/webgear_ex.md @@ -0,0 +1,233 @@ + + +# WebGear Examples + +  + +## Using WebGear with RaspberryPi Camera Module + +Because of WebGear API's flexible internal wapper around VideoGear, it can easily access any parameter of CamGear and PiGear videocapture APIs. + +!!! info "Following usage examples are just an idea of what can be done with WebGear API, you can try various [VideoGear](../../gears/videogear/params/), [CamGear](../../gears/camgear/params/) and [PiGear](../../gears/pigear/params/) parameters directly in WebGear API in the similar manner." + +Here's a bare-minimum example of using WebGear API with the Raspberry Pi camera module while tweaking its various properties in just one-liner: + +```python +# import libs +import uvicorn +from vidgear.gears.asyncio import WebGear + +# various webgear performance and Raspberry Pi camera tweaks +options = { + "frame_size_reduction": 40, + "jpeg_compression_quality": 80, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": False, + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, +} + +# initialize WebGear app +web = WebGear( + enablePiCamera=True, resolution=(640, 480), framerate=60, logging=True, **options +) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +  + +## Using WebGear with real-time Video Stabilization enabled + +Here's an example of using WebGear API with real-time Video Stabilization enabled: + +```python +# import libs +import uvicorn +from vidgear.gears.asyncio import WebGear + +# various webgear performance tweaks +options = { + "frame_size_reduction": 40, + "jpeg_compression_quality": 80, + "jpeg_compression_fastdct": True, + "jpeg_compression_fastupsample": False, +} + +# initialize WebGear app with a raw source and enable video stabilization(`stabilize=True`) +web = WebGear(source="foo.mp4", stabilize=True, logging=True, **options) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +  + + +## Display Two Sources Simultaneously in WebGear + +In this example, we'll be displaying two video feeds side-by-side simultaneously on browser using WebGear API by defining two separate frame generators: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +**Step-1 (Trigger Auto-Generation Process):** Firstly, run this bare-minimum code to trigger the [**Auto-generation**](../../gears/webgear/#auto-generation-process) process, this will create `.vidgear` directory at current location _(directory where you'll run this code)_: + +```python +# import required libraries +import uvicorn +from vidgear.gears.asyncio import WebGear + +# provide current directory to save data files +options = {"custom_data_location": "./"} + +# initialize WebGear app +web = WebGear(source=0, logging=True, **options) + +# close app safely +web.shutdown() +``` + +**Step-2 (Replace HTML file):** Now, go inside `.vidgear` :arrow_right: `webgear` :arrow_right: `templates` directory at current location of your machine, and there replace content of `index.html` file with following: + +```html +{% extends "base.html" %} +{% block content %} +

WebGear Video Feed

+
+ Feed + Feed +
+{% endblock %} +``` + +**Step-3 (Build your own Frame Producers):** Now, create a python script code with OpenCV source, as follows: + +```python +# import necessary libs +import uvicorn, asyncio, cv2 +from vidgear.gears.asyncio import WebGear +from vidgear.gears.asyncio.helper import reducer +from starlette.responses import StreamingResponse +from starlette.routing import Route + +# provide current directory to load data files +options = {"custom_data_location": "./"} + +# initialize WebGear app without any source +web = WebGear(logging=True, **options) + +# create your own custom frame producer +async def my_frame_producer1(): + + # !!! define your first video source here !!! + # Open any video stream such as "foo1.mp4" + stream = cv2.VideoCapture("foo1.mp4") + # loop over frames + while True: + # read frame from provided source + (grabbed, frame) = stream.read() + # break if NoneType + if not grabbed: + break + + # do something with your OpenCV frame here + + # reducer frames size if you want more performance otherwise comment this line + frame = await reducer(frame, percentage=30) # reduce frame by 30% + # handle JPEG encoding + encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() + # yield frame in byte format + yield (b"--frame\r\nContent-Type:video/jpeg2000\r\n\r\n" + encodedImage + b"\r\n") + await asyncio.sleep(0.00001) + # close stream + stream.release() + + +# create your own custom frame producer +async def my_frame_producer2(): + + # !!! define your second video source here !!! + # Open any video stream such as "foo2.mp4" + stream = cv2.VideoCapture("foo2.mp4") + # loop over frames + while True: + # read frame from provided source + (grabbed, frame) = stream.read() + # break if NoneType + if not grabbed: + break + + # do something with your OpenCV frame here + + # reducer frames size if you want more performance otherwise comment this line + frame = await reducer(frame, percentage=30) # reduce frame by 30% + # handle JPEG encoding + encodedImage = cv2.imencode(".jpg", frame)[1].tobytes() + # yield frame in byte format + yield (b"--frame\r\nContent-Type:video/jpeg2000\r\n\r\n" + encodedImage + b"\r\n") + await asyncio.sleep(0.00001) + # close stream + stream.release() + + +async def custom_video_response(scope): + """ + Return a async video streaming response for `my_frame_producer2` generator + """ + assert scope["type"] in ["http", "https"] + await asyncio.sleep(0.00001) + return StreamingResponse( + my_frame_producer2(), + media_type="multipart/x-mixed-replace; boundary=frame", + ) + + +# add your custom frame producer to config +web.config["generator"] = my_frame_producer1 + +# append new route i.e. new custom route with custom response +web.routes.append( + Route("/video2", endpoint=custom_video_response) + ) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +!!! success "On successfully running this code, the output stream will be displayed at address http://localhost:8000/ in Browser." + + +  \ No newline at end of file diff --git a/docs/help/webgear_faqs.md b/docs/help/webgear_faqs.md index ca7e1b42d..e39194337 100644 --- a/docs/help/webgear_faqs.md +++ b/docs/help/webgear_faqs.md @@ -48,7 +48,7 @@ limitations under the License. ## Is it possible to stream on a different device on the network with WebGear? -!!! note "If you set `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine, then you must still use http://localhost:8000/ to access stream on your host machine browser." +!!! alert "If you set `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine, then you must still use http://localhost:8000/ to access stream on that same host machine browser." For accessing WebGear on different Client Devices on the network, use `"0.0.0.0"` as host value instead of `"localhost"` on Host Machine. Then type the IP-address of source machine followed by the defined `port` value in your desired Client Device's browser (for e.g. http://192.27.0.101:8000) to access the stream. diff --git a/docs/help/webgear_rtc_ex.md b/docs/help/webgear_rtc_ex.md new file mode 100644 index 000000000..894599957 --- /dev/null +++ b/docs/help/webgear_rtc_ex.md @@ -0,0 +1,213 @@ + + +# WebGear_RTC_RTC Examples + +  + +## Using WebGear_RTC with RaspberryPi Camera Module + +Because of WebGear_RTC API's flexible internal wapper around VideoGear, it can easily access any parameter of CamGear and PiGear videocapture APIs. + +!!! info "Following usage examples are just an idea of what can be done with WebGear_RTC API, you can try various [VideoGear](../../gears/videogear/params/), [CamGear](../../gears/camgear/params/) and [PiGear](../../gears/pigear/params/) parameters directly in WebGear_RTC API in the similar manner." + +Here's a bare-minimum example of using WebGear_RTC API with the Raspberry Pi camera module while tweaking its various properties in just one-liner: + +```python +# import libs +import uvicorn +from vidgear.gears.asyncio import WebGear_RTC + +# various webgear_rtc performance and Raspberry Pi camera tweaks +options = { + "frame_size_reduction": 25, + "hflip": True, + "exposure_mode": "auto", + "iso": 800, + "exposure_compensation": 15, + "awb_mode": "horizon", + "sensor_mode": 0, +} + +# initialize WebGear_RTC app +web = WebGear_RTC( + enablePiCamera=True, resolution=(640, 480), framerate=60, logging=True, **options +) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +  + +## Using WebGear_RTC with real-time Video Stabilization enabled + +Here's an example of using WebGear_RTC API with real-time Video Stabilization enabled: + +```python +# import libs +import uvicorn +from vidgear.gears.asyncio import WebGear_RTC + +# various webgear_rtc performance tweaks +options = { + "frame_size_reduction": 25, +} + +# initialize WebGear_RTC app with a raw source and enable video stabilization(`stabilize=True`) +web = WebGear_RTC(source="foo.mp4", stabilize=True, logging=True, **options) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +  + +## Display Two Sources Simultaneously in WebGear_RTC + +In this example, we'll be displaying two video feeds side-by-side simultaneously on browser using WebGear_RTC API by simply concatenating frames in real-time: + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +```python +# import necessary libs +import uvicorn, asyncio, cv2 +import numpy as np +from av import VideoFrame +from aiortc import VideoStreamTrack +from vidgear.gears.asyncio import WebGear_RTC +from vidgear.gears.asyncio.helper import reducer + +# initialize WebGear_RTC app without any source +web = WebGear_RTC(logging=True) + +# frame concatenator +def get_conc_frame(frame1, frame2): + h1, w1 = frame1.shape[:2] + h2, w2 = frame2.shape[:2] + + # create empty matrix + vis = np.zeros((max(h1, h2), w1 + w2, 3), np.uint8) + + # combine 2 frames + vis[:h1, :w1, :3] = frame1 + vis[:h2, w1 : w1 + w2, :3] = frame2 + + return vis + + +# create your own Bare-Minimum Custom Media Server +class Custom_RTCServer(VideoStreamTrack): + """ + Custom Media Server using OpenCV, an inherit-class + to aiortc's VideoStreamTrack. + """ + + def __init__(self, source1=None, source2=None): + + # don't forget this line! + super().__init__() + + # check is source are provided + if source1 is None or source2 is None: + raise ValueError("Provide both source") + + # initialize global params + # define both source here + self.stream1 = cv2.VideoCapture(source1) + self.stream2 = cv2.VideoCapture(source2) + + async def recv(self): + """ + A coroutine function that yields `av.frame.Frame`. + """ + # don't forget this function!!! + + # get next timestamp + pts, time_base = await self.next_timestamp() + + # read video frame + (grabbed1, frame1) = self.stream1.read() + (grabbed2, frame2) = self.stream2.read() + + # if NoneType + if not grabbed1 or not grabbed2: + return None + else: + print("Got frames") + + print(frame1.shape) + print(frame2.shape) + + # concatenate frame + frame = get_conc_frame(frame1, frame2) + + print(frame.shape) + + # reducer frames size if you want more performance otherwise comment this line + # frame = await reducer(frame, percentage=30) # reduce frame by 30% + + # contruct `av.frame.Frame` from `numpy.nd.array` + av_frame = VideoFrame.from_ndarray(frame, format="bgr24") + av_frame.pts = pts + av_frame.time_base = time_base + + # return `av.frame.Frame` + return av_frame + + def terminate(self): + """ + Gracefully terminates VideoGear stream + """ + # don't forget this function!!! + + # terminate + if not (self.stream1 is None): + self.stream1.release() + self.stream1 = None + + if not (self.stream2 is None): + self.stream2.release() + self.stream2 = None + + +# assign your custom media server to config with both adequate sources (for e.g. foo1.mp4 and foo2.mp4) +web.config["server"] = Custom_RTCServer( + source1="dance_videos/foo1.mp4", source2="dance_videos/foo2.mp4" +) + +# run this app on Uvicorn server at address http://localhost:8000/ +uvicorn.run(web(), host="localhost", port=8000) + +# close app safely +web.shutdown() +``` + +!!! success "On successfully running this code, the output stream will be displayed at address http://localhost:8000/ in Browser." + + +  \ No newline at end of file diff --git a/docs/help/writegear_ex.md b/docs/help/writegear_ex.md new file mode 100644 index 000000000..c505a55cb --- /dev/null +++ b/docs/help/writegear_ex.md @@ -0,0 +1,306 @@ + + + +# WriteGear Examples + +  + +## Using WriteGear's Compression Mode for YouTube-Live Streaming + +!!! new "New in v0.2.1" + This example was added in `v0.2.1`. + +!!! alert "This example assume you already have a [**YouTube Account with Live-Streaming enabled**](https://support.google.com/youtube/answer/2474026#enable) for publishing video." + +!!! danger "Make sure to change [_YouTube-Live Stream Key_](https://support.google.com/youtube/answer/2907883#zippy=%2Cstart-live-streaming-now) with yours in following code before running!" + +```python +# import required libraries +from vidgear.gears import CamGear +from vidgear.gears import WriteGear +import cv2 + +# define video source +VIDEO_SOURCE = "/home/foo/foo.mp4" + +# Open stream +stream = CamGear(source=VIDEO_SOURCE, logging=True).start() + +# define required FFmpeg optimizing parameters for your writer +# [NOTE]: Added VIDEO_SOURCE as audio-source, since YouTube rejects audioless streams! +output_params = { + "-i": VIDEO_SOURCE, + "-acodec": "aac", + "-ar": 44100, + "-b:a": 712000, + "-vcodec": "libx264", + "-preset": "medium", + "-b:v": "4500k", + "-bufsize": "512k", + "-pix_fmt": "yuv420p", + "-f": "flv", +} + +# [WARNING] Change your YouTube-Live Stream Key here: +YOUTUBE_STREAM_KEY = "xxxx-xxxx-xxxx-xxxx-xxxx" + +# Define writer with defined parameters and +writer = WriteGear( + output_filename="rtmp://a.rtmp.youtube.com/live2/{}".format(YOUTUBE_STREAM_KEY), + logging=True, + **output_params +) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # write frame to writer + writer.write(frame) + +# safely close video stream +stream.stop() + +# safely close writer +writer.close() +``` + +  + + +## Using WriteGear's Compression Mode creating MP4 segments from a video stream + +!!! new "New in v0.2.1" + This example was added in `v0.2.1`. + +```python +# import required libraries +from vidgear.gears import VideoGear +from vidgear.gears import WriteGear +import cv2 + +# Open any video source `foo.mp4` +stream = VideoGear( + source="foo.mp4", logging=True +).start() + +# define required FFmpeg optimizing parameters for your writer +output_params = { + "-c:v": "libx264", + "-crf": 22, + "-map": 0, + "-segment_time": 9, + "-g": 9, + "-sc_threshold": 0, + "-force_key_frames": "expr:gte(t,n_forced*9)", + "-clones": ["-f", "segment"], +} + +# Define writer with defined parameters +writer = WriteGear(output_filename="output%03d.mp4", logging=True, **output_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # write frame to writer + writer.write(frame) + + # 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 writer +writer.close() +``` + +  + + +## Using WriteGear's Compression Mode to add external audio file input to video frames + +!!! new "New in v0.2.1" + This example was added in `v0.2.1`. + +!!! failure "Make sure this `-i` audio-source it compatible with provided video-source, otherwise you encounter multiple errors or no output at all." + +```python +# import required libraries +from vidgear.gears import CamGear +from vidgear.gears import WriteGear +import cv2 + +# open any valid video stream(for e.g `foo_video.mp4` file) +stream = CamGear(source="foo_video.mp4").start() + +# add various parameters, along with custom audio +stream_params = { + "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-i": "foo_audio.aac", # assigns input audio-source: "foo_audio.aac" +} + +# Define writer with defined parameters +writer = WriteGear(output_filename="Output.mp4", logging=True, **stream_params) + +# loop over +while True: + + # read frames from stream + frame = stream.read() + + # check for frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # write frame to writer + writer.write(frame) + + # 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 writer +writer.close() +``` + +  + + +## Using WriteGear with ROS(Robot Operating System) + +We will be using [`cv_bridge`](http://wiki.ros.org/cv_bridge/Tutorials/ConvertingBetweenROSImagesAndOpenCVImagesPython) to convert OpenCV frames to ROS image messages and vice-versa. + +In this example, we'll create a node that listens to a ROS image message topic, converts the recieved images messages into OpenCV frames, draws a circle on it, and then process these frames into a lossless compressed file format in real-time. + +!!! new "New in v0.2.2" + This example was added in `v0.2.2`. + +!!! note "This example is vidgear implementation of this [wiki example](http://wiki.ros.org/cv_bridge/Tutorials/ConvertingBetweenROSImagesAndOpenCVImagesPython)." + +```python +# import roslib +import roslib + +roslib.load_manifest("my_package") + +# import other required libraries +import sys +import rospy +import cv2 +from std_msgs.msg import String +from sensor_msgs.msg import Image +from cv_bridge import CvBridge, CvBridgeError +from vidgear.gears import WriteGear + +# custom publisher class +class image_subscriber: + def __init__(self, output_filename="Output.mp4"): + # create CV bridge + self.bridge = CvBridge() + # define publisher topic + self.image_pub = rospy.Subscriber("image_topic_sub", Image, self.callback) + # Define writer with default parameters + self.writer = WriteGear(output_filename=output_filename) + + def callback(self, data): + # convert recieved data to frame + try: + cv_image = self.bridge.imgmsg_to_cv2(data, "bgr8") + except CvBridgeError as e: + print(e) + + # check if frame is valid + if cv_image: + + # {do something with the frame here} + + # add circle + (rows, cols, channels) = cv_image.shape + if cols > 60 and rows > 60: + cv2.circle(cv_image, (50, 50), 10, 255) + + # write frame to writer + writer.write(frame) + + def close(self): + # safely close video stream + self.writer.close() + + +def main(args): + # define publisher with suitable output filename + # such as `Output.mp4` for saving output + ic = image_subscriber(output_filename="Output.mp4") + # initiate ROS node on publisher + rospy.init_node("image_subscriber", anonymous=True) + try: + # run node + rospy.spin() + except KeyboardInterrupt: + print("Shutting down") + finally: + # close publisher + ic.close() + + +if __name__ == "__main__": + main(sys.argv) +``` + +  \ No newline at end of file diff --git a/docs/help/writegear_faqs.md b/docs/help/writegear_faqs.md index 53fe2950c..bb2764b2c 100644 --- a/docs/help/writegear_faqs.md +++ b/docs/help/writegear_faqs.md @@ -39,10 +39,8 @@ limitations under the License. **Answer:** WriteGear will exit with `ValueError` if you feed frames of different dimensions or channels. -   - ## How to install and configure FFmpeg correctly for WriteGear on my machine? **Answer:** Follow these [Installation Instructions ➶](../../gears/writegear/compression/advanced/ffmpeg_install/) for its installation. @@ -109,205 +107,21 @@ limitations under the License. ## Is YouTube-Live Streaming possibe with WriteGear? -**Answer:** Yes, See example below: - -!!! new "New in v0.2.1" - This example was added in `v0.2.1`. - -!!! alert "This example assume you already have a [**YouTube Account with Live-Streaming enabled**](https://support.google.com/youtube/answer/2474026#enable) for publishing video." - -!!! danger "Make sure to change [_YouTube-Live Stream Key_](https://support.google.com/youtube/answer/2907883#zippy=%2Cstart-live-streaming-now) with yours in following code before running!" - -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import WriteGear -import cv2 - -# define video source -VIDEO_SOURCE = "/home/foo/foo.mp4" - -# Open stream -stream = CamGear(source=VIDEO_SOURCE, logging=True).start() - -# define required FFmpeg optimizing parameters for your writer -# [NOTE]: Added VIDEO_SOURCE as audio-source, since YouTube rejects audioless streams! -output_params = { - "-i": VIDEO_SOURCE, - "-acodec": "aac", - "-ar": 44100, - "-b:a": 712000, - "-vcodec": "libx264", - "-preset": "medium", - "-b:v": "4500k", - "-bufsize": "512k", - "-pix_fmt": "yuv420p", - "-f": "flv", -} - -# [WARNING] Change your YouTube-Live Stream Key here: -YOUTUBE_STREAM_KEY = "xxxx-xxxx-xxxx-xxxx-xxxx" - -# Define writer with defined parameters and -writer = WriteGear( - output_filename="rtmp://a.rtmp.youtube.com/live2/{}".format(YOUTUBE_STREAM_KEY), - logging=True, - **output_params -) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # write frame to writer - writer.write(frame) - -# safely close video stream -stream.stop() - -# safely close writer -writer.close() -``` +**Answer:** Yes, See [this bonus example ➶](../writegear_ex/#using-writegears-compression-mode-for-youtube-live-streaming).   ## How to create MP4 segments from a video stream with WriteGear? -**Answer:** See example below: - -!!! new "New in v0.2.1" - This example was added in `v0.2.1`. - -```python -# import required libraries -from vidgear.gears import VideoGear -from vidgear.gears import WriteGear -import cv2 - -# Open any video source `foo.mp4` -stream = VideoGear( - source="foo.mp4", logging=True -).start() - -# define required FFmpeg optimizing parameters for your writer -output_params = { - "-c:v": "libx264", - "-crf": 22, - "-map": 0, - "-segment_time": 9, - "-g": 9, - "-sc_threshold": 0, - "-force_key_frames": "expr:gte(t,n_forced*9)", - "-clones": ["-f", "segment"], -} - -# Define writer with defined parameters -writer = WriteGear(output_filename="output%03d.mp4", logging=True, **output_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # write frame to writer - writer.write(frame) - - # 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 writer -writer.close() -``` +**Answer:** See [this bonus example ➶](../writegear_ex/#using-writegears-compression-mode-creating-mp4-segments-from-a-video-stream).   ## How add external audio file input to video frames? -**Answer:** See example below: - -!!! new "New in v0.2.1" - This example was added in `v0.2.1`. - -!!! failure "Make sure this `-i` audio-source it compatible with provided video-source, otherwise you encounter multiple errors or no output at all." - -```python -# import required libraries -from vidgear.gears import CamGear -from vidgear.gears import WriteGear -import cv2 - -# open any valid video stream(for e.g `foo_video.mp4` file) -stream = CamGear(source="foo_video.mp4").start() - -# add various parameters, along with custom audio -stream_params = { - "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - "-i": "foo_audio.aac", # assigns input audio-source: "foo_audio.aac" -} - -# Define writer with defined parameters -writer = WriteGear(output_filename="Output.mp4", logging=True, **stream_params) - -# loop over -while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # write frame to writer - writer.write(frame) - - # 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 writer -writer.close() -``` +**Answer:** See [this bonus example ➶](../writegear_ex/#using-writegears-compression-mode-to-add-external-audio-file-input-to-video-frames).   diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 411c08edd..32f12528d 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -35,176 +35,213 @@ limitations under the License. --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' 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='M7,2V4H8V18A4,4 0 0,0 12,22A4,4 0 0,0 16,18V4H17V2H7M11,16C10.4,16 10,15.6 10,15C10,14.4 10.4,14 11,14C11.6,14 12,14.4 12,15C12,15.6 11.6,16 11,16M13,12C12.4,12 12,11.6 12,11C12,10.4 12.4,10 13,10C13.6,10 14,10.4 14,11C14,11.6 13.6,12 13,12M14,7H10V4H14V7Z' /%3E%3C/svg%3E"); } + .md-typeset .admonition.advance, .md-typeset details.advance { - border-color: rgb(27,77,62); + border-color: rgb(27, 77, 62); } + .md-typeset .admonition.new, .md-typeset details.new { - border-color: rgb(43, 155, 70); + border-color: rgb(57,255,20); +} + +.md-typeset .admonition.alert, +.md-typeset details.alert { + border-color: rgb(255, 0, 255); } + .md-typeset .new > .admonition-title, .md-typeset .new > summary { - background-color: rgba(43, 155, 70, 0.1); - border-color: rgb(43, 155, 70); + background-color: rgb(57,255,20,0.1); + border-color: rgb(57,255,20); } + .md-typeset .new > .admonition-title::before, .md-typeset .new > summary::before { - background-color: rgb(228,24,30); + background-color: rgb(57,255,20); -webkit-mask-image: var(--md-admonition-icon--new); mask-image: var(--md-admonition-icon--new); } -.md-typeset .admonition.alert, -.md-typeset details.alert { - border-color: rgb(255, 0, 255); -} + .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 { background-color: rgb(255, 0, 255); -webkit-mask-image: var(--md-admonition-icon--alert); mask-image: var(--md-admonition-icon--alert); } -.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 { + +.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); +} + +.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); } -.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 { + +.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; } -.md-typeset .info>.admonition-title::before, -.md-typeset .info>summary::before, -.md-typeset .todo>.admonition-title::before, -.md-typeset .todo>summary::before { + +.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); } -.md-typeset .danger>.admonition-title::before, -.md-typeset .danger>summary::before, -.md-typeset .error>.admonition-title::before, -.md-typeset .error>summary::before { + +.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); } -.md-typeset .note>.admonition-title::before, -.md-typeset .note>summary::before { + +.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); } -.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 { + +.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); } -.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 { + +.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); } -.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 { + +.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); } -.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 { + +.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); } -.md-typeset .bug>.admonition-title::before, -.md-typeset .bug>summary::before { + +.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); } -.md-typeset .example>.admonition-title::before, -.md-typeset .example>summary::before { + +.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); } -.md-typeset .cite>.admonition-title::before, -.md-typeset .cite>summary::before, -.md-typeset .quote>.admonition-title::before, -.md-typeset .quote>summary::before { + +.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); } -.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); -} .md-nav__item--active > .md-nav__link { font-weight: bold; } + .center { display: block; margin-left: auto; margin-right: auto; width: 80%; } + .center-small { display: block; margin-left: auto; margin-right: auto; width: 90%; } + .md-tabs__link--active { font-weight: bold; } + .md-nav__title { font-size: 1rem !important; } + .md-version__link { overflow: hidden; } + .md-version__current { text-transform: uppercase; font-weight: bolder; } + .md-typeset .task-list-control .task-list-indicator::before { - background-color: #FF0000; - -webkit-mask-image: var(--md-admonition-icon--failure); - mask-image: var(--md-admonition-icon--failure); + background-color: #ff0000; + -webkit-mask-image: var(--md-admonition-icon--failure); + mask-image: var(--md-admonition-icon--failure); } + blockquote { padding: 0.5em 10px; quotes: "\201C""\201D""\2018""\2019"; } + blockquote:before { color: #ccc; content: open-quote; @@ -213,10 +250,12 @@ blockquote:before { margin-right: 0.25em; vertical-align: -0.4em; } + blockquote:after { visibility: hidden; content: close-quote; } + blockquote p { display: inline; } @@ -229,6 +268,7 @@ blockquote p { display: flex; justify-content: center; } + .embed-responsive { position: relative; display: block; @@ -236,10 +276,12 @@ blockquote p { padding: 0; overflow: hidden; } + .embed-responsive::before { display: block; content: ""; } + .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, @@ -253,15 +295,19 @@ blockquote p { height: 100%; border: 0; } + .embed-responsive-21by9::before { padding-top: 42.857143%; } + .embed-responsive-16by9::before { padding-top: 56.25%; } + .embed-responsive-4by3::before { padding-top: 75%; } + .embed-responsive-1by1::before { padding-top: 100%; } @@ -270,6 +316,7 @@ blockquote p { footer.sponsorship { text-align: center; } + footer.sponsorship hr { display: inline-block; width: 2rem; @@ -277,15 +324,19 @@ footer.sponsorship hr { 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:not(:hover) .twemoji.heart-throb-hover svg { color: var(--md-default-fg-color--lighter) !important; } + .doc-heading { padding-top: 50px; } + .btn { z-index: 1; overflow: hidden; @@ -300,10 +351,12 @@ footer.sponsorship:not(:hover) .twemoji.heart-throb-hover svg { font-weight: bold; margin: 5px 0px; } + .btn.bcolor { border: 4px solid var(--md-typeset-a-color); color: var(--blue); } + .btn.bcolor:before { content: ""; position: absolute; @@ -315,53 +368,68 @@ footer.sponsorship:not(:hover) .twemoji.heart-throb-hover svg { z-index: -1; transition: 0.2s ease; } + .btn.bcolor:hover { color: var(--white); background: var(--md-typeset-a-color); transition: 0.2s ease; } + .btn.bcolor:hover:before { width: 100%; } + main #g6219 { transform-origin: 85px 4px; - animation: an1 12s .5s infinite ease-out; + animation: an1 12s 0.5s infinite ease-out; } + @keyframes an1 { 0% { transform: rotate(0); } + 5% { transform: rotate(3deg); } + 15% { transform: rotate(-2.5deg); } + 25% { transform: rotate(2deg); } + 35% { transform: rotate(-1.5deg); } + 45% { transform: rotate(1deg); } + 55% { transform: rotate(-1.5deg); } + 65% { transform: rotate(2deg); } + 75% { transform: rotate(-2deg); } + 85% { transform: rotate(2.5deg); } + 95% { transform: rotate(-3deg); } + 100% { transform: rotate(0); } -} +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 61d467c2a..b819890b4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -299,3 +299,15 @@ nav: - WebGear_RTC FAQs: help/webgear_rtc_faqs.md - NetGear_Async FAQs: help/netgear_async_faqs.md - Stabilizer Class FAQs: help/stabilizer_faqs.md + - Bonus Examples: + - CamGear Examples: help/camgear_ex.md + - PiGear Examples: help/pigear_ex.md + - VideoGear Examples: help/videogear_ex.md + - ScreenGear Examples: help/screengear_ex.md + - WriteGear Examples: help/writegear_ex.md + - StreamGear Examples: help/streamgear_ex.md + - NetGear Examples: help/netgear_ex.md + - WebGear Examples: help/webgear_ex.md + - WebGear_RTC Examples: help/webgear_rtc_ex.md + - NetGear_Async Examples: help/netgear_async_ex.md + - Stabilizer Class Examples: help/stabilizer_ex.md \ No newline at end of file From 2ea4e89ce0b5faec54d49d97eea90de1af25c0b4 Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 1 Sep 2021 11:43:21 +0530 Subject: [PATCH 108/112] =?UTF-8?q?=F0=9F=90=9B=20WebGearRTC:=20Fixed=20As?= =?UTF-8?q?sertion=20error=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🚑️ Source must raise MediaStreamError when stream ends instead of returning None-type. - 👷 Updated CI tests. - 📝 Updated Docs Examples. --- docs/gears/webgear_rtc/advanced.md | 4 ++-- vidgear/gears/asyncio/webgear_rtc.py | 4 ++-- .../asyncio_tests/test_webgear_rtc.py | 21 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/gears/webgear_rtc/advanced.md b/docs/gears/webgear_rtc/advanced.md index 1f4646d7a..2726cdc64 100644 --- a/docs/gears/webgear_rtc/advanced.md +++ b/docs/gears/webgear_rtc/advanced.md @@ -77,6 +77,7 @@ Let's implement a bare-minimum example with a Custom Source using WebGear_RTC AP import uvicorn, asyncio, cv2 from av import VideoFrame from aiortc import VideoStreamTrack +from aiortc.mediastreams import MediaStreamError from vidgear.gears.asyncio import WebGear_RTC from vidgear.gears.asyncio.helper import reducer @@ -112,7 +113,7 @@ class Custom_RTCServer(VideoStreamTrack): # if NoneType if not grabbed: - return None + return MediaStreamError # reducer frames size if you want more performance otherwise comment this line frame = await reducer(frame, percentage=30) # reduce frame by 30% @@ -145,7 +146,6 @@ uvicorn.run(web(), host="localhost", port=8000) # close app safely web.shutdown() - ``` **And that's all, Now you can see output at [`http://localhost:8000/`](http://localhost:8000/) address.** diff --git a/vidgear/gears/asyncio/webgear_rtc.py b/vidgear/gears/asyncio/webgear_rtc.py index 3ae39b931..86cfc2a37 100644 --- a/vidgear/gears/asyncio/webgear_rtc.py +++ b/vidgear/gears/asyncio/webgear_rtc.py @@ -223,14 +223,14 @@ async def recv(self): # read video frame f_stream = None if self.__stream is None: - return None + raise MediaStreamError else: f_stream = self.__stream.read() # display blank if NoneType if f_stream is None: if self.blank_frame is None or not self.is_running: - return None + raise MediaStreamError else: f_stream = self.blank_frame[:] if not self.__enable_inf and not self.__reset_enabled: 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 cec35673f..a150c20ff 100644 --- a/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py +++ b/vidgear/tests/streamer_tests/asyncio_tests/test_webgear_rtc.py @@ -43,6 +43,7 @@ RTCSessionDescription, ) from av import VideoFrame +from aiortc.mediastreams import MediaStreamError from vidgear.gears.asyncio import WebGear_RTC from vidgear.gears.helper import logger_handler @@ -142,7 +143,7 @@ async def recv(self): # if NoneType if not grabbed: - return None + raise MediaStreamError # contruct `av.frame.Frame` from `numpy.nd.array` av_frame = VideoFrame.from_ndarray(frame, format="bgr24") @@ -187,7 +188,7 @@ async def recv(self): # if NoneType if not grabbed: - return None + raise MediaStreamError # contruct `av.frame.Frame` from `numpy.nd.array` av_frame = VideoFrame.from_ndarray(frame, format="bgr24") @@ -252,7 +253,8 @@ async def test_webgear_rtc_class(source, stabilize, colorspace, time_delay): await offer_pc.close() web.shutdown() except Exception as e: - pytest.fail(str(e)) + if not isinstance(e, MediaStreamError): + pytest.fail(str(e)) test_data = [ @@ -314,7 +316,7 @@ async def test_webgear_rtc_options(options): await offer_pc.close() web.shutdown() except Exception as e: - if isinstance(e, AssertionError): + if isinstance(e, (AssertionError, MediaStreamError)): logger.exception(str(e)) elif isinstance(e, requests.exceptions.Timeout): logger.exceptions(str(e)) @@ -396,7 +398,7 @@ async def test_webpage_reload(options): # shutdown await offer_pc.close() except Exception as e: - if "enable_live_broadcast" in options and isinstance(e, AssertionError): + if "enable_live_broadcast" in options and isinstance(e, (AssertionError, MediaStreamError)): pytest.xfail("Test Passed") else: pytest.fail(str(e)) @@ -414,7 +416,7 @@ async def test_webpage_reload(options): @pytest.mark.asyncio -@pytest.mark.xfail(raises=ValueError) +@pytest.mark.xfail(raises=(ValueError, MediaStreamError)) @pytest.mark.parametrize("server, result", test_data_class) async def test_webgear_rtc_custom_server_generator(server, result): """ @@ -448,7 +450,7 @@ async def test_webgear_rtc_custom_middleware(middleware, result): assert response.status_code == 200 web.shutdown() except Exception as e: - if result: + if result and not isinstance(e, MediaStreamError): pytest.fail(str(e)) @@ -493,7 +495,8 @@ async def test_webgear_rtc_routes(): await offer_pc.close() web.shutdown() except Exception as e: - pytest.fail(str(e)) + if not isinstance(e, MediaStreamError): + pytest.fail(str(e)) @pytest.mark.asyncio @@ -515,7 +518,7 @@ async def test_webgear_rtc_routes_validity(): async with TestClient(web()) as client: pass except Exception as e: - if isinstance(e, RuntimeError): + if isinstance(e, (RuntimeError, MediaStreamError)): pytest.xfail(str(e)) else: pytest.fail(str(e)) From f57a889be1fecbaae837522ecfed3de48b66b9ff Mon Sep 17 00:00:00 2001 From: abhiTronix Date: Wed, 1 Sep 2021 12:36:01 +0530 Subject: [PATCH 109/112] =?UTF-8?q?=F0=9F=9A=B8=20Docs:=20Added=20Gitter?= =?UTF-8?q?=20sidecard=20embed=20widget.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🍱 Imported gitter-sidecar script to `main.html`. - 💄 Updated `custom.js` to set global window option. - 💄 Updated Sidecard UI in `custom.css`. - ⚰️ Removed dead code from docs. - ✏️ Fixed more typos. --- docs/bonus/reference/helper.md | 4 ++++ docs/bonus/reference/helper_async.md | 8 -------- docs/help/camgear_faqs.md | 10 +++------- docs/help/pigear_faqs.md | 2 +- docs/overrides/assets/javascripts/extra.js | 11 ++++++++++- docs/overrides/assets/stylesheets/custom.css | 7 +++++++ docs/overrides/main.html | 1 + vidgear/gears/__init__.py | 4 ++-- vidgear/gears/helper.py | 2 +- 9 files changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/bonus/reference/helper.md b/docs/bonus/reference/helper.md index 20c94b625..2214e37f6 100644 --- a/docs/bonus/reference/helper.md +++ b/docs/bonus/reference/helper.md @@ -98,6 +98,10 @@ limitations under the License.   +::: vidgear.gears.helper.import_dependency_safe + +  + ::: vidgear.gears.helper.get_video_bitrate   diff --git a/docs/bonus/reference/helper_async.md b/docs/bonus/reference/helper_async.md index cfc329656..8e3e56b87 100644 --- a/docs/bonus/reference/helper_async.md +++ b/docs/bonus/reference/helper_async.md @@ -18,14 +18,6 @@ limitations under the License. =============================================== --> -::: vidgear.gears.asyncio.helper.logger_handler - -  - -::: vidgear.gears.asyncio.helper.mkdir_safe - -  - ::: vidgear.gears.asyncio.helper.reducer   diff --git a/docs/help/camgear_faqs.md b/docs/help/camgear_faqs.md index a2b394105..4aea0d91b 100644 --- a/docs/help/camgear_faqs.md +++ b/docs/help/camgear_faqs.md @@ -72,9 +72,7 @@ limitations under the License. ## How to change quality and parameters of YouTube Streams with CamGear? -CamGear provides exclusive attributes `STREAM_RESOLUTION` _(for specifying stream resolution)_ & `STREAM_PARAMS` _(for specifying underlying API(e.g. `youtube-dl`) parameters)_ with its [`options`](../../gears/camgear/params/#options) dictionary parameter. The complete usage example is as follows: - -**Answer:** See [this bonus example ➶](../camgear_ex/#using-variable-youtube-dl-parameters-in-camgear). +**Answer:** CamGear provides exclusive attributes `STREAM_RESOLUTION` _(for specifying stream resolution)_ & `STREAM_PARAMS` _(for specifying underlying API(e.g. `youtube-dl`) parameters)_ with its [`options`](../../gears/camgear/params/#options) dictionary parameter. See [this bonus example ➶](../camgear_ex/#using-variable-youtube-dl-parameters-in-camgear).   @@ -82,9 +80,7 @@ CamGear provides exclusive attributes `STREAM_RESOLUTION` _(for specifying strea ## How to open RSTP network streams with CamGear? -You can open any local network stream _(such as RTSP)_ just by providing its URL directly to CamGear's [`source`](../../gears/camgear/params/#source) parameter. The complete usage example is as follows: - -**Answer:** See [this bonus example ➶](../camgear_ex/#using-camgear-for-capturing-rstprtmp-urls). +**Answer:** You can open any local network stream _(such as RTSP)_ just by providing its URL directly to CamGear's [`source`](../../gears/camgear/params/#source) parameter. See [this bonus example ➶](../camgear_ex/#using-camgear-for-capturing-rstprtmp-urls).   @@ -102,7 +98,7 @@ You can open any local network stream _(such as RTSP)_ just by providing its URL ## How to synchronize between two cameras? -**Answer:** See [this issue comment ➶](https://github.com/abhiTronix/vidgear/issues/1#issuecomment-473943037). +**Answer:** See [this bonus example ➶](../camgear_ex/#synchronizing-two-sources-in-camgear).   diff --git a/docs/help/pigear_faqs.md b/docs/help/pigear_faqs.md index 30661a518..3c24814da 100644 --- a/docs/help/pigear_faqs.md +++ b/docs/help/pigear_faqs.md @@ -67,6 +67,6 @@ limitations under the License. ## How to change `picamera` settings for Camera Module at runtime? -**Answer:** You can use `stream` global parameter in PiGear to feed any `picamera` setting at runtime. See [this bonus example ➶](../pigear_ex/#setting-variable-picamera-parameters-for-camera-module-at-runtime). +**Answer:** You can use `stream` global parameter in PiGear to feed any `picamera` setting at runtime. See [this bonus example ➶](../pigear_ex/#setting-variable-picamera-parameters-for-camera-module-at-runtime)   \ No newline at end of file diff --git a/docs/overrides/assets/javascripts/extra.js b/docs/overrides/assets/javascripts/extra.js index 65c96542c..a09882de3 100755 --- a/docs/overrides/assets/javascripts/extra.js +++ b/docs/overrides/assets/javascripts/extra.js @@ -17,6 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. =============================================== */ + +// DASH StreamGear demo var player_dash = new Clappr.Player({ source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/dca65250d95eeeb87d594686c2f2c2208a015486/streamgear_video_segments/DASH/streamgear_dash.mpd', plugins: [DashShakaPlayback, LevelSelector], @@ -46,6 +48,7 @@ var player_dash = new Clappr.Player({ preload: 'metadata', }); +// HLS StremGear demo var player_hls = new Clappr.Player({ source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/abc0c193ab26e21f97fa30c9267de6beb8a72295/streamgear_video_segments/HLS/streamgear_hls.m3u8', plugins: [HlsjsPlayback, LevelSelector], @@ -81,6 +84,7 @@ var player_hls = new Clappr.Player({ preload: 'metadata', }); +// DASH Stabilizer demo var player_stab = new Clappr.Player({ source: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/fbcf0377b171b777db5e0b3b939138df35a90676/stabilizer_video_chunks/stabilizer_dash.mpd', plugins: [DashShakaPlayback], @@ -97,4 +101,9 @@ var player_stab = new Clappr.Player({ parentId: '#player_stab', poster: 'https://rawcdn.githack.com/abhiTronix/vidgear-docs-additionals/94bf767c28bf2fe61b9c327625af8e22745f9fdf/stabilizer_video_chunks/hd_thumbnail_2.png', preload: 'metadata', -}); \ No newline at end of file +}); + +// gitter sidecard +((window.gitter = {}).chat = {}).options = { + room: 'vidgear/community' +}; \ No newline at end of file diff --git a/docs/overrides/assets/stylesheets/custom.css b/docs/overrides/assets/stylesheets/custom.css index 32f12528d..a04895b69 100755 --- a/docs/overrides/assets/stylesheets/custom.css +++ b/docs/overrides/assets/stylesheets/custom.css @@ -207,6 +207,13 @@ limitations under the License. width: 80%; } +/* Handles Gitter Sidecard UI */ +.gitter-open-chat-button { + background-color: var(--md-primary-fg-color) !important; + font-family: inherit !important; + font-size: 12px; +} + .center-small { display: block; margin-left: auto; diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 969e3c710..1c8d80468 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -27,6 +27,7 @@ + {% endblock %}