diff --git a/README.md b/README.md index ac9429546..a0aa327d2 100644 --- a/README.md +++ b/README.md @@ -427,21 +427,21 @@ In addition to this, WriteGear also provides flexible access to [**OpenCV's Vide NetGear API

-> _StreamGear automates transcoding workflow for generating Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH and Apple HLS) in just few lines of python code._ +> _StreamGear streamlines and simplifies the transcoding workflow to generate Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats like MPEG-DASH and Apple HLS with just a few lines of Python code, allowing developers to focus on their application logic rather than dealing with the complexities of transcoding and chunking media files._ -StreamGear provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**][ffmpeg] multimedia framework for generating chunked-encoded media segments of the content. +StreamGear API provides a standalone, highly extensible, and flexible wrapper around the [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunk-encoded media segments from your multimedia content effortlessly. -SteamGear is an out-of-the-box solution for transcoding source videos/audio files & real-time video frames and breaking them into a sequence of multiple smaller chunks/segments of suitable lengths. These segments make it possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. +With StreamGear, you can transcode source video/audio files and real-time video frames into a sequence of multiple smaller chunks/segments of suitable lengths. These segments facilitate streaming at different quality levels _(bitrates or spatial resolutions)_ and allow for seamless switching between quality levels during playback based on available bandwidth. You can serve these segments on a web server, making them easily accessible via standard **HTTP GET** requests. -SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. But, Multiple DRM support is yet to be implemented. +SteamGear currently supports both [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Master Playlist _(such as M3U8 in-case of Apple HLS)_ besides segments that describe these segment information _(timing, URL, media characteristics like video resolution and bit rates)_ and is provided to the client before the streaming session. +Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrates_. They are provided to the client before the streaming session begins. **StreamGear primarily works in two Independent Modes for transcoding which serves different purposes:** -- **Single-Source Mode:** In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. **_Learn more about this mode [here ➶][ss-mode-doc]_** +- **Single-Source Mode :cd: :** In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. **_Learn more about this mode [here ➶][ss-mode-doc]_** -- **Real-time Frames Mode:** In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. **_Learn more about this mode [here ➶][rtf-mode-doc]_** +- **Real-time Frames Mode :film_frames: :** In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. **_Learn more about this mode [here ➶][rtf-mode-doc]_** ### StreamGear API Guide: diff --git a/docs/gears/netgear/advanced/secure_mode.md b/docs/gears/netgear/advanced/secure_mode.md index 2f1dddaed..51615363b 100644 --- a/docs/gears/netgear/advanced/secure_mode.md +++ b/docs/gears/netgear/advanced/secure_mode.md @@ -33,7 +33,6 @@ Secure Mode uses a new wire protocol, [**ZMTP 3.0**](http://zmtp.org/) that adds Secure Mode can be easily activated in NetGear API through `secure_mode` attribute of its [`options`](../../params/#options) dictionary parameter, during initialization. Furthermore, for managing this mode, NetGear API provides additional `custom_cert_location` & `overwrite_cert` like attribute too. -   ## Supported ZMQ Security Layers @@ -47,11 +46,12 @@ Secure mode supports the two most powerful ZMQ security layers:   - !!! danger "Important Information regarding Secure Mode" * The `secure_mode` attribute value at the Client's end **MUST** match exactly the Server's end _(i.e. **IronHouse** security layer is only compatible with **IronHouse**, and **NOT** with **StoneHouse**)_. + * In Secure Mode, The Client's end **MUST** run before the Server's end to establish a secure connection. + * The Public+Secret Keypairs generated at the Server end **MUST** be made available at the Client's end too for successful authentication. If mismatched, connection failure will occur. * By Default, the Public+Secret Keypairs will be generated/stored at the `$HOME/.vidgear/keys` directory of your machine _(e.g. `/home/foo/.vidgear/keys` on Linux)_. But you can also use [`custom_cert_location`](../../params/#options) attribute to set your own Custom-Path for a directory to generate/store these Keypairs. @@ -60,8 +60,11 @@ Secure mode supports the two most powerful ZMQ security layers: * **IronHouse** is the strongest Security Layer available, but it involves certain security checks that lead to **ADDITIONAL LATENCY**. + * Secure Mode only supports `libzmq` library version `>= 4.0`. + +   @@ -125,10 +128,56 @@ For implementing Secure Mode, NetGear API currently provide following exclusive Following is the bare-minimum code you need to get started with Secure Mode in NetGear API: -#### Server's End +!!! alert "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" + +#### Client's End Open your favorite terminal and execute the following python code: +!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" + +```python linenums="1" hl_lines="6" +# import required libraries +from vidgear.gears import NetGear +import cv2 + +# activate StoneHouse security mechanism +options = {"secure_mode": 1} + +# define NetGear Client with `receive_mode = True` and defined parameter +client = NetGear(pattern=1, receive_mode=True, logging=True, **options) + +# loop over +while True: + + # receive frames from network + frame = client.recv() + + # check for received frame if Nonetype + if frame is None: + break + + # {do something with the frame here} + + # Show output window + cv2.imshow("Output Frame", frame) + + # 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 + +Then open another terminal on the same system and execute the following python code to send the frames to our client: + !!! tip "You can terminate both sides anytime by pressing ++ctrl+"C"++ on your keyboard!" ```python linenums="1" hl_lines="9" @@ -171,49 +220,7 @@ stream.stop() server.close() ``` -#### Client's End - -Then open another terminal on the same system and execute the following python code and see the output: - -!!! tip "You can terminate client anytime by pressing ++ctrl+"C"++ on your keyboard!" - -```python linenums="1" hl_lines="6" -# import required libraries -from vidgear.gears import NetGear -import cv2 - -# activate StoneHouse security mechanism -options = {"secure_mode": 1} - -# define NetGear Client with `receive_mode = True` and defined parameter -client = NetGear(pattern=1, receive_mode=True, logging=True, **options) - -# loop over -while True: - # receive frames from network - frame = client.recv() - - # check for received frame if Nonetype - if frame is None: - break - - # {do something with the frame here} - - # Show output window - cv2.imshow("Output Frame", frame) - - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - -# close output window -cv2.destroyAllWindows() - -# safely close client -client.close() -```   @@ -222,11 +229,12 @@ client.close() ### Using Secure 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: +!!! alert "In Secure Mode, Client's end MUST run before the Server's end to establish a secure connection!" + !!! info "Note down the local IP-address of this system(required at Server's end) and also replace it in the following code. You can follow [this FAQ](../../../../help/netgear_faqs/#how-to-find-local-ip-address-on-different-os-platforms) for this purpose." !!! danger "You need to paste the Public+Secret Keypairs _(generated at the Server End)_ at the `$HOME/.vidgear/keys` directory of your Client machine for a successful authentication!" diff --git a/docs/gears/streamgear/introduction.md b/docs/gears/streamgear/introduction.md index 8a12c3b12..9a27fc9c1 100644 --- a/docs/gears/streamgear/introduction.md +++ b/docs/gears/streamgear/introduction.md @@ -30,15 +30,15 @@ limitations under the License. ## Overview -> StreamGear automates transcoding workflow for generating _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats (such as MPEG-DASH and Apple HLS)_ in just few lines of python code. +> StreamGear streamlines and simplifies the transcoding workflow to generate _Ultra-Low Latency, High-Quality, Dynamic & Adaptive Streaming Formats like MPEG-DASH and Apple HLS_ with just a few lines of Python code, allowing developers to focus on their application logic rather than dealing with the complexities of transcoding and chunking media files. -StreamGear provides a standalone, highly extensible, and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunked-encoded media segments of the content. +StreamGear API provides a standalone, highly extensible, and flexible wrapper around the [**FFmpeg**](https://ffmpeg.org/) multimedia framework for generating chunk-encoded media segments from your multimedia content effortlessly. -SteamGear is an out-of-the-box solution for transcoding source videos/audio files & real-time video frames and breaking them into a sequence of multiple smaller chunks/segments of suitable lengths. These segments make it possible to stream videos at different quality levels _(different bitrates or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another – if bandwidth permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. +With StreamGear, you can transcode source video/audio files and real-time video frames into a sequence of multiple smaller chunks/segments of suitable lengths. These segments facilitate streaming at different quality levels _(bitrates or spatial resolutions)_ and allow for seamless switching between quality levels during playback based on available bandwidth. You can serve these segments on a web server, making them easily accessible via standard **HTTP GET** requests. -SteamGear currently supports [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. +SteamGear currently supports both [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/) _(Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1)_ and [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming) _(HTTP Live Streaming)_. -SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Master Playlist _(such as M3U8 in-case of Apple HLS)_ besides segments that describe these segment information _(timing, URL, media characteristics like video resolution and adaptive bit rates)_ and is provided to the client before the streaming session. +Additionally, StreamGear generates a manifest file _(such as MPD for DASH)_ or a master playlist _(such as M3U8 for Apple HLS)_ alongside the segments. These files contain essential segment information, _including timing, URLs, and media characteristics like video resolution and adaptive bitrate_. They are provided to the client before the streaming session begins. !!! alert "For streaming with older traditional protocols such as RTMP, RTSP/RTP you could use [WriteGear](../../writegear/introduction/) API instead." @@ -59,9 +59,9 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast !!! tip "Useful Links" - - Checkout [this detailed blogpost](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works. - - Checkout [this detailed blogpost](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) on how HLS works. - - Checkout [this detailed blogpost](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) for HLS vs. MPEG-DASH comparison. + - Checkout [this detailed blogpost ➶](https://ottverse.com/mpeg-dash-video-streaming-the-complete-guide/) on how MPEG-DASH works. + - Checkout [this detailed blogpost ➶](https://ottverse.com/hls-http-live-streaming-how-does-it-work/) on how HLS works. + - Checkout [this detailed blogpost ➶](https://imagekit.io/blog/hls-vs-dash/) for HLS vs. MPEG-DASH comparison.   @@ -71,14 +71,12 @@ SteamGear also creates a Manifest file _(such as MPD in-case of DASH)_ or a Mast StreamGear primarily operates in following independent modes for transcoding: -??? warning "Real-time Frames Mode is NOT Live-Streaming." +???+ alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout [this usage example ➶](../rtfm/usage/#bare-minimum-usage-with-live-streaming) for more information. - Rather, you can enable live-streaming in Real-time Frames Mode by using the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter in StreamGear API. Checkout [this usage example](../rtfm/usage/#bare-minimum-usage-with-live-streaming) for more information. +- [**Single-Source Mode :material-file-video-outline:**](../ssm/overview) : In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. - -- [**Single-Source Mode**](../ssm/overview): In this mode, StreamGear **transcodes entire video file** _(as opposed to frame-by-frame)_ into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you're transcoding long-duration lossless videos(with audio) for streaming that required no interruptions. But on the downside, the provided source cannot be flexibly manipulated or transformed before sending onto FFmpeg Pipeline for processing. - -- [**Real-time Frames Mode**](../rtfm/overview): In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams. +- [**Real-time Frames Mode :material-camera-burst:**](../rtfm/overview) : In this mode, StreamGear directly **transcodes frame-by-frame** _(as opposed to a entire video file)_, into a sequence of multiple smaller chunks/segments for streaming. This mode works exceptionally well when you desire to flexibility manipulate or transform [`numpy.ndarray`](https://numpy.org/doc/1.18/reference/generated/numpy.ndarray.html#numpy-ndarray) frames in real-time before sending them onto FFmpeg Pipeline for processing. But on the downside, audio has to added manually _(as separate source)_ for streams.   diff --git a/docs/gears/streamgear/params.md b/docs/gears/streamgear/params.md index 14575db35..3512b074d 100644 --- a/docs/gears/streamgear/params.md +++ b/docs/gears/streamgear/params.md @@ -24,15 +24,13 @@ limitations under the License. ## **`output`** -This parameter sets the valid filename/path for storing the StreamGear assets _(Manifest file (such as MPD in-case of DASH) or a Master Playlist (such as M3U8 in-case of Apple HLS) & Transcoded sequence of segments)_. +This parameter sets the valid filename/path for storing the StreamGear assets, including Manifest file _(such as MPD in case of DASH)_ or a Master Playlist _(such as M3U8 in case of Apple HLS)_ and generated sequence of chunks/segments. -!!! warning "StreamGear API will throw `ValueError` if `output` provided is empty or invalid." +!!! warning "StreamGear API will throw `ValueError` if the provided `output` is empty or invalid." -!!! failure "Make sure to provide _valid filename with valid file-extension_ for selected [`format`](#format) value _(such as `.mpd` in case of MPEG-DASH and `.m3u8` in case of APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." +!!! failure "Make sure to provide a valid filename with a valid file extension for the selected `format` value _(such as `.mpd` for MPEG-DASH and `.m3u8` for APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." -!!! note "StreamGear generated sequence of multiple chunks/segments are also stored in the same directory." - -!!! tip "You can easily delete all previous assets at `output` location, by using [`-clear_prev_assets`](#a-exclusive-parameters) attribute of [`stream_params`](#stream_params) dictionary parameter." +!!! tip "You can easily delete all previous assets at the `output` location by using the [`-clear_prev_assets`](#a-exclusive-parameters) attribute of the [`stream_params`](#stream_params) dictionary parameter." **Data-Type:** String @@ -40,60 +38,63 @@ This parameter sets the valid filename/path for storing the StreamGear assets _( Its valid input can be one of the following: -* **Path to directory**: Valid path of the directory. In this case, StreamGear API will automatically assign a unique filename for Manifest file. This can be defined as follows: +* **Path to directory**: Valid path of the directory. In this case, StreamGear API will automatically assign a unique filename for the Manifest file. This can be defined as follows: === "DASH" ```python - streamer = StreamGear(output = "/home/foo/foo1") # Define streamer with manifest saving directory path + # Define streamer with output directory path for saving DASH assets + streamer = StreamGear(output = "/home/foo/bar") ``` === "HLS" ```python - streamer = StreamGear(output = "/home/foo/foo1", format="hls") # Define streamer with playlist saving directory path + # Define streamer with output directory path for saving HLS assets + streamer = StreamGear(output = "/home/foo/bar", format="hls") ``` -* **Filename** _(with/without path)_: Valid filename(_with valid extension_) of the output Manifest file. In case filename is provided without path, then current working directory will be used. +* **Filename** _(with/without path)_: Valid filename _(with a valid extension)_ of the output Manifest or Playlist file. If the filename is provided without a path, the current working directory will be used. This can be defined as follows: === "DASH" ```python - streamer = StreamGear(output = "output_foo.mpd") # Define streamer with manifest file name + # Define streamer with output manifest filename + streamer = StreamGear(output = "output_dash.mpd") ``` === "HLS" ```python - streamer = StreamGear(output = "output_foo.m3u8", format="hls") # Define streamer with playlist file name + # Define streamer with output playlist filename + streamer = StreamGear(output = "output_hls.m3u8", format="hls") ``` -* **URL**: Valid URL of a network stream with a protocol supported by installed FFmpeg _(verify with command `ffmpeg -protocols`)_ only. This is useful for directly storing assets to a network server. For example, you can use a `http` protocol URL as follows: - +* **URL**: Valid URL of a network stream with a protocol supported by the installed FFmpeg _(verify with the `ffmpeg -protocols` command)_. This is useful for directly storing assets to a network server. For example, you can use an `HTTP` protocol URL as follows: === "DASH" ```python - streamer = StreamGear(output = "http://195.167.1.101/live/test.mpd") #Define streamer + # Define streamer with output manifest URL + streamer = StreamGear(output = "http://some_dummy_serverip/live/output_dash.mpd") ``` === "HLS" ```python - streamer = StreamGear(output = "http://195.167.1.101/live/test.m3u8", format="hls") #Define streamer + # Define streamer with output playlist URL + streamer = StreamGear(output = "http://some_dummy_serverip/live/output_hls.m3u8", format="hls") ```   ## **`format`** +This parameter enables the adaptive HTTP streaming format. This parameter currently supported these formats: `dash` _(i.e [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/))_ and `hls` _(i.e [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming))_. -This parameter select the adaptive HTTP streaming formats. For now, the supported format are: `dash` _(i.e [**MPEG-DASH**](https://www.encoding.com/mpeg-dash/))_ and `hls` _(i.e [**Apple HLS**](https://developer.apple.com/documentation/http_live_streaming))_. - -!!! warning "Any invalid value to `format` parameter will result in ValueError!" - -!!! failure "Make sure to provide _valid filename with valid file-extension_ in [`output`](#output) for selected `format` value _(such as `.mpd` in case of MPEG-DASH and `.m3u8` in case of APPLE-HLS)_, otherwise StreamGear will throw `AssertionError`." +!!! danger "Make sure to provide a valid filename with a valid file extension in the [`output`](#output) parameter for the selected `format` value _(i.e., `.mpd` for MPEG-DASH and `.m3u8` for APPLE-HLS)_, otherwise StreamGear will throw an `AssertionError`." +!!! warning "Any improper value assigned to `format` parameter will result in a `ValueError`!" **Data-Type:** String @@ -104,16 +105,17 @@ This parameter select the adaptive HTTP streaming formats. For now, the supporte === "DASH" ```python - StreamGear(output = "output_foo.mpd", format="dash") + # Define streamer with DASH format + StreamGear(output = "output_dash.mpd", format="dash") ``` === "HLS" ```python - StreamGear(output = "output_foo.m3u8", format="hls") + # Define streamer with HLS format + StreamGear(output = "output_hls.m3u8", format="hls") ``` -   @@ -121,9 +123,10 @@ This parameter select the adaptive HTTP streaming formats. For now, the supporte This parameter assigns the custom _path/directory_ where the custom/downloaded FFmpeg executables are located. -!!! info "Behavior on Windows" +!!! info "Behavior on :fontawesome-brands-windows: Windows Systems" + + On Windows, if a custom FFmpeg executable's path/directory is not provided through this `custom_ffmpeg` parameter, the StreamGear API will automatically attempt to download and extract suitable Static FFmpeg binaries at a suitable location on your Windows machine. More information can be found [here ➶](../ffmpeg_install/#a-auto-installation). - If a custom FFmpeg executable's path | directory is not provided through `custom_ffmpeg` parameter on Windows machine, then StreamGear API will ==automatically attempt to download and extract suitable Static FFmpeg binaries at suitable location on your windows machine==. More information can be found [here ➶](../ffmpeg_install/#a-auto-installation). **Data-Type:** String @@ -132,8 +135,8 @@ This parameter assigns the custom _path/directory_ where the custom/downloaded F **Usage:** ```python -# If ffmpeg executables are located at "/foo/foo1/ffmpeg" -StreamGear(output = 'output_foo.mpd', custom_ffmpeg="/foo/foo1/ffmpeg") +# Define streamer with custom ffmpeg binary +StreamGear(output = 'output_foo.mpd', custom_ffmpeg="C://foo//bar//ffmpeg.exe") ```   @@ -141,11 +144,9 @@ StreamGear(output = 'output_foo.mpd', custom_ffmpeg="/foo/foo1/ffmpeg") ## **`stream_params`** -This parameter allows us to exploit almost all FFmpeg supported parameters effortlessly and flexibly change its internal settings for transcoding and seamlessly generating high-quality streams. All [supported parameters](#supported-parameters) can formatting as attributes for this dictionary parameter: - - -!!! danger "Kindly read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully, before passing any additional values to `stream_params` parameter. Wrong values may result in undesired errors or no output at all." +This parameter allows developers to leverage nearly all FFmpeg options, providing effortless and flexible control over its internal settings for transcoding and generating high-quality streams. All [supported parameters](#supported-parameters) can be formatted as attributes within this dictionary parameter. +!!! danger "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." **Data-Type:** Dictionary @@ -158,183 +159,219 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor StreamGear API provides some exclusive internal parameters to easily generate Streaming Assets and effortlessly tweak its internal properties. These parameters are discussed below: -* **`-streams`** _(list of dicts)_: This important attribute makes it simple and pretty straight-forward to define additional multiple streams as _list of dictionaries_ of different quality levels _(i.e. different bitrates or spatial resolutions)_ for streaming. +* **`-streams`** _(list of dicts)_: This important attribute makes it simple and pretty straight-forward to define additional multiple streams as _list of dictionaries_ of different quality levels _(i.e. different bitrate or spatial resolutions)_ for streaming. + + ???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" - !!! danger "Important `-streams` attribute facts" - * ==On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate[^1] as the input Video, at the index `0`.== - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically using `-bpps` and `-resolution` values. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. + * In addition to the user-defined Secondary Streams, ==StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution as the input frames and at default framerate[^1], at the index `0`.== + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. - **To construct the additional stream dictionaries, you'll will need following sub-attributes:** + **To construct the additional stream dictionaries, you will need the following sub-attributes::** - * `-resolution` _(string)_: It is **compulsory** to define the required resolution/dimension/size for the stream, otherwise given stream will be rejected. Its value can be a `"{width}x{height}"` as follows: + * `-resolution` _(string)_: It is **compulsory** to define the required resolution/dimension/size for the stream, otherwise, the given stream will be rejected. Its value should be in the format `"{width}x{height}"`, as shown below: ```python - "-streams" = [{"-resolution": "1280x720"}] # to produce a 1280x720 resolution/scale + # produce a 1280x720 resolution/scale stream + "-streams" = [{"-resolution": "1280x720"}] ``` - * `-video_bitrate` _(string)_: It is an **optional** _(can be ignored if `-framerate` parameter is defined)_ sub-attribute that generally determines the bandwidth and quality of stream, i.e. the higher the bitrate, the better the quality and the larger will be bandwidth and more will be strain on network. It value is generally in `kbps` _(kilobits per second)_ for OBS (Open Broadcasting Softwares). You can easily define this attribute as follows: + * `-video_bitrate` _(string)_: This is an **optional** sub-attribute _(can be ignored if the `-framerate` parameter is defined)_ that generally determines the bandwidth and quality of the stream. The higher the bitrate, the better the quality and the larger the bandwidth, which can place more strain on the network. Its value is typically in `k` _(kilobits per second)_ or `M` _(Megabits per second)_. Define this attribute as follows: ```python - "-streams" : [{"-resolution": "1280x720", "-video_bitrate": "2000k"}] # to produce a 1280x720 resolution and 2000kbps bitrate stream + # produce a 1280x720 resolution and 2000 kbps bitrate stream + "-streams" : [{"-resolution": "1280x720", "-video_bitrate": "2000k"}] ``` - * `-framerate` _(float/int)_: It is another **optional** _(can be ignored if `-video_bitrate` parameter is defined)_ sub-attribute that defines the assumed framerate for the stream. It's value can be float/integer as follows: + * `-framerate` _(float/int)_: This is another **optional** sub-attribute _(can be ignored if the `-video_bitrate` parameter is defined)_ that defines the assumed framerate for the stream. Its value can be a float or integer, as shown below: ```python - "-streams" : [{"-resolution": "1280x720", "-framerate": "60.0"}] # to produce a 1280x720 resolution and 60fps framerate stream + # produce a 1280x720 resolution and 60fps framerate stream + "-streams" : [{"-resolution": "1280x720", "-framerate": "60.0"}] ``` **Usage:** You can easily define any number of streams using `-streams` attribute as follows: - !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-additional-streams)" - ```python stream_params = {"-streams": - [{"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": "30.0"}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": "60.0"}, # Stream3: 640x360 at 60fps - ]} + [ + {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate + {"-resolution": "1280x720", "-framerate": 30}, # Stream2: 1280x720 at 30fps + {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + ] + } ``` + !!! example "Its usage example can be found [here ➶](../ssm/usage/#usage-with-additional-streams)" +   -* **`-video_source`** _(string)_: This attribute takes valid Video path as input and activates [**Single-Source Mode**](../ssm/overview), for transcoding it into multiple smaller chunks/segments for streaming after successful validation. Its value be one of the following: +* **`-video_source`** _(string)_: This attribute takes a valid video path as input and activates [**Single-Source Mode**](../ssm/overview), for transcoding it into multiple smaller chunks/segments for streaming after successful validation. Its value can be one of the following: - !!! tip "Usage example can be found [here ➶](../ssm/usage/#bare-minimum-usage)" + * **Video Filename**: Valid path to a video file as follows: - * **Video Filename**: Valid path to Video file as follows: ```python - stream_params = {"-video_source": "/home/foo/foo1.mp4"} # set input video source: /home/foo/foo1.mp4 + # set video source as `/home/foo/bar.mp4` + stream_params = {"-video_source": "/home/foo/bar.mp4"} ``` * **Video URL**: Valid URL of a network video stream as follows: - !!! danger "Make sure given Video URL has protocol that is supported by installed FFmpeg. _(verify with `ffmpeg -protocols` terminal command)_" + !!! danger "Ensure the given video URL uses a protocol supported by the installed FFmpeg _(verify with `ffmpeg -protocols` terminal command)_." ```python - stream_params = {"-video_source": "http://livefeed.com:5050"} # set input video source: http://livefeed.com:5050 + # set video source as `http://livefeed.com:5050` + stream_params = {"-video_source": "http://livefeed.com:5050"} ``` + !!! example "Its usage example can be found [here ➶](../ssm/usage/#bare-minimum-usage)" +   +* **`-audio`** _(string/list)_: This attribute takes an external custom audio path _(as a string)_ or an audio device name followed by a suitable demuxer _(as a list)_ as the audio source input for all StreamGear streams. Its value can be one of the following: -* **`-audio`** _(string/list)_: This attribute takes external custom audio path _(as `string`)_ or audio device name followed by suitable demuxer _(as `list`)_ as audio source input for all StreamGear streams. Its value be one of the following: + !!! failure "Ensure the provided `-audio` audio source is compatible with the input video source. Incompatibility can cause multiple errors or result in no output at all." - !!! failure "Make sure this audio-source is compatible with provided video -source, otherwise you could encounter multiple errors, or even no output at all!" + * **Audio Filename** _(string)_: Valid path to an audio file as follows: - * **Audio Filename** _(string)_: Valid path to Audio file as follows: ```python - stream_params = {"-audio": "/home/foo/foo1.aac"} # set input audio source: /home/foo/foo1.aac + # set audio source as `/home/foo/foo1.aac` + stream_params = {"-audio": "/home/foo/foo1.aac"} ``` - !!! tip "Usage example can be found [here ➶](../ssm/usage/#usage-with-custom-audio)" + + !!! example "Its usage examples can be found [here ➶](../ssm/usage/#usage-with-custom-audio) and [here ➶](../ssm/usage/#usage-with-file-audio-input)" * **Audio URL** _(string)_: Valid URL of a network audio stream as follows: - !!! danger "Make sure given Video URL has protocol that is supported by installed FFmpeg. _(verify with `ffmpeg -protocols` terminal command)_" + !!! danger "Ensure the given audio URL uses a protocol supported by the installed FFmpeg _(verify with `ffmpeg -protocols` terminal command)_." ```python - stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 + # set input audio source as `https://exampleaudio.org/example-160.mp3` + stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} ``` - * **Device name and Demuxer** _(list)_: Valid audio device name followed by suitable demuxer as follows: + * **Device name and Demuxer** _(list)_: Valid audio device name followed by a suitable demuxer as follows: ```python - stream_params = {"-audio": "https://exampleaudio.org/example-160.mp3"} # set input audio source: https://exampleaudio.org/example-160.mp3 + # Assign appropriate input audio-source device (compatible with video source) and its demuxer + stream_params = {"-audio": [ + "-f", + "dshow", + "-i", + "audio=Microphone (USB2.0 Camera)", + ]} ``` - !!! tip "Usage example can be found [here ➶](../rtfm/usage/#usage-with-device-audio--input)" - + !!! example "Its usage example can be found [here ➶](../rtfm/usage/#usage-with-device-audio--input)"   -* **`-livestream`** _(bool)_: ***(optional)*** specifies whether to enable **Livestream Support**_(chunks will contain information for new frames only)_ for the selected mode, or not. You can easily set it to `True` to enable this feature, and default value is `False`. It can be used as follows: +* **`-livestream`** _(bool)_: ***(optional)*** specifies whether to enable **Low-latency Live-Streaming :material-video-wireless-outline:** in Real-time Frames Mode only, where chunks will contain information for new frames only and forget previous ones, or not. The default value is `False`. It can be used as follows: - !!! tip "Use `window_size` & `extra_window_size` FFmpeg parameters for controlling number of frames to be kept in New Chunks." + !!! warning "The `-livestream` optional parameter is **NOT** supported in [Single-Source mode](../ssm/overview)." ```python - stream_params = {"-livestream": True} # enable livestreaming + stream_params = {"-livestream": True} # enable live-streaming ``` -  + !!! example "Its usage example can be found [here ➶](../rtfm/usage/#bare-minimum-usage-with-live-streaming)" -* **`-input_framerate`** _(float/int)_ : ***(optional)*** specifies the assumed input video source framerate, and only works in [Real-time Frames Mode](../usage/#b-real-time-frames-mode). It can be used as follows: +  - !!! tip "Usage example can be found [here ➶](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)" +* **`-input_framerate`** _(float/int)_ : ***(optional)*** This parameter specifies the assumed input video source framerate and only works in [Real-time Frames Mode](../usage/#b-real-time-frames-mode). Its default value is `25.0` fps. Its value can be a float or integer, as shown below: ```python - stream_params = {"-input_framerate": 60.0} # set input video source framerate to 60fps + # set input video source framerate to 60fps + stream_params = {"-input_framerate": 60.0} ``` + !!! example "Its usage example can be found [here ➶](../rtfm/usage/#bare-minimum-usage-with-controlled-input-framerate)" +   -* **`-bpp`** _(float/int)_: ***(optional)*** This attribute controls constant _Bits-Per-Pixel_(BPP) value, which is kind of a constant value to ensure good quality of high motion scenes ,and thereby used in calculating desired video-bitrate for streams. Higher the BPP, better will be motion quality. Its default value is `0.1`. Going over `0.1`helps to fill gaps between current bitrate and upload limit/ingest cap. Its value can be anything above `0.001`, can be used as follows: +* **`-bpp`** _(float/int)_: ***(optional)*** This attribute controls the constant **BPP** _(Bits-Per-Pixel)_ value, which helps ensure good quality in high motion scenes by determining the desired video bitrate for streams. A higher BPP value improves motion quality. The default value is `0.1`. Increasing the BPP value helps fill the gaps between the current bitrate and the upload limit/ingest cap. Its value can be anything above `0.001` and can be used as follows: - !!! tip "Important BPP tips for streaming" - * `-bpp` a sensitive value, try 0.001, and then make increments in 0.0001 to fine tune - * If your desired resolution/fps/audio combination is below maximum service bitrate, raise BPP to match it for extra quality. - * It is generally better to lower resolution (and/or fps) and raise BPP than raise resolution and loose on BPP. + !!! tip "Important points while tweaking BPP" + * BPP is a sensitive value; start with `0.001` and make small increments (`0.0001`) to fine-tune. + * If your desired resolution/fps/audio combination is below the maximum service bitrate, raise BPP to match it for extra quality. + * It is generally better to lower resolution _(and/or `fps`)_ and raise BPP than to raise resolution and lose BPP. ```python - stream_params = {"-bpp": 0.05} # sets BPP to 0.05 + # sets BPP to 0.05 + stream_params = {"-bpp": 0.05} ```   -* **`-gop`** _(float/int)_ : ***(optional)*** specifies the number of frames between two I-frames for accurate GOP length. By increasing the length of the GOP, there will be fewer I-frames per time frame, which minimizes bandwidth consumption. So, for example, with extremely complex subjects such as water sports or action mode, you’ll want to use a shorter GOP length such as 15 or below that results in excellent video quality. For more static video such as talking heads, then much longer GOP sizes are not only sufficient but also more efficient. It can be used as follows: +* **`-gop`** _(float/int)_ : ***(optional)*** This parameter specifies the number of frames between two I-frames for accurate **GOP** _(Group of Pictures)_ length. Increasing the GOP length reduces the number of I-frames per time frame, minimizing bandwidth consumption. For example, with complex subjects such as water sports or action scenes, a shorter GOP length _(e.g., `15` or below)_ results in excellent video quality. For more static video, such as talking heads, much longer GOP sizes are not only sufficient but also more efficient. It can be used as follows: - !!! tip "The larger the GOP size, the more efficient the compression and the less bandwidth you will need" + !!! tip "The larger the GOP size, the more efficient the compression and the less bandwidth you will need." - !!! info "By default, StreamGear automatically sets recommended fixed GOP value _(i.e. every two seconds)_ w.r.t input framerate and selected encoder." + !!! info "By default, StreamGear automatically sets a recommended fixed GOP value _(i.e., every two seconds)_ based on the input framerate and selected encoder." ```python - stream_params = {"-gop": 70} # set GOP length to 70 + # set GOP length to 70 + stream_params = {"-gop": 70} ```   -* **`-clones`** _(list)_: ***(optional)*** sets the special FFmpeg parameters that are repeated more than once in the command _(For more info., see [this issue](https://github.com/abhiTronix/vidgear/issues/141))_ as **list** only. Usage is as follows: +* **`-clones`** _(list)_: ***(optional)*** This parameter sets special FFmpeg options that need to be repeated more than once in the command. For more information, see [this issue](https://github.com/abhiTronix/vidgear/issues/141). It accepts values as a **list** only. Usage is as follows: ```python + # sets special FFmpeg options repeated multiple times stream_params = {"-clones": ['-map', '0:v:0', '-map', '1:a?']} ```   -* **`-ffmpeg_download_path`** _(string)_: ***(optional)*** sets the custom directory for downloading FFmpeg Static Binaries in Compression Mode, during the [Auto-Installation](../ffmpeg_install/#a-auto-installation) on Windows Machines Only. If this parameter is not altered, then these binaries will auto-save to the default temporary directory (for e.g. `C:/User/temp`) on your windows machine. It can be used as follows: +* **`-ffmpeg_download_path`** _(string)_: ***(optional)*** This parameter sets a custom directory for downloading FFmpeg static binaries in Compression Mode during the [**Auto-Installation**](../ffmpeg_install/#a-auto-installation) step on Windows machines only. If this parameter is not altered, the binaries will be saved to the default temporary directory _(e.g., `C:/User/foo/temp`)_ on your Windows machine. It can be used as follows: ```python - stream_params = {"-ffmpeg_download_path": "C:/User/foo/foo1"} # will be saved to "C:/User/foo/foo1" + # download FFmpeg static binaries to `C:/User/foo/bar` + stream_params = {"-ffmpeg_download_path": "C:/User/foo/bar"} ```   -* **`-clear_prev_assets`** _(bool)_: ***(optional)*** specify whether to force-delete any previous copies of StreamGear Assets _(i.e. Manifest files(.mpd) & streaming chunks(.m4s) etc.)_ present at path specified by [`output`](#output) parameter. You can easily set it to `True` to enable this feature, and default value is `False`. It can be used as follows: +* **`-clear_prev_assets`** _(bool)_: ***(optional)*** This parameter specifies whether to force-delete any previous copies of StreamGear assets _(i.e., manifest (`mpd`), playlist (`mu38`), and streaming chunks (`.m4s`), etc. files)_ present at the path specified by the [`output`](#output) parameter. The default value is `False`. It can be used as follows: - !!! info "In Single-Source Mode, additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." + !!! info "Additional segments _(such as `.webm`, `.mp4` chunks)_ are also cleared automatically." ```python - stream_params = {"-clear_prev_assets": True} # will delete all previous assets + # delete all previous assets + stream_params = {"-clear_prev_assets": True} ```   -#### B. FFmpeg Parameters +* **`-enable_force_termination`** _(bool)_: sets a special flag to enable the forced termination of the FFmpeg process, required only if StreamGear is getting frozen when terminated. Its usage is as follows: + + !!! warning "The `-enable_force_termination` flag can potentially cause unexpected behavior or corrupted output in certain scenarios. It is recommended to use this flag with caution." -Almost all FFmpeg parameter can be passed as dictionary attributes in `stream_params`. For example, for using `libx264 encoder` to produce a lossless output video, we can pass required FFmpeg parameters as dictionary attributes, as follows: + ```python + # enables forced termination of FFmpeg process + stream_params = {"-enable_force_termination": True} + ``` + +  -!!! 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" +#### B. FFmpeg Parameters +Almost all FFmpeg parameters can be passed as dictionary attributes in `stream_params`. For example, to use the `libx264` encoder to produce a lossless output video, you can pass the required FFmpeg parameters as dictionary attributes as follows: -!!! failure "All ffmpeg parameters are case-sensitive. Remember to double check every parameter if any error occurs." +!!! tip "Please check the [H.264 documentation ➶](https://trac.ffmpeg.org/wiki/Encode/H.264) and [FFmpeg Documentation ➶](https://ffmpeg.org/documentation.html) for more information on following parameters." +!!! failure "All FFmpeg parameters are case-sensitive. Double-check each parameter if any errors occur." -!!! note "In addition to these parameters, almost any FFmpeg parameter _(supported by installed FFmpeg)_ is also supported. But make sure to read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully first." +!!! note "In addition to these parameters, almost any FFmpeg parameter _(supported by the installed FFmpeg)_ is also supported. Be sure to read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully first." ```python +# libx264 encoder and its supported parameters stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "zerolatency"} ``` @@ -342,9 +379,11 @@ stream_params = {"-vcodec":"libx264", "-crf": 0, "-preset": "fast", "-tune": "ze ### Supported Encoders and Decoders -All the encoders and decoders that are compiled with FFmpeg in use, are supported by WriteGear API. You can easily check the compiled encoders by running following command in your terminal: +All encoders and decoders compiled with the FFmpeg in use are supported by the StreamGear API. You can check the compiled encoders by running the following command in your terminal: + +!!! warning "Stream copy (`-vcodec copy`) is not compatible with Real-time Frames Mode as this mode requires re-encoding of incoming frames." -!!! info "Similarily, supported demuxers and filters depends upons compiled FFmpeg in use." +!!! info "Similarly, supported audio/video demuxers and filters depend on the FFmpeg binaries in use." ```sh # for checking encoder diff --git a/docs/gears/streamgear/rtfm/overview.md b/docs/gears/streamgear/rtfm/overview.md index 13e78154b..f213821cb 100644 --- a/docs/gears/streamgear/rtfm/overview.md +++ b/docs/gears/streamgear/rtfm/overview.md @@ -18,7 +18,7 @@ limitations under the License. =============================================== --> -# StreamGear API: Real-time Frames Mode +# StreamGear API: Real-time Frames Mode :material-camera-burst:
@@ -44,18 +44,15 @@ For this mode, StreamGear API provides exclusive [`stream()`](../../../../bonus/ Apple HLS support was added in `v0.2.2`. -!!! alert "Real-time Frames Mode is NOT Live-Streaming." +!!! alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout [this usage example ➶](../usage/#bare-minimum-usage-with-live-streaming) for more information. - Rather, you can easily enable live-streaming in Real-time Frames Mode by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. Checkout its [usage example here](../usage/#bare-minimum-usage-with-live-streaming). +!!! danger "Please Remember :material-police-badge-outline:" -!!! danger + * Using [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function instead of [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) in Real-time Frames Mode will immediately result in **`RuntimeError`**! - * Using [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) function instead of [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) in Real-time Frames Mode will instantly result in **`RuntimeError`**! - - * **NEVER** assign anything to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, otherwise [Single-Source Mode](../#a-single-source-mode) may get activated, and as a result, using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function will throw **`RuntimeError`**! - - * You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in this mode, otherwise audio delay will occur in output streams. + * **NEVER** assign anything to [`-video_source`](../../params/#a-exclusive-parameters) attribute of [`stream_params`](../../params/#supported-parameters) dictionary parameter, otherwise [Single-Source Mode](../#a-single-source-mode) get activated, and as a result, using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function will throw **`RuntimeError`**! * Input framerate defaults to `25.0` fps if [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value not defined. diff --git a/docs/gears/streamgear/rtfm/usage.md b/docs/gears/streamgear/rtfm/usage.md index be8a38e23..55e84ae5a 100644 --- a/docs/gears/streamgear/rtfm/usage.md +++ b/docs/gears/streamgear/rtfm/usage.md @@ -18,22 +18,25 @@ limitations under the License. =============================================== --> -# StreamGear API Usage Examples: Real-time Frames Mode +# StreamGear API Usage Examples: Real-time Frames Mode :material-camera-burst: -!!! alert "Real-time Frames Mode is NOT Live-Streaming." +!!! alert "Real-time Frames Mode itself is NOT Live-Streaming :material-video-wireless-outline:" - Rather you can easily enable live-streaming in Real-time Frames Mode by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. Checkout following [usage example](#bare-minimum-usage-with-live-streaming). + To enable live-streaming in Real-time Frames Mode, use the exclusive [`-livestream`](../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter in the StreamGear API. Checkout following [usage example ➶](#bare-minimum-usage-with-live-streaming) for more information. -!!! warning "Important Information" +!!! warning "Important Information :fontawesome-solid-person-military-pointing:" - * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../../ffmpeg_install/) for its installation. + - [x] StreamGear API **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../../ffmpeg_install/) for its installation. API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + - [x] In this mode, ==API by default generates a primary stream _(at the index `0`)_ of same resolution as the input frames and at default framerate[^1].== + - [x] In this mode, API **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. + - [x] In this mode, Stream copy (`-vcodec copy`) is not compatible as this mode requires re-encoding of incoming frames. + - [x] Always use `close()` function at the very end of the main code. - * StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - - * By default, ==StreamGear generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== - - * Always use `terminate()` function at the very end of the main code. +???+ danger "DEPRECATION NOTICES for `v0.3.3` and above" + + - [ ] The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. + - [ ] The `rgb_mode` parameter in [`stream()`](../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) method, which earlier used to support RGB frames in Real-time Frames Mode is now deprecated, and will be removed in a future version. Only BGR format frames will be supported going forward. Please update your code to handle BGR format frames. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" @@ -47,6 +50,7 @@ Following is the bare-minimum code you need to get started with StreamGear API i !!! note "We are using [CamGear](../../../camgear/overview/) in this Bare-Minimum example, but any [VideoCapture Gear](../../../#a-videocapture-gears) will work in the similar manner." +!!! danger "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." === "DASH" @@ -94,7 +98,7 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -143,41 +147,36 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` !!! success "After running this bare-minimum example, StreamGear will produce a Manifest file _(`dash.mpd`)_ with streamable chunks that contains information about a Primary Stream of same resolution and framerate[^1] as input _(without any audio)_." -   -## Bare-Minimum Usage with Live-Streaming - -You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, where chunks will contain information for few new frames only and forgets all previous ones), using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: +## Bare-Minimum Usage with controlled Input-framerate -!!! note "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." +> In Real-time Frames Mode, StreamGear API provides the exclusive [`-input_framerate`](../../params/#a-exclusive-parameters) attribute for the `stream_params` dictionary parameter, which allows you to set the assumed constant framerate for incoming frames. -=== "DASH" +In this example, we will retrieve the framerate from a webcam video stream and set it as the value for the `-input_framerate` attribute in StreamGear. - !!! tip "Chunk size in DASH" - Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks in DASH stream. Less these value, less will be latency. +!!! danger "Remember, the input framerate defaults to 25.0 fps if the `-input_framerate` attribute value is not defined in Real-time Frames mode." - !!! alert "After every few chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in manifest will contain NO information of any older ones, and therefore resultant DASH stream will play only the most recent frames." +=== "DASH" - ```python linenums="1" hl_lines="11" + ```python linenums="1" hl_lines="10" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(from web-camera attached at index `0`) + # Open live video stream on webcam at first index(i.e. 0) device stream = CamGear(source=0).start() - # enable livestreaming and retrieve framerate from CamGear Stream and - # pass it as `-input_framerate` parameter for controlled framerate - stream_params = {"-input_framerate": stream.framerate, "-livestream": True} + # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value + stream_params = {"-input_framerate":stream.framerate} - # describe a suitable manifest-file location/name + # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) # loop over @@ -210,31 +209,24 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - !!! tip "Chunk size in HLS" - - Use `-hls_init_time` & `-hls_time` FFmpeg parameters for controlling number of frames to be kept in Chunks in HLS stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in playlist will contain NO information of any older ones, and therefore resultant HLS stream will play only the most recent frames." - - ```python linenums="1" hl_lines="11" + ```python linenums="1" hl_lines="10" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(from web-camera attached at index `0`) + # Open live video stream on webcam at first index(i.e. 0) device stream = CamGear(source=0).start() - # enable livestreaming and retrieve framerate from CamGear Stream and - # pass it as `-input_framerate` parameter for controlled framerate - stream_params = {"-input_framerate": stream.framerate, "-livestream": True} + # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value + stream_params = {"-input_framerate":stream.framerate} - # describe a suitable manifest-file location/name + # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) # loop over @@ -267,145 +259,39 @@ You can easily activate ==Low-latency Livestreaming in Real-time Frames Mode==, stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` -   -## Bare-Minimum Usage with RGB Mode +## Bare-Minimum Usage with Live-Streaming -In Real-time Frames Mode, StreamGear API provide [`rgb_mode`](../../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) boolean parameter with its `stream()` function, which if enabled _(i.e. `rgb_mode=True`)_, specifies that incoming frames are of RGB format _(instead of default BGR format)_, thereby also known as ==RGB Mode==. +You can easily activate **Low-latency Live-Streaming :material-video-wireless-outline:** in Real-time Frames Mode, where chunks will contain information for new frames only and forget previous ones, using the exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. +The complete example is as follows: -The complete usage example is as follows: +!!! danger "In this mode, StreamGear **DOES NOT** automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." === "DASH" - ```python linenums="1" hl_lines="28" - # 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() - ``` + !!! tip "Controlling chunk size in DASH" + To control the number of frames kept in Chunks for the DASH stream _(controlling latency)_, you can use the `-window_size` and `-extra_window_size` FFmpeg parameters. Lower values for these parameters will result in lower latency. -=== "HLS" + !!! alert "After every few chunks _(equal to the sum of `-window_size` and `-extra_window_size` values)_, all chunks will be overwritten while Live-Streaming. This means that newer chunks in the manifest will contain NO information from older chunks, and the resulting DASH stream will only play the most recent frames, reducing latency." - ```python linenums="1" hl_lines="28" - # import required libraries - from vidgear.gears import CamGear - from vidgear.gears import StreamGear - import cv2 - - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source='foo1.mp4').start() - - # describe a suitable manifest-file location/name - streamer = StreamGear(output="hls_out.m3u8", format = "hls") - - # loop over - while True: - - # read frames from stream - frame = stream.read() - - # check for frame if Nonetype - if frame is None: - break - - - # {simulating RGB frame for this example} - frame_rgb = frame[:,:,::-1] - - - # send frame to streamer - streamer.stream(frame_rgb, rgb_mode = True) #activate RGB Mode - - # Show output window - cv2.imshow("Output Frame", frame) - - # check for 'q' key if pressed - key = cv2.waitKey(1) & 0xFF - if key == ord("q"): - break - - # close output window - cv2.destroyAllWindows() - - # safely close video stream - stream.stop() - - # safely close streamer - streamer.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." - - -=== "DASH" - - ```python linenums="1" hl_lines="10" + ```python linenums="1" hl_lines="11" # 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 + # open any valid video stream(from web-camera attached at index `0`) stream = CamGear(source=0).start() - # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value - stream_params = {"-input_framerate":stream.framerate} + # 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 and assign params + # describe a suitable manifest-file location/name streamer = StreamGear(output="dash_out.mpd", **stream_params) # loop over @@ -418,10 +304,8 @@ In this example, we will retrieve framerate from webcam video-stream, and set it if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -440,24 +324,30 @@ In this example, we will retrieve framerate from webcam video-stream, and set it stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="10" + !!! tip "Controlling chunk size in HLS" + To control the number of frames kept in Chunks for the HLS stream _(controlling latency)_, you can use the `-hls_init_time` & `-hls_time` FFmpeg parameters. Lower values for these parameters will result in lower latency. + + !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten while Live-Streaming. This means that newer chunks in the master playlist will contain NO information from older chunks, and the resulting HLS stream will only play the most recent frames, reducing latency." + + ```python linenums="1" hl_lines="11" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # Open live video stream on webcam at first index(i.e. 0) device + # open any valid video stream(from web-camera attached at index `0`) stream = CamGear(source=0).start() - # retrieve framerate from CamGear Stream and pass it as `-input_framerate` value - stream_params = {"-input_framerate":stream.framerate} + # 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 and assign params + # describe a suitable manifest-file location/name streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) # loop over @@ -470,10 +360,8 @@ In this example, we will retrieve framerate from webcam video-stream, and set it if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -492,18 +380,19 @@ In this example, we will retrieve framerate from webcam video-stream, and set it stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` +   ## Bare-Minimum Usage with OpenCV -You can easily use StreamGear API directly with any other Video Processing library(_For e.g. [OpenCV](https://github.com/opencv/opencv) itself_) in Real-time Frames Mode. +> You can easily use the StreamGear API directly with any other Video Processing library _(for e.g. [OpenCV](https://github.com/opencv/opencv))_ in Real-time Frames Mode. -The complete usage example is as follows: +The following is a complete StreamGear API usage example with OpenCV: -!!! tip "This just a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature/example will work in the similar manner." +!!! note "This is a bare-minimum example with OpenCV, but any other Real-time Frames Mode feature or example will work in a similar manner." === "DASH" @@ -532,7 +421,6 @@ The complete usage example is as follows: # lets convert frame to gray for this example gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # send frame to streamer streamer.stream(gray) @@ -551,7 +439,7 @@ The complete usage example is as follows: stream.release() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -581,7 +469,6 @@ The complete usage example is as follows: # lets convert frame to gray for this example gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - # send frame to streamer streamer.stream(gray) @@ -600,33 +487,35 @@ The complete usage example is as follows: stream.release() # safely close streamer - streamer.terminate() + streamer.close() ``` -   ## Usage with Additional Streams -Similar to Single-Source Mode, you can easily generate any number of additional Secondary Streams of variable bitrates or spatial resolutions, using exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to add each resolution and bitrate/framerate as list of dictionaries to this attribute, and rest is done automatically. +> Similar to Single-Source Mode, in addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrate or spatial resolution, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. + +To generate Secondary Streams, add each desired resolution and bitrate/framerate as a list of dictionaries to the `-streams` attribute. StreamGear will handle the rest automatically. The complete example is as follows: !!! info "A more detailed information on `-streams` attribute can be found [here ➶](../../params/#a-exclusive-parameters)" -The complete example is as follows: +!!! alert "In this mode, StreamGear DOES NOT automatically maps video-source audio to generated streams. You need to manually assign separate audio-source through [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." -??? danger "Important `-streams` attribute Information" - * On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate[^1] as the input, at the index `0`. - * :warning: Make sure your System/Machine/Server/Network is able to handle these additional streams, discretion is advised! - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. +???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" -!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + * In addition to the user-defined Secondary Streams, StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution as the input frames and at default framerate[^1]. + * :warning: Ensure that your system, machine, server, or network can handle the additional resource requirements of the Secondary Streams. Exercise discretion when configuring multiple streams. + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. +!!! failure "Always use the `-streams` attribute to define additional streams safely. Duplicate or incorrect definitions can break the transcoding pipeline and corrupt the output chunks." === "DASH" - ```python linenums="1" hl_lines="11-15" + ```python linenums="1" hl_lines="12-14" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -638,8 +527,8 @@ The complete example is as follows: # define various streams stream_params = { "-streams": [ - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "1280x720", "-framerate": 30.0}, # Stream1: 1280x720 at 30fps framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps framerate {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate ], } @@ -657,10 +546,8 @@ The complete example is as follows: if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -679,12 +566,12 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="11-15" + ```python linenums="1" hl_lines="12-14" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear @@ -696,8 +583,8 @@ The complete example is as follows: # define various streams stream_params = { "-streams": [ - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps framerate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps framerate + {"-resolution": "1280x720", "-framerate": 30.0}, # Stream1: 1280x720 at 30fps framerate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps framerate {"-resolution": "320x240", "-video_bitrate": "500k"}, # Stream3: 320x240 at 500kbs bitrate ], } @@ -715,10 +602,8 @@ The complete example is as follows: if frame is None: break - # {do something with the frame here} - # send frame to streamer streamer.stream(frame) @@ -737,23 +622,22 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with File Audio-Input -In Real-time Frames Mode, if you want to add audio to your streams, you've to use exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to input the path of your audio file to this attribute as `string` value, and the API will automatically validate as well as maps it to all generated streams. +> In Real-time Frames Mode, if you want to add audio to your streams, you need to use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -The complete example is as follows: +To add a audio source, provide the path to your audio file as a string to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: -!!! failure "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the input video source. Incompatibility can cause multiple errors or result in no output at all." !!! warning "You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in Real-time Frames mode, otherwise audio delay will occur in output streams." -!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../../params/#a-exclusive-parameters)" - +!!! tip "You can also assign a valid audio URL as input instead of a file path. More details can be found [here ➶](../../params/#a-exclusive-parameters)" === "DASH" @@ -774,7 +658,7 @@ The complete example is as follows: {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac" # assign external audio-source } # describe a suitable manifest-file location/name and assign params @@ -812,7 +696,7 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -834,7 +718,7 @@ The complete example is as follows: {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac" # assign external audio-source } # describe a suitable manifest-file location/name and assign params @@ -872,25 +756,26 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with Device Audio-Input -In Real-time Frames Mode, you've can also use exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter for streaming live audio from external device. You just need to format your audio device name followed by suitable demuxer as `list` and assign to this attribute, and the API will automatically validate as well as map it to all generated streams. +> In Real-time Frames Mode, you can also use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter for streaming live audio from an external device. -The complete example is as follows: +To stream live audio, format your audio device name followed by a suitable demuxer as a list, and assign it to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: +!!! alert "Example Assumptions :octicons-checklist-24:" -!!! alert "Example Assumptions" + - [x] You're running a Windows machine with all necessary audio drivers and software installed. + - [x] There's an audio device named "Microphone (USB2.0 Camera)" connected to your Windows machine. Check instructions below to use device sources with the `-audio` attribute on different OS platforms. - * You're running are Windows machine with all neccessary audio drivers and software installed. - * There's a audio device with named `"Microphone (USB2.0 Camera)"` connected to your windows machine. +??? info "Using devices sources with `-audio` attribute on different OS platforms" -??? tip "Using devices with `-audio` attribute on different OS platforms" + To use device sources with the `-audio` attribute on different OS platforms, follow these instructions: === ":fontawesome-brands-windows: Windows" @@ -1005,41 +890,40 @@ The complete example is as follows: !!! failure "If audio still doesn't work then reach us out on [Gitter ➶](https://gitter.im/vidgear/community) Community channel" +!!! tip "It is advised to use this example with live-streaming enabled(`True`) by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." -!!! danger "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the video source device. Incompatibility can cause multiple errors or result in no output at all." !!! warning "You **MUST** use [`-input_framerate`](../../params/#a-exclusive-parameters) attribute to set exact value of input framerate when using external audio in Real-time Frames mode, otherwise audio delay will occur in output streams." -!!! note "It is advised to use this example with live-streaming enabled(True) by using StreamGear API's exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter." - - === "DASH" - ```python linenums="1" hl_lines="18-24" + ```python linenums="1" hl_lines="18-25" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source="foo1.mp4").start() + # open any valid DEVICE video stream + stream = CamGear(source=0).start() # add various streams, along with custom audio stream_params = { "-streams": [ { - "-resolution": "1280x720", + "-resolution": "640x360", "-video_bitrate": "4000k", - }, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps + }, # Stream1: 640x360 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 30.0}, # Stream2: 320x240 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-livestream": True, "-audio": [ "-f", "dshow", "-i", "audio=Microphone (USB2.0 Camera)", - ], # assign appropriate input audio-source device and demuxer + ], # assign appropriate input audio-source device(compatible with video source) and its demuxer } # describe a suitable manifest-file location/name and assign params @@ -1075,40 +959,41 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="18-24" + ```python linenums="1" hl_lines="18-25" # import required libraries from vidgear.gears import CamGear from vidgear.gears import StreamGear import cv2 - # open any valid video stream(for e.g `foo1.mp4` file) - stream = CamGear(source="foo1.mp4").start() + # open any valid DEVICE video stream + stream = CamGear(source=0).start() # add various streams, along with custom audio stream_params = { "-streams": [ { - "-resolution": "1280x720", + "-resolution": "640x360", "-video_bitrate": "4000k", - }, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 30.0}, # Stream2: 640x360 at 30fps + }, # Stream1: 640x360 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 30.0}, # Stream2: 320x240 at 30fps ], "-input_framerate": stream.framerate, # controlled framerate for audio-video sync !!! don't forget this line !!! + "-livestream": True, "-audio": [ "-f", "dshow", "-i", "audio=Microphone (USB2.0 Camera)", - ], # assign appropriate input audio-source device and demuxer + ], # assign appropriate input audio-source device(compatible with video source) and its demuxer } # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="dash_out.m3u8", format="hls", **stream_params) + streamer = StreamGear(output="hls_out.m3u8", format="hls", **stream_params) # loop over while True: @@ -1140,21 +1025,22 @@ The complete example is as follows: stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   ## Usage with Hardware Video-Encoder +> In Real-time Frames Mode, you can easily change the video encoder according to your requirements by passing the `-vcodec` FFmpeg parameter as an attribute in the `stream_params` dictionary parameter. Additionally, you can specify additional properties, features, and optimizations for your system's GPU. -In Real-time Frames Mode, you can also easily change encoder as per your requirement just by passing `-vcodec` FFmpeg parameter as an attribute in `stream_params` dictionary parameter. In addition to this, you can also specify the additional properties/features/optimizations for your system's GPU similarly. +In this example, we will be using `h264_vaapi` as our Hardware Encoder and specifying the device hardware's location and compatible video filters by formatting them as attributes in the `stream_params` dictionary parameter. -In this example, we will be using `h264_vaapi` as our hardware encoder and also optionally be specifying our device hardware's location (i.e. `'-vaapi_device':'/dev/dri/renderD128'`) and other features such as `'-vf':'format=nv12,hwupload'` like properties by formatting them as `option` dictionary parameter's attributes, as follows: +!!! danger "This example is just conveying the idea of how to use FFmpeg's hardware encoders with the StreamGear API in Real-time Frames Mode, which MAY OR MAY NOT suit your system. Please use suitable parameters based on your supported system and FFmpeg configurations only." -!!! warning "Check VAAPI support" +!!! warning "Stream copy (`-vcodec copy`) is not compatible with this Mode as it requires re-encoding of incoming frames." - **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.** +??? info "Check VAAPI support" To use `h264_vaapi` encoder, remember to check if its available and your FFmpeg compiled with VAAPI support. You can easily do this by executing following one-liner command in your terminal, and observing if output contains something similar as follows: @@ -1168,6 +1054,8 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also V..... vp8_vaapi VP8 (VAAPI) (codec vp8) ``` +!!! failure "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." + === "DASH" @@ -1189,7 +1077,7 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also ], "-vcodec": "h264_vaapi", # define custom Video encoder "-vaapi_device": "/dev/dri/renderD128", # define device location - "-vf": "format=nv12,hwupload", # define video pixformat + "-vf": "format=nv12,hwupload", # define video filters } # describe a suitable manifest-file location/name and assign params @@ -1227,7 +1115,7 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ``` === "HLS" @@ -1288,10 +1176,10 @@ In this example, we will be using `h264_vaapi` as our hardware encoder and also stream.stop() # safely close streamer - streamer.terminate() + streamer.close() ```   [^1]: - :bulb: In Real-time Frames Mode, the Primary Stream's framerate defaults to [`-input_framerate`](../../params/#a-exclusive-parameters) attribute value, if defined, else it will be 25fps. \ No newline at end of file + :bulb: In Real-time Frames Mode, the Primary Stream's framerate defaults to the value of the [`-input_framerate`](../../params/#a-exclusive-parameters) attribute, if defined. Otherwise, it will be set to 25 fps. \ No newline at end of file diff --git a/docs/gears/streamgear/ssm/overview.md b/docs/gears/streamgear/ssm/overview.md index 83e899b12..bbcdc32ce 100644 --- a/docs/gears/streamgear/ssm/overview.md +++ b/docs/gears/streamgear/ssm/overview.md @@ -18,7 +18,7 @@ limitations under the License. =============================================== --> -# StreamGear API: Single-Source Mode +# StreamGear API: Single-Source Mode :material-file-video-outline:
Single-Source Mode Flow Diagram @@ -45,7 +45,7 @@ This mode can be easily activated by assigning suitable video path as input to [ Apple HLS support was added in `v0.2.2`. -!!! warning +!!! danger "Please Remember :material-police-badge-outline:" * Using [`stream()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.stream) function instead of [`transcode_source()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.transcode_source) in Single-Source Mode will instantly result in **`RuntimeError`**! * Any invalid value to the [`-video_source`](../../params/#a-exclusive-parameters) attribute will result in **`AssertionError`**! diff --git a/docs/gears/streamgear/ssm/usage.md b/docs/gears/streamgear/ssm/usage.md index 6a83d82ff..4f1753113 100644 --- a/docs/gears/streamgear/ssm/usage.md +++ b/docs/gears/streamgear/ssm/usage.md @@ -18,17 +18,19 @@ limitations under the License. =============================================== --> -# StreamGear API Usage Examples: Single-Source Mode +# StreamGear API Usage Examples: Single-Source Mode :material-file-video-outline: -!!! warning "Important Information" +!!! warning "Important Information :fontawesome-solid-person-military-pointing:" - * StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../../ffmpeg_install/) for its installation. + - [x] StreamGear **MUST** requires FFmpeg executables for its core operations. Follow these dedicated [Platform specific Installation Instructions ➶](../../ffmpeg_install/) for its installation. API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. + - [x] In this mode, ==API auto generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== + - [x] In this mode, if input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams. + - [x] Always use `close()` function at the very end of the main code. - * StreamGear API will throw **RuntimeError**, if it fails to detect valid FFmpeg executables on your system. - - * By default, ==StreamGear generates a primary stream of same resolution and framerate[^1] as the input video _(at the index `0`)_.== - - * Always use `terminate()` function at the very end of the main code. +???+ danger "DEPRECATION NOTICES for `v0.3.3` and above" + + - [ ] The `terminate()` method in StreamGear is now deprecated and will be removed in a future release. Developers should use the new [`close()`](../../../../bonus/reference/streamgear/#vidgear.gears.streamgear.StreamGear.close) method instead, as it offers a more descriptive name, similar to the WriteGear API, for safely terminating StreamGear processes. + - [ ] The [`-livestream`](../../params/#a-exclusive-parameters) optional parameter is NOT supported in this Single-Source Mode. !!! example "After going through following Usage Examples, Checkout more of its advanced configurations [here ➶](../../../help/streamgear_ex/)" @@ -40,7 +42,7 @@ limitations under the License. Following is the bare-minimum code you need to get started with StreamGear API in Single-Source Mode: -!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." +!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams." === "DASH" @@ -52,12 +54,14 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream_params = {"-video_source": "foo.mp4"} # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` + !!! success "After running this bare-minimum example, StreamGear will produce a Manifest file (`dash_out.mpd`) with streamable chunks, containing information about a Primary Stream with the same resolution and framerate as the input." + === "HLS" ```python linenums="1" @@ -68,88 +72,37 @@ Following is the bare-minimum code you need to get started with StreamGear API i stream_params = {"-video_source": "foo.mp4"} # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` + !!! success "After running this bare-minimum example, StreamGear will produce a Master Playlist file (`hls_out.mpd`) with streamable chunks, containing information about a Primary Stream with the same resolution and framerate as the input." -!!! success "After running this bare-minimum example, StreamGear will produce a Manifest file _(`dash.mpd`)_ with streamable chunks that contains information about a Primary Stream of same resolution and framerate as the input." - -  - -## Bare-Minimum Usage with Live-Streaming - -You can easily activate ==Low-latency Livestreaming in Single-Source Mode== - chunks will contain information only for few new frames and forgets all previous ones, using exclusive [`-livestream`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter as follows: - -!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." - -=== "DASH" - - !!! tip "Chunk size in DASH" - Use `-window_size` & `-extra_window_size` FFmpeg parameters for controlling number of frames to be kept in Chunks in DASH stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-window_size` & `-extra_window_size` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in manifest will contain NO information of any older ones, and therefore resultant DASH stream will play only the most recent frames." - - ```python linenums="1" hl_lines="5" - # import required libraries - from vidgear.gears import StreamGear - - # activate Single-Source Mode with valid video input and enable livestreaming - stream_params = {"-video_source": 0, "-livestream": True} - # describe a suitable manifest-file location/name and assign params - streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source - streamer.transcode_source() - # terminate - streamer.terminate() - ``` - -=== "HLS" - - !!! tip "Chunk size in HLS" - - Use `-hls_init_time` & `-hls_time` FFmpeg parameters for controlling number of frames to be kept in Chunks in HLS stream. Less these value, less will be latency. - - !!! alert "After every few chunks _(equal to the sum of `-hls_init_time` & `-hls_time` values)_, all chunks will be overwritten in Live-Streaming. Thereby, since newer chunks in playlist will contain NO information of any older ones, and therefore resultant HLS stream will play only the most recent frames." - - ```python linenums="1" hl_lines="5" - # import required libraries - from vidgear.gears import StreamGear - - # activate Single-Source Mode with valid video input and enable livestreaming - stream_params = {"-video_source": 0, "-livestream": True} - # describe a suitable master playlist location/name and assign params - streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # 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. +> In addition to the Primary Stream, you can easily generate any number of additional Secondary Streams with variable bitrate or spatial resolutions, using the exclusive [`-streams`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -!!! info "A more detailed information on `-streams` attribute can be found [here ➶](../../params/#a-exclusive-parameters)" +To generate Secondary Streams, add each desired resolution and bitrate/framerate as a list of dictionaries to the `-streams` attribute. StreamGear will handle the rest automatically. The complete example is as follows: -The complete example is as follows: +!!! info "A more detailed information on `-streams` attribute can be found [here ➶](../../params/#a-exclusive-parameters)" -!!! note "If input video-source contains any audio stream/channel, then it automatically gets assigned to all generated streams without any extra efforts." +!!! note "If input video-source _(i.e. `-video_source`)_ contains any audio stream/channel, then it automatically gets mapped to all generated streams without any extra efforts." -??? danger "Important `-streams` attribute Information" - - * On top of these additional streams, StreamGear by default, generates a primary stream of same resolution and framerate as the input, at the index `0`. - * :warning: Make sure your System/Machine/Server/Network is able to handle these additional streams, discretion is advised! - * You **MUST** need to define `-resolution` value for your stream, otherwise stream will be discarded! - * You only need either of `-video_bitrate` or `-framerate` for defining a valid stream. Since with `-framerate` value defined, video-bitrate is calculated automatically. - * If you define both `-video_bitrate` and `-framerate` values at the same time, StreamGear will discard the `-framerate` value automatically. +???+ danger "Important Information about `-streams` attribute :material-file-document-alert-outline:" -!!! failure "Always use `-stream` attribute to define additional streams safely, any duplicate or incorrect definition can break things!" + * In addition to the user-defined Secondary Streams, StreamGear automatically generates a Primary Stream _(at index `0`)_ with the same resolution and framerate as the input video-source _(i.e. `-video_source`)_. + * :warning: Ensure that your system, machine, server, or network can handle the additional resource requirements of the Secondary Streams. Exercise discretion when configuring multiple streams. + * You **MUST** define the `-resolution` value for each stream; otherwise, the stream will be discarded. + * You only need to define either the `-video_bitrate` or the `-framerate` for a valid stream. + * If you specify the `-framerate`, the video bitrate will be calculated automatically. + * If you define both the `-video_bitrate` and the `-framerate`, the `-framerate` will get discard automatically. +!!! failure "Always use the `-streams` attribute to define additional streams safely. Duplicate or incorrect definitions can break the transcoding pipeline and corrupt the output chunks." === "DASH" @@ -169,10 +122,10 @@ The complete example is as follows: } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" @@ -193,28 +146,28 @@ The complete example is as follows: } # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ```   -## Usage with Custom Audio +## Usage with Custom Audio-Input -By default, if input video-source _(i.e. `-video_source`)_ contains any audio, then it gets automatically mapped to all generated streams. But, if you want to add any custom audio, you can easily do it by using exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. You just need to input the path of your audio file to this attribute as `string`, and the API will automatically validate as well as map it to all generated streams. +> In single source mode, by default, if the input video source (i.e., `-video_source`) contains audio, it gets automatically mapped to all generated streams. However, if you want to add a custom audio source, you can use the exclusive [`-audio`](../../params/#a-exclusive-parameters) attribute of the `stream_params` dictionary parameter. -The complete example is as follows: +To add a custom audio source, provide the path to your audio file as a string to the `-audio` attribute. The API will automatically validate and map the audio to all generated streams. The complete example is as follows: -!!! failure "Make sure this `-audio` audio-source it compatible with provided video-source, otherwise you could encounter multiple errors or no output at all." +!!! failure "Ensure the provided `-audio` audio source is compatible with the input video source (`-video_source`). Incompatibility can cause multiple errors or result in no output at all." -!!! tip "You can also assign a valid Audio URL as input, rather than filepath. More details can be found [here ➶](../../params/#a-exclusive-parameters)" +!!! tip "You can also assign a valid audio URL as input instead of a file path. More details can be found [here ➶](../../params/#a-exclusive-parameters)" === "DASH" - ```python linenums="1" hl_lines="12" + ```python linenums="1" hl_lines="11-12" # import required libraries from vidgear.gears import StreamGear @@ -222,23 +175,23 @@ The complete example is as follows: stream_params = { "-video_source": "foo.mp4", "-streams": [ - {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac", # define custom audio-source + "-acodec": "copy", # define copy audio encoder } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="12" + ```python linenums="1" hl_lines="11-12" # import required libraries from vidgear.gears import StreamGear @@ -246,18 +199,18 @@ The complete example is as follows: stream_params = { "-video_source": "foo.mp4", "-streams": [ - {"-resolution": "1920x1080", "-video_bitrate": "4000k"}, # Stream1: 1920x1080 at 4000kbs bitrate - {"-resolution": "1280x720", "-framerate": 30.0}, # Stream2: 1280x720 at 30fps - {"-resolution": "640x360", "-framerate": 60.0}, # Stream3: 640x360 at 60fps + {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac" # assigns input audio-source: "/home/foo/foo1.aac" + "-audio": "/home/foo/foo1.aac", # define custom audio-source + "-acodec": "copy", # define copy audio encoder } # describe a suitable master playlist location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` @@ -266,75 +219,68 @@ The complete example is as follows: ## Usage with Variable FFmpeg Parameters -For seamlessly generating these streaming assets, StreamGear provides a highly extensible and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) and access to almost all of its parameter. Thereby, you can access almost any parameter available with FFmpeg itself as dictionary attributes in [`stream_params` dictionary parameter](../../params/#stream_params), and use it to manipulate transcoding as you like. +> For fine-grained control over the transcoding process, StreamGear provides a highly extensible and flexible wrapper around [**FFmpeg**](https://ffmpeg.org/) library and access to almost all of its configurational parameter. -For this example, let us use our own [H.265/HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) video and [AAC](https://trac.ffmpeg.org/wiki/Encode/AAC) audio encoder, and set custom audio bitrate, and various other optimizations: +In this example, we'll use the [H.265/HEVC](https://trac.ffmpeg.org/wiki/Encode/H.265) video encoder and [AAC](https://trac.ffmpeg.org/wiki/Encode/AAC) audio encoder, apply various optimal FFmpeg configurational parameters. +!!! warning "This example assumes that the given input video source (`-video_source`) contains at least one audio stream." -!!! tip "This example is just conveying the idea on how to use FFmpeg's encoders/parameters with StreamGear API. You can use any FFmpeg parameter in the similar manner." +!!! info "This example is just conveying the idea on how to use FFmpeg's internal encoders/parameters with StreamGear API. You can use any FFmpeg parameter in the similar manner." -!!! danger "Kindly read [**FFmpeg Docs**](https://ffmpeg.org/documentation.html) carefully, before passing any FFmpeg values to `stream_params` parameter. Wrong values may result in undesired errors or no output at all." +!!! danger "Please read the [**FFmpeg Documentation**](https://ffmpeg.org/documentation.html) carefully before passing any additional values to the `stream_params` parameter. Incorrect values may cause errors or result in no output." -!!! failure "Always use `-streams` attribute to define additional streams safely, any duplicate or incorrect stream definition can break things!" === "DASH" - ```python linenums="1" hl_lines="6-10 15-17" + ```python linenums="1" hl_lines="6-9 14" # import required libraries from vidgear.gears import StreamGear # activate Single-Source Mode and various other parameters stream_params = { "-video_source": "foo.mp4", # define Video-Source - "-vcodec": "libx265", # assigns H.265/HEVC video encoder + "-vcodec": "libx265", # specify H.265/HEVC video encoder "-x265-params": "lossless=1", # enables Lossless encoding - "-crf": 25, # Constant Rate Factor: 25 - "-bpp": "0.15", # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes + "-bpp": 0.15, # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps + {"-resolution": "640x360", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac", # define input audio-source: "/home/foo/foo1.aac", - "-acodec": "libfdk_aac", # assign lossless AAC audio encoder - "-vbr": 4, # Variable Bit Rate: `4` + "-acodec": "aac", # specify AAC audio encoder } # describe a suitable manifest-file location/name and assign params streamer = StreamGear(output="dash_out.mpd", logging=True, **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ``` === "HLS" - ```python linenums="1" hl_lines="6-10 15-17" + ```python linenums="1" hl_lines="6-9 14" # import required libraries from vidgear.gears import StreamGear - # activate Single-Source Mode and various other parameters stream_params = { "-video_source": "foo.mp4", # define Video-Source - "-vcodec": "libx265", # assigns H.265/HEVC video encoder + "-vcodec": "libx265", # specify H.265/HEVC video encoder "-x265-params": "lossless=1", # enables Lossless encoding - "-crf": 25, # Constant Rate Factor: 25 - "-bpp": "0.15", # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes + "-bpp": 0.15, # Bits-Per-Pixel(BPP), an Internal StreamGear parameter to ensure good quality of high motion scenes "-streams": [ - {"-resolution": "1280x720", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate - {"-resolution": "640x360", "-framerate": 60.0}, # Stream2: 640x360 at 60fps + {"-resolution": "640x360", "-video_bitrate": "4000k"}, # Stream1: 1280x720 at 4000kbs bitrate + {"-resolution": "320x240", "-framerate": 60.0}, # Stream2: 640x360 at 60fps ], - "-audio": "/home/foo/foo1.aac", # define input audio-source: "/home/foo/foo1.aac", - "-acodec": "libfdk_aac", # assign lossless AAC audio encoder - "-vbr": 4, # Variable Bit Rate: `4` + "-acodec": "aac", # specify AAC audio encoder } # describe a suitable master playlist file location/name and assign params streamer = StreamGear(output="hls_out.m3u8", format = "hls", logging=True, **stream_params) - # trancode source + # transcode source streamer.transcode_source() - # terminate - streamer.terminate() + # close + streamer.close() ```   diff --git a/docs/gears/writegear/compression/params.md b/docs/gears/writegear/compression/params.md index 9afe0e092..b5724db2e 100644 --- a/docs/gears/writegear/compression/params.md +++ b/docs/gears/writegear/compression/params.md @@ -188,9 +188,9 @@ This parameter allows us to exploit almost all FFmpeg supported parameters effor output_params = {"-disable_ffmpeg_window": True} # disables FFmpeg creation window ``` - * **`-disable_force_termination`** _(bool)_: sets a special flag to manually disable the default forced-termination behaviour in WriteGear API when `-i` FFmpeg parameter is used _(For more details, see issue: #149)_. Its usage is as follows: + * **`-disable_force_termination`** _(bool)_: sets a special flag to manually disable the default forced termination of FFmpeg process in WriteGear API when `-i` FFmpeg parameter is used _(For more details, see issue: #149)_. Its usage is as follows: - !!! warning "`-disable_force_termination` flag is a absolute necessity when video duration is too short(<60sec), otherwise WriteGear will not produce any valid output." + !!! warning "The `-disable_force_termination` flag is a absolute necessity when video duration is too short(`< 60sec`), otherwise WriteGear may produce invalid or no output." ```python output_params = {"-disable_force_termination": True} # disable the default forced-termination behaviour diff --git a/docs/help/streamgear_faqs.md b/docs/help/streamgear_faqs.md index 073c59c9a..31cdf9c65 100644 --- a/docs/help/streamgear_faqs.md +++ b/docs/help/streamgear_faqs.md @@ -77,16 +77,7 @@ limitations under the License.   -## 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`](../../gears/streamgear/params/#a-exclusive-parameters) attribute of `stream_params` dictionary parameter. - - -## How to use Hardware/GPU encoder for StreamGear trancoding? +## How to use Hardware/GPU encoder for transcoding in StreamGear API? **Answer:** [See this example ➶](../../gears/streamgear/rtfm/usage/#usage-with-hardware-video-encoder) diff --git a/docs/overrides/hooks/js_hook.py b/docs/overrides/hooks/js_hook.py index 27b72a194..8f0175307 100644 --- a/docs/overrides/hooks/js_hook.py +++ b/docs/overrides/hooks/js_hook.py @@ -22,7 +22,7 @@ from mkdocs.structure.pages import Page js_scripts = """ -, + diff --git a/setup.py b/setup.py index d1c35c53f..0c0e89feb 100644 --- a/setup.py +++ b/setup.py @@ -110,7 +110,7 @@ def latest_version(package_name): # API specific deps "core": [ "yt_dlp{}".format(latest_version("yt_dlp")), - "pyzmq==24.0.1", + "pyzmq{}".format(latest_version("pyzmq")), "Pillow", "simplejpeg{}".format(latest_version("simplejpeg")), "mss{}".format(latest_version("mss")), @@ -124,7 +124,7 @@ def latest_version(package_name): # API specific + Asyncio deps "asyncio": [ "yt_dlp{}".format(latest_version("yt_dlp")), - "pyzmq==24.0.1", + "pyzmq{}".format(latest_version("pyzmq")), "simplejpeg{}".format(latest_version("simplejpeg")), "mss{}".format(latest_version("mss")), "Pillow", diff --git a/vidgear/gears/helper.py b/vidgear/gears/helper.py index e900d016c..47069021d 100755 --- a/vidgear/gears/helper.py +++ b/vidgear/gears/helper.py @@ -35,6 +35,8 @@ import logging as log import platform import socket +import warnings +from functools import wraps from tqdm import tqdm from contextlib import closing from pathlib import Path @@ -154,6 +156,42 @@ def get_module_version(module=None): return str(version) +def deprecated(parameter=None, message=None, stacklevel=2): + """ + ### deprecated + + Decorator to mark a parameter or function as deprecated. + + Parameters: + parameter(str): Name of parameter to be deprecated. + message(str): Custom message to display in warning message. + stacklevel(int): Stack frames level. + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if parameter and parameter in kwargs: + warnings.warn( + message + or f"Parameter '{parameter}' is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=stacklevel, + ) + else: + warnings.warn( + message + or f"Function '{func.__name__}' is deprecated and will be removed in future versions.", + DeprecationWarning, + stacklevel=stacklevel, + ) + return func(*args, **kwargs) + + return wrapper + + return decorator + + def import_dependency_safe( name, error="raise", @@ -669,12 +707,11 @@ def extract_time(value): return 0 else: stripped_data = value.strip() - t_duration = re.findall( - r"(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)", stripped_data - ) + t_duration = re.findall(r"\d{2}:\d{2}:\d{2}(?:\.\d{2})?", stripped_data) return ( sum( - int(x) * 60**i for i, x in enumerate(reversed(t_duration[0].split(":"))) + float(x) * 60**i + for i, x in enumerate(reversed(t_duration[0].split(":"))) ) if t_duration else 0 diff --git a/vidgear/gears/netgear.py b/vidgear/gears/netgear.py index 17b5aeab8..7bae7d644 100644 --- a/vidgear/gears/netgear.py +++ b/vidgear/gears/netgear.py @@ -17,9 +17,12 @@ limitations under the License. =============================================== """ + # import the necessary packages import os import time +import asyncio +import platform import string import secrets import numpy as np @@ -185,6 +188,9 @@ def __init__( # Handle NetGear's internal exclusive modes and params + # define Secure Mode + self.__z_auth = None + # define SSH Tunneling Mode self.__ssh_tunnel_mode = None # handles ssh_tunneling mode state self.__ssh_tunnel_pwd = None @@ -413,56 +419,6 @@ def __init__( else: pass - # Handle Secure mode - if self.__secure_mode: - # activate and log if overwriting is enabled - if overwrite_cert: - if not receive_mode: - self.__logging and logger.warning( - "Overwriting ZMQ Authentication certificates over previous ones!" - ) - else: - overwrite_cert = False - self.__logging and logger.critical( - "Overwriting ZMQ Authentication certificates is disabled for Client's end!" - ) - - # Validate certificate generation paths - try: - # check if custom certificates path is specified - if custom_cert_location: - ( - auth_cert_dir, - self.__auth_secretkeys_dir, - self.__auth_publickeys_dir, - ) = generate_auth_certificates( - custom_cert_location, overwrite=overwrite_cert, logging=logging - ) - else: - # otherwise auto-generate suitable path - ( - auth_cert_dir, - self.__auth_secretkeys_dir, - self.__auth_publickeys_dir, - ) = generate_auth_certificates( - os.path.join(expanduser("~"), ".vidgear"), - overwrite=overwrite_cert, - logging=logging, - ) - # log it - self.__logging and logger.debug( - "`{}` is the default location for storing ZMQ authentication certificates/keys.".format( - auth_cert_dir - ) - ) - except Exception as e: - # catch if any error occurred and disable Secure mode - logger.exception(str(e)) - self.__secure_mode = 0 - logger.critical( - "ZMQ Security Mechanism is disabled for this connection due to errors!" - ) - # Handle ssh tunneling if enabled if not (self.__ssh_tunnel_mode is None): # SSH Tunnel Mode only available for server mode @@ -528,17 +484,93 @@ def __init__( ) ) - # define messaging context instance + # On Windows, NetGear requires the ``WindowsSelectorEventLoop`` but Python 3.8 and above, + # defaults to an ``ProactorEventLoop`` loop that is not compatible with it. Thereby, + # we had to set it manually. + platform.system() == "Windows" and asyncio.set_event_loop_policy( + asyncio.WindowsSelectorEventLoopPolicy() + ) + + # define ZMQ messaging context instance self.__msg_context = zmq.Context.instance() # initialize and assign receive mode to global variable self.__receive_mode = receive_mode + # Handle Secure mode + if self.__secure_mode > 0: + # activate and log if overwriting is enabled + if receive_mode: + overwrite_cert = False + overwrite_cert and logger.warning( + "Overwriting ZMQ Authentication certificates is disabled for Client's end!" + ) + else: + overwrite_cert and self.__logging and logger.info( + "Overwriting ZMQ Authentication certificates over previous ones!" + ) + + # Validate certificate generation paths + # Start threaded authenticator for this context + try: + # check if custom certificates path is specified + if custom_cert_location: + ( + auth_cert_dir, + self.__auth_secretkeys_dir, + self.__auth_publickeys_dir, + ) = generate_auth_certificates( + custom_cert_location, overwrite=overwrite_cert, logging=logging + ) + else: + # otherwise auto-generate suitable path + ( + auth_cert_dir, + self.__auth_secretkeys_dir, + self.__auth_publickeys_dir, + ) = generate_auth_certificates( + os.path.join(expanduser("~"), ".vidgear"), + overwrite=overwrite_cert, + logging=logging, + ) + # log it + self.__logging and logger.debug( + "`{}` is the default location for storing ZMQ authentication certificates/keys.".format( + auth_cert_dir + ) + ) + + # start an authenticator for this context + self.__z_auth = ThreadAuthenticator(self.__msg_context) + self.__z_auth.start() + self.__z_auth.allow(str(address)) # allow current address + + # check if `IronHouse` is activated + if self.__secure_mode == 2: + # tell authenticator to use the certificate from given valid dir + self.__z_auth.configure_curve( + domain="*", location=self.__auth_publickeys_dir + ) + else: + # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated + self.__z_auth.configure_curve( + domain="*", location=auth.CURVE_ALLOW_ANY + ) + except zmq.ZMQError as e: + if "Address in use" in str(e): + logger.info("ZMQ Authenticator already running.") + else: + # catch if any error occurred and disable Secure mode + logger.exception(str(e)) + self.__secure_mode = 0 + logger.error( + "ZMQ Security Mechanism is disabled for this connection due to errors!" + ) + # check whether `receive_mode` is enabled if self.__receive_mode: # define connection address - if address is None: - address = "*" # define address + address = "*" if address is None else address # check if multiserver_mode is enabled if self.__multiserver_mode: @@ -574,35 +606,14 @@ def __init__( self.__port = port else: # otherwise assign local port address if None - if port is None: - port = "5555" + port = "5555" if port is None else port try: - # activate secure_mode threaded authenticator - if self.__secure_mode > 0: - # start an authenticator for this context - 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 - z_auth.configure_curve( - domain="*", location=self.__auth_publickeys_dir - ) - else: - # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - z_auth.configure_curve( - domain="*", location=auth.CURVE_ALLOW_ANY - ) - # define thread-safe messaging socket self.__msg_socket = self.__msg_context.socket(msg_pattern[1]) # define pub-sub flag - if self.__pattern == 2: - self.__msg_socket.set_hwm(1) + self.__pattern == 2 and self.__msg_socket.set_hwm(1) # enable specified secure mode for the socket if self.__secure_mode > 0: @@ -671,27 +682,29 @@ def __init__( except Exception as e: # otherwise log and raise error logger.exception(str(e)) - if self.__secure_mode: - logger.critical( - "Failed to activate Secure Mode: `{}` for this connection!".format( - valid_security_mech[self.__secure_mode] - ) + # Handle Secure Mode + self.__secure_mode and logger.critical( + "Failed to activate Secure Mode: `{}` for this connection!".format( + valid_security_mech[self.__secure_mode] ) + ) + # raise errors for exclusive modes if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( - "Multi-Server" - if self.__multiserver_mode - else "Multi-Client", + ( + "Multi-Server" + if self.__multiserver_mode + else "Multi-Client" + ), (protocol + "://" + str(address) + ":" + str(port)), pattern, ) ) else: - if self.__bi_mode: - logger.critical( - "Failed to activate Bidirectional Mode for this connection!" - ) + self.__bi_mode and logger.critical( + "Failed to activate Bidirectional Mode for this connection!" + ) raise RuntimeError( "[NetGear:ERROR] :: Receive Mode failed to bind address: {} and pattern: {}! Kindly recheck all parameters.".format( (protocol + "://" + str(address) + ":" + str(port)), pattern @@ -718,35 +731,31 @@ def __init__( (protocol + "://" + str(address) + ":" + str(port)), pattern ) ) - if self.__jpeg_compression: - logger.debug( - "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( - self.__jpeg_compression_colorspace, - self.__jpeg_compression_quality, - "enabled" - if self.__jpeg_compression_fastdct - else "disabled", + self.__jpeg_compression and logger.debug( + "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + self.__jpeg_compression_colorspace, + self.__jpeg_compression_quality, + ("enabled" if self.__jpeg_compression_fastdct else "disabled"), + ( "enabled" if self.__jpeg_compression_fastupsample - else "disabled", - ) + else "disabled" + ), ) - if self.__secure_mode: - logger.debug( - "Successfully enabled ZMQ Security Mechanism: `{}` for this connection.".format( - valid_security_mech[self.__secure_mode] - ) + ) + self.__secure_mode and logger.debug( + "Successfully enabled ZMQ Security Mechanism: `{}` for this connection.".format( + valid_security_mech[self.__secure_mode] ) + ) logger.debug("Multi-threaded Receive Mode is successfully enabled.") logger.debug("Unique System ID is {}.".format(self.__id)) logger.debug("Receive Mode is now activated.") else: # otherwise default to `Send Mode` - # define connection address - if address is None: - address = "localhost" + address = "localhost" if address is None else address # check if multiserver_mode is enabled if self.__multiserver_mode: @@ -782,29 +791,9 @@ def __init__( self.__port_buffer = [] else: # otherwise assign local port address if None - if port is None: - port = "5555" + port = "5555" if port is None else port try: - # activate secure_mode threaded authenticator - if self.__secure_mode > 0: - # start an authenticator for this context - 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 - z_auth.configure_curve( - domain="*", location=self.__auth_publickeys_dir - ) - else: - # otherwise tell the authenticator how to handle the CURVE requests, if `StoneHouse` is activated - z_auth.configure_curve( - domain="*", location=auth.CURVE_ALLOW_ANY - ) - # define thread-safe messaging socket self.__msg_socket = self.__msg_context.socket(msg_pattern[0]) @@ -886,33 +875,34 @@ def __init__( except Exception as e: # otherwise log and raise error logger.exception(str(e)) - if self.__secure_mode: - logger.critical( - "Failed to activate Secure Mode: `{}` for this connection!".format( - valid_security_mech[self.__secure_mode] - ) + # Handle Secure Mode + self.__secure_mode and logger.critical( + "Failed to activate Secure Mode: `{}` for this connection!".format( + valid_security_mech[self.__secure_mode] ) + ) + # raise errors for exclusive modes if self.__multiserver_mode or self.__multiclient_mode: raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to activate {} Mode at address: {} with pattern: {}! Kindly recheck all parameters.".format( - "Multi-Server" - if self.__multiserver_mode - else "Multi-Client", + ( + "Multi-Server" + if self.__multiserver_mode + else "Multi-Client" + ), (protocol + "://" + str(address) + ":" + str(port)), pattern, ) ) else: - if self.__bi_mode: - logger.critical( - "Failed to activate Bidirectional Mode for this connection!" - ) - if self.__ssh_tunnel_mode: - logger.critical( - "Failed to initiate SSH Tunneling Mode for this server with `{}` back-end!".format( - "paramiko" if self.__paramiko_present else "pexpect" - ) + self.__bi_mode and logger.critical( + "Failed to activate Bidirectional Mode for this connection!" + ) + self.__ssh_tunnel_mode and logger.critical( + "Failed to initiate SSH Tunneling Mode for this server with `{}` back-end!".format( + "paramiko" if self.__paramiko_present else "pexpect" ) + ) raise RuntimeError( "[NetGear:ERROR] :: Send Mode failed to connect address: {} and pattern: {}! Kindly recheck all parameters.".format( (protocol + "://" + str(address) + ":" + str(port)), pattern @@ -926,25 +916,23 @@ def __init__( (protocol + "://" + str(address) + ":" + str(port)), pattern ) ) - if self.__jpeg_compression: - logger.debug( - "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( - self.__jpeg_compression_colorspace, - self.__jpeg_compression_quality, - "enabled" - if self.__jpeg_compression_fastdct - else "disabled", + self.__jpeg_compression and logger.debug( + "JPEG Frame-Compression is activated for this connection with Colorspace:`{}`, Quality:`{}`%, Fastdct:`{}`, and Fastupsample:`{}`.".format( + self.__jpeg_compression_colorspace, + self.__jpeg_compression_quality, + ("enabled" if self.__jpeg_compression_fastdct else "disabled"), + ( "enabled" if self.__jpeg_compression_fastupsample - else "disabled", - ) + else "disabled" + ), ) - if self.__secure_mode: - logger.debug( - "Enabled ZMQ Security Mechanism: `{}` for this connection.".format( - valid_security_mech[self.__secure_mode] - ) + ) + self.__secure_mode and logger.debug( + "Enabled ZMQ Security Mechanism: `{}` for this connection.".format( + valid_security_mech[self.__secure_mode] ) + ) logger.debug("Unique System ID is {}.".format(self.__id)) logger.debug( "Send Mode is successfully activated and ready to send data." @@ -955,8 +943,9 @@ def __recv_handler(self): A threaded receiver handler, that keep iterating data from ZMQ socket to a internally monitored deque, until the thread is terminated, or socket disconnects. """ - # initialize frame variable + # initialize variables frame = None + msg_json = None # keep looping infinitely until the thread is terminated while not self.__terminate: @@ -1014,7 +1003,7 @@ def __recv_handler(self): break # check if terminate_flag` received - if msg_json["terminate_flag"]: + if msg_json and msg_json["terminate_flag"]: # if multiserver_mode is enabled if self.__multiserver_mode: # check and remove from which ports signal is received @@ -1050,11 +1039,17 @@ def __recv_handler(self): ) continue - msg_data = self.__msg_socket.recv( - flags=self.__msg_flag | zmq.DONTWAIT, - copy=self.__msg_copy, - track=self.__msg_track, - ) + try: + msg_data = self.__msg_socket.recv( + flags=self.__msg_flag | zmq.DONTWAIT, + copy=self.__msg_copy, + track=self.__msg_track, + ) + except zmq.ZMQError as e: + logger.critical("Socket Session Expired. Exiting!") + self.__terminate = True + self.__queue.append(None) + break # handle data transfer in synchronous modes. if self.__pattern < 2: @@ -1103,19 +1098,25 @@ def __recv_handler(self): return_dict.update( dict( return_type=(type(self.__return_data).__name__), - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - "colorspace": self.__jpeg_compression_colorspace, - } - if self.__jpeg_compression - else False, - array_dtype=str(self.__return_data.dtype) - if not (self.__jpeg_compression) - else "", - array_shape=self.__return_data.shape - if not (self.__jpeg_compression) - else "", + compression=( + { + "dct": self.__jpeg_compression_fastdct, + "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, + } + if self.__jpeg_compression + else False + ), + array_dtype=( + str(self.__return_data.dtype) + if not (self.__jpeg_compression) + else "" + ), + array_shape=( + self.__return_data.shape + if not (self.__jpeg_compression) + else "" + ), data=None, ) ) @@ -1302,13 +1303,15 @@ def send(self, frame, message=None): msg_dict.update( dict( terminate_flag=exit_flag, - compression={ - "dct": self.__jpeg_compression_fastdct, - "ups": self.__jpeg_compression_fastupsample, - "colorspace": self.__jpeg_compression_colorspace, - } - if self.__jpeg_compression - else False, + compression=( + { + "dct": self.__jpeg_compression_fastdct, + "ups": self.__jpeg_compression_fastupsample, + "colorspace": self.__jpeg_compression_colorspace, + } + if self.__jpeg_compression + else False + ), message=message, pattern=str(self.__pattern), dtype=str(frame.dtype) if not (self.__jpeg_compression) else "", @@ -1470,7 +1473,6 @@ def send(self, frame, message=None): # connect normally self.__msg_socket.connect(self.__connection_address) self.__poll.register(self.__msg_socket, zmq.POLLIN) - return None # log confirmation @@ -1498,9 +1500,16 @@ def close(self, kill=False): self.__terminate = True # properly close the socket self.__logging and logger.debug("Terminating. Please wait...") + # Handle Secure Mode Thread + if self.__z_auth: + self.__logging and logger.debug("Terminating Authenticator Thread.") + self.__z_auth.stop() + while self.__z_auth.is_alive(): + pass # wait until stream resources are released # (producer thread might be still grabbing frame) if self.__thread is not None: + self.__logging and logger.debug("Terminating Main Thread.") # properly handle thread exit if self.__thread.is_alive() and kill: # force close if still alive @@ -1508,8 +1517,8 @@ def close(self, kill=False): self.__msg_context.destroy() self.__thread.join() else: - self.__thread.join() self.__msg_socket.close(linger=0) + self.__thread.join() self.__thread = None self.__logging and logger.debug("Terminated Successfully!") else: @@ -1519,6 +1528,12 @@ def close(self, kill=False): kill and logger.warning( "`kill` parmeter is only available in the receive mode." ) + # Handle Secure Mode Thread + if self.__z_auth: + self.__logging and logger.debug("Terminating Authenticator Thread.") + self.__z_auth.stop() + while self.__z_auth.is_alive(): + pass # check if all attempts of reconnecting failed, then skip to closure if (self.__pattern < 2 and not self.__max_retries) or ( self.__multiclient_mode and not self.__port_buffer diff --git a/vidgear/gears/streamgear.py b/vidgear/gears/streamgear.py index 65e623d97..7fd89a1ae 100644 --- a/vidgear/gears/streamgear.py +++ b/vidgear/gears/streamgear.py @@ -22,8 +22,7 @@ import os import time import math -import platform -import pathlib +import signal import difflib import logging as log import subprocess as sp @@ -33,7 +32,7 @@ # import helper packages from .helper import ( - capPropId, + deprecated, dict2Args, delete_ext_safe, extract_time, @@ -60,11 +59,11 @@ class StreamGear: StreamGear provides a standalone, highly extensible, and flexible wrapper around FFmpeg multimedia framework for generating chunked-encoded media segments of the content. SteamGear easily transcodes source videos/audio files & real-time video-frames and breaks them into a sequence of multiple smaller chunks/segments of suitable length. These segments make it - possible to stream videos at different quality levels (different bitrates or spatial resolutions) and can be switched in the middle of a video from one quality level to another – if bandwidth - permits – on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. + possible to stream videos at different quality levels _(different bitrate or spatial resolutions)_ and can be switched in the middle of a video from one quality level to another - if bandwidth + permits - on a per-segment basis. A user can serve these segments on a web server that makes it easier to download them through HTTP standard-compliant GET requests. - SteamGear also creates a Manifest/Playlist file (such as MPD in-case of DASH and M3U8 in-case of HLS) besides segments that describe these segment information (timing, URL, media characteristics like video resolution and bit rates) - and is provided to the client before the streaming session. + SteamGear also creates a Manifest/Playlist file (such as MPD in-case of DASH and M3U8 in-case of HLS) besides segments that describe these segment information + (timing, URL, media characteristics like video resolution and bit rates) and is provided to the client before the streaming session. SteamGear currently supports MPEG-DASH (Dynamic Adaptive Streaming over HTTP, ISO/IEC 23009-1) and Apple HLS (HTTP live streaming). """ @@ -76,13 +75,12 @@ def __init__( This constructor method initializes the object state and attributes of the StreamGear class. Parameters: - output (str): sets the valid filename/path for storing the StreamGear assets. + output (str): sets the valid filename/path for generating the StreamGear assets. format (str): select the adaptive HTTP streaming format(DASH and HLS). custom_ffmpeg (str): assigns the location of custom path/directory for custom FFmpeg executables. logging (bool): enables/disables logging. stream_params (dict): provides the flexibility to control supported internal parameters and FFmpeg properties. """ - # enable logging if specified self.__logging = logging if isinstance(logging, bool) else False @@ -109,9 +107,7 @@ def __init__( # cleans and reformat user-defined parameters self.__params = { - str(k).strip(): ( - str(v).strip() if not isinstance(v, (dict, list, int, float)) else v - ) + str(k).strip(): (v.strip() if isinstance(v, str) else v) for k, v in stream_params.items() } @@ -141,24 +137,25 @@ def __init__( ) # handle Audio-Input - audio = self.__params.pop("-audio", "") + audio = self.__params.pop("-audio", False) if audio and isinstance(audio, str): if os.path.isfile(audio): self.__audio = os.path.abspath(audio) elif is_valid_url(self.__ffmpeg, url=audio, logging=self.__logging): self.__audio = audio else: - self.__audio = "" + self.__audio = False elif audio and isinstance(audio, list): self.__audio = audio else: - self.__audio = "" - - if self.__audio and self.__logging: - logger.debug("External audio source detected!") + self.__audio = False + # log external audio source + self.__audio and self.__logging and logger.debug( + "External audio source `{}` detected.".format(self.__audio) + ) # handle Video-Source input - source = self.__params.pop("-video_source", "") + source = self.__params.pop("-video_source", False) # Check if input is valid string if source and isinstance(source, str) and len(source) > 1: # Differentiate input @@ -168,7 +165,8 @@ def __init__( self.__video_source = source else: # discard the value otherwise - self.__video_source = "" + self.__video_source = False + # Validate input if self.__video_source: validation_results = validate_video( @@ -190,10 +188,17 @@ def __init__( ) ) else: - logger.warning("No valid video_source provided.") + # log warning + logger.warning("Discarded invalid `-video_source` value provided.") else: + if source: + # log warning if source provided + logger.warning("Invalid `-video_source` value provided.") + else: + # log normally + logger.info("No `-video_source` value provided.") # discard the value otherwise - self.__video_source = "" + self.__video_source = False # handle user-defined framerate self.__inputframerate = self.__params.pop("-input_framerate", 0.0) @@ -204,80 +209,109 @@ def __init__( # reset improper values self.__inputframerate = 0.0 - # handle old assests - self.__clear_assets = self.__params.pop("-clear_prev_assets", False) - if not isinstance(self.__clear_assets, bool): + # handle old assets + clear_assets = self.__params.pop("-clear_prev_assets", False) + if isinstance(clear_assets, bool): + self.__clear_assets = clear_assets + # log if clearing assets is enabled + clear_assets and logger.debug( + "Previous StreamGear API assets will be deleted in this run." + ) + else: # reset improper values self.__clear_assets = False # handle whether to livestream? - self.__livestreaming = self.__params.pop("-livestream", False) - if not isinstance(self.__livestreaming, bool): + livestreaming = self.__params.pop("-livestream", False) + if isinstance(livestreaming, bool): + # NOTE: `livestream` is only available with real-time mode. + self.__livestreaming = livestreaming if not (self.__video_source) else False + if self.__video_source: + logger.error( + "Live-Streaming is only available with Real-time Mode. Refer docs for more information." + ) + else: + # log if live streaming is enabled + livestreaming and logger.info( + "Live-Streaming is successfully enabled for this run." + ) + else: # reset improper values self.__livestreaming = False - # handle Streaming formats - supported_formats = ["dash", "hls"] # will be extended in future - # Validate - if not (format is None) and format and isinstance(format, str): + # handle the special-case of forced-termination + enable_force_termination = self.__params.pop("-enable_force_termination", False) + # check if value is valid + if isinstance(enable_force_termination, bool): + self.__forced_termination = enable_force_termination + # log if forced termination is enabled + self.__forced_termination and logger.warning( + "Forced termination is enabled for this run. This may result in corrupted output in certain scenarios!" + ) + else: + # handle improper values + self.__forced_termination = False + + # handle streaming format + supported_formats = ["dash", "hls"] # TODO will be extended in future + if format and isinstance(format, str): _format = format.strip().lower() if _format in supported_formats: self.__format = _format logger.info( - "StreamGear will generate files for {} HTTP streaming format.".format( + "StreamGear will generate asset files for {} streaming format.".format( self.__format.upper() ) ) elif difflib.get_close_matches(_format, supported_formats): raise ValueError( - "[StreamGear:ERROR] :: Incorrect format! Did you mean `{}`?".format( + "[StreamGear:ERROR] :: Incorrect `format` parameter value! Did you mean `{}`?".format( difflib.get_close_matches(_format, supported_formats)[0] ) ) else: raise ValueError( - "[StreamGear:ERROR] :: format value `{}` not valid/supported!".format( + "[StreamGear:ERROR] :: The `format` parameter value `{}` not valid/supported!".format( format ) ) else: raise ValueError( - "[StreamGear:ERROR] :: format value is Missing/Incorrect. Check vidgear docs!" + "[StreamGear:ERROR] :: The `format` parameter value is Missing or Invalid!" ) - # handles output name - if not output: - raise ValueError( - "[StreamGear:ERROR] :: Kindly provide a valid `output` value. Refer Docs for more information." - ) - else: + # handles output asset filenames + if output: # validate this class has the access rights to specified directory or not abs_path = os.path.abspath(output) - + # check if given output is a valid system path if check_WriteAccess( os.path.dirname(abs_path), is_windows=self.__os_windows, logging=self.__logging, ): - # check if given path is directory - valid_extension = "mpd" if self.__format == "dash" else "m3u8" # get all assets extensions + valid_extension = "mpd" if self.__format == "dash" else "m3u8" assets_exts = [ ("chunk-stream", ".m4s"), # filename prefix, extension ("chunk-stream", ".ts"), # filename prefix, extension ".{}".format(valid_extension), ] # add source file extension too - if self.__video_source: - assets_exts.append( - ( - "chunk-stream", - os.path.splitext(self.__video_source)[1], - ) # filename prefix, extension - ) + self.__video_source and assets_exts.append( + ( + "chunk-stream", + os.path.splitext(self.__video_source)[1], + ) # filename prefix, extension + ) + # handle output + # check if path is a directory if os.path.isdir(abs_path): - if self.__clear_assets: - delete_ext_safe(abs_path, assets_exts, logging=self.__logging) + # clear previous assets if specified + self.__clear_assets and delete_ext_safe( + abs_path, assets_exts, logging=self.__logging + ) + # auto-assign valid name and adds it to path abs_path = os.path.join( abs_path, "{}-{}.{}".format( @@ -285,8 +319,10 @@ def __init__( time.strftime("%Y%m%d-%H%M%S"), valid_extension, ), - ) # auto-assign valid name and adds it to path - elif self.__clear_assets and os.path.isfile(abs_path): + ) + # or check if path is a file + elif os.path.isfile(abs_path) and self.__clear_assets: + # clear previous assets if specified delete_ext_safe( os.path.dirname(abs_path), assets_exts, @@ -299,54 +335,60 @@ def __init__( output, self.__format.upper() ) self.__logging and logger.debug( - "Path:`{}` is sucessfully configured for streaming.".format( + "Output Path:`{}` is successfully configured for generating streaming assets.".format( abs_path ) ) - # assign it - self.__out_file = abs_path.replace( - "\\", "/" - ) # workaround for Windows platform only, others will not be affected - elif platform.system() == "Linux" and pathlib.Path(output).is_char_device(): - # check if linux video device path (such as `/dev/video0`) - self.__logging and logger.debug( - "Path:`{}` is a valid Linux Video Device path.".format(output) - ) - self.__out_file = output + # workaround patch for Windows only, + # others platforms will not be affected + self.__out_file = abs_path.replace("\\", "/") # check if given output is a valid URL elif is_valid_url(self.__ffmpeg, url=output, logging=self.__logging): self.__logging and logger.debug( - "URL:`{}` is valid and sucessfully configured for streaming.".format( + "URL:`{}` is valid and successfully configured for generating streaming assets.".format( output ) ) self.__out_file = output + # raise ValueError otherwise else: raise ValueError( - "[StreamGear:ERROR] :: Output value:`{}` is not valid/supported!".format( + "[StreamGear:ERROR] :: The output parameter value:`{}` is not valid/supported!".format( output ) ) + else: + # raise ValueError otherwise + raise ValueError( + "[StreamGear:ERROR] :: Kindly provide a valid `output` parameter value. Refer Docs for more information." + ) + # log Mode of operation - logger.info( + self.__video_source and logger.info( "StreamGear has been successfully configured for {} Mode.".format( "Single-Source" if self.__video_source else "Real-time Frames" ) ) + @deprecated( + parameter="rgb_mode", + message="The `rgb_mode` parameter is deprecated and will be removed in a future version. Only BGR format frames will be supported going forward.", + ) def stream(self, frame, rgb_mode=False): """ - Pipelines `ndarray` frames to FFmpeg Pipeline for transcoding into multi-bitrate streamable assets. + Pipes `ndarray` frames to FFmpeg Pipeline for transcoding them into chunked-encoded media segments of + streaming formats such as MPEG-DASH and HLS. + + !!! warning "[DEPRECATION NOTICE]: The `rgb_mode` parameter is deprecated and will be removed in a future version." Parameters: frame (ndarray): a valid numpy frame rgb_mode (boolean): enable this flag to activate RGB mode _(i.e. specifies that incoming frames are of RGB format instead of default BGR)_. - """ # check if function is called in correct context if self.__video_source: raise RuntimeError( - "[StreamGear:ERROR] :: `stream()` function cannot be used when streaming from a `-video_source` input file. Kindly refer vidgear docs!" + "[StreamGear:ERROR] :: The `stream()` method cannot be used when streaming from a `-video_source` input file. Kindly refer vidgear docs!" ) # None-Type frames will be skipped if frame is None: @@ -394,12 +436,13 @@ def stream(self, frame, rgb_mode=False): def transcode_source(self): """ - Transcodes entire Video Source _(with audio)_ into multi-bitrate streamable assets + Transcodes an entire video file _(with or without audio)_ into chunked-encoded media segments of + streaming formats such as MPEG-DASH and HLS. """ # check if function is called in correct context if not (self.__video_source): raise RuntimeError( - "[StreamGear:ERROR] :: `transcode_source()` function cannot be used without a valid `-video_source` input. Kindly refer vidgear docs!" + "[StreamGear:ERROR] :: The `transcode_source()` method cannot be used without a valid `-video_source` input. Kindly refer vidgear docs!" ) # assign height, width and framerate self.__inputheight = int(self.__aspect_source[1]) @@ -410,27 +453,47 @@ def transcode_source(self): def __PreProcess(self, channels=0, rgb=False): """ - Internal method that pre-processes default FFmpeg parameters before beginning pipelining. + Internal method that pre-processes default FFmpeg parameters before starting pipelining. Parameters: channels (int): Number of channels - rgb_mode (boolean): activates RGB mode _(if enabled)_. + rgb (boolean): activates RGB mode _(if enabled)_. """ # turn off initiate flag self.__initiate_stream = False - # initialize parameters + # initialize I/O parameters input_parameters = OrderedDict() output_parameters = OrderedDict() # pre-assign default codec parameters (if not assigned by user). default_codec = "libx264rgb" if rgb else "libx264" - output_parameters["-vcodec"] = self.__params.pop("-vcodec", default_codec) - # enable optimizations and enforce compatibility - output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") - aspect_ratio = Fraction( - self.__inputwidth / self.__inputheight - ).limit_denominator(10) - output_parameters["-aspect"] = ":".join(str(aspect_ratio).split("/")) - # w.r.t selected codec + output_vcodec = self.__params.pop("-vcodec", default_codec) + # enforce default encoder if stream copy specified + # in Real-time Frames Mode + output_parameters["-vcodec"] = ( + default_codec + if output_vcodec == "copy" and not (self.__video_source) + else output_vcodec + ) + # enforce compatibility with stream copy + if output_parameters["-vcodec"] != "copy": + # NOTE: these parameters only supported when stream copy not defined + output_parameters["-vf"] = self.__params.pop("-vf", "format=yuv420p") + # Non-essential `-aspect` parameter is removed from the default pipeline. + else: + # log warnings if stream copy specified in Real-time Frames Mode + not (self.__video_source) and logger.error( + "Stream copy is not compatible with Real-time Frames Mode as it requires re-encoding of incoming frames. Discarding the `-vcodec copy` parameter!" + ) + # log warnings for these parameters + self.__params.pop("-vf", False) and logger.warning( + "Filtering and stream copy cannot be used together. Discarding specified `-vf` parameter!" + ) + self.__params.pop("-aspect", False) and logger.warning( + "Overriding aspect ratio with stream copy may produce invalid files. Discarding specified `-aspect` parameter!" + ) + + # enable optimizations w.r.t selected codec + ### OPTIMIZATION-1 ### if output_parameters["-vcodec"] in [ "libx264", "libx264rgb", @@ -438,33 +501,36 @@ def __PreProcess(self, channels=0, rgb=False): "libvpx-vp9", ]: output_parameters["-crf"] = self.__params.pop("-crf", "20") - if output_parameters["-vcodec"] in ["libx264", "libx264rgb"]: + ### OPTIMIZATION-2 ### + if output_parameters["-vcodec"] == "libx264": if not (self.__video_source): output_parameters["-profile:v"] = self.__params.pop( "-profile:v", "high" ) + ### OPTIMIZATION-3 ### + if output_parameters["-vcodec"] in ["libx264", "libx264rgb"]: output_parameters["-tune"] = self.__params.pop("-tune", "zerolatency") output_parameters["-preset"] = self.__params.pop("-preset", "veryfast") + ### OPTIMIZATION-4 ### if output_parameters["-vcodec"] == "libx265": output_parameters["-x265-params"] = self.__params.pop( "-x265-params", "lossless=1" ) + # enable audio (if present) if self.__audio: # validate audio source bitrate = validate_audio(self.__ffmpeg, source=self.__audio) if bitrate: logger.info( - "Detected External Audio Source is valid, and will be used for streams." + "Detected External Audio Source is valid, and will be used for generating streams." ) - # assign audio source output_parameters[ "{}".format( "-core_asource" if isinstance(self.__audio, list) else "-i" ) ] = self.__audio - # assign audio codec output_parameters["-acodec"] = self.__params.pop( "-acodec", "aac" if isinstance(self.__audio, list) else "copy" @@ -474,33 +540,35 @@ def __PreProcess(self, channels=0, rgb=False): ["-map", "1:a:0"] if self.__format == "dash" else [] ) else: + # discard invalid audio logger.warning( "Audio source `{}` is not valid, Skipped!".format(self.__audio) ) + self.__audio = False + # validate input video's audio source if available elif self.__video_source: - # validate audio source bitrate = validate_audio(self.__ffmpeg, source=self.__video_source) if bitrate: - logger.info("Source Audio will be used for streams.") + logger.info("Input Video's audio source will be used for this run.") # assign audio codec output_parameters["-acodec"] = ( "aac" if self.__format == "hls" else "copy" ) output_parameters["a_bitrate"] = bitrate # temporary handler else: - logger.warning( - "No valid audio_source available. Disabling audio for streams!" + logger.info( + "No valid audio source available in the input video. Disabling audio while generating streams." ) else: - logger.warning( - "No valid audio_source provided. Disabling audio for streams!" + logger.info( + "No valid audio source provided. Disabling audio while generating streams." ) # enable audio optimizations based on audio codec if "-acodec" in output_parameters and output_parameters["-acodec"] == "aac": output_parameters["-movflags"] = "+faststart" # set input framerate - if self.__sourceframerate > 0 and not (self.__video_source): + if self.__sourceframerate > 0.0 and not (self.__video_source): # set input framerate self.__logging and logger.debug( "Setting Input framerate: {}".format(self.__sourceframerate) @@ -531,7 +599,7 @@ def __PreProcess(self, channels=0, rgb=False): # check if processing completed successfully assert not ( process_params is None - ), "[StreamGear:ERROR] :: {} stream cannot be initiated!".format( + ), "[StreamGear:ERROR] :: `{}` stream cannot be initiated properly!".format( self.__format.upper() ) # Finally start FFmpef pipline and process everything @@ -547,42 +615,45 @@ def __handle_streams(self, input_params, output_params): """ # handle bit-per-pixels bpp = self.__params.pop("-bpp", 0.1000) - if isinstance(bpp, (float, int)) and bpp > 0.0: - bpp = float(bpp) if (bpp > 0.001) else 0.1000 + if isinstance(bpp, float) and bpp >= 0.001: + bpp = float(bpp) else: - # reset to defaut if invalid + # reset to default if invalid bpp = 0.1000 # log it - self.__logging and logger.debug( + bpp and self.__logging and logger.debug( "Setting bit-per-pixels: {} for this stream.".format(bpp) ) # handle gop - gop = self.__params.pop("-gop", 0) - if isinstance(gop, (int, float)) and gop > 0: + gop = self.__params.pop("-gop", 2 * int(self.__sourceframerate)) + if isinstance(gop, (int, float)) and gop >= 0: gop = int(gop) else: # reset to some recommended value gop = 2 * int(self.__sourceframerate) # log it - self.__logging and logger.debug("Setting GOP: {} for this stream.".format(gop)) + gop and self.__logging and logger.debug( + "Setting GOP: {} for this stream.".format(gop) + ) - # define and map default stream - if self.__format != "hls": - output_params["-map"] = 0 - else: + # define default stream and its mapping + if self.__format == "hls": output_params["-corev0"] = ["-map", "0:v"] if "-acodec" in output_params: output_params["-corea0"] = [ "-map", "{}:a".format(1 if "-core_audio" in output_params else 0), ] - # assign resolution + else: + output_params["-map"] = 0 + + # assign default output resolution if "-s:v:0" in self.__params: # prevent duplicates del self.__params["-s:v:0"] output_params["-s:v:0"] = "{}x{}".format(self.__inputwidth, self.__inputheight) - # assign video-bitrate + # assign default output video-bitrate if "-b:v:0" in self.__params: # prevent duplicates del self.__params["-b:v:0"] @@ -597,12 +668,13 @@ def __handle_streams(self, input_params, output_params): ) + "k" ) - # assign audio-bitrate + + # assign default output audio-bitrate if "-b:a:0" in self.__params: # prevent duplicates del self.__params["-b:a:0"] - # extract audio-bitrate from temporary handler - a_bitrate = output_params.pop("a_bitrate", "") + # extract and assign audio-bitrate from temporary handler + a_bitrate = output_params.pop("a_bitrate", False) if "-acodec" in output_params and a_bitrate: output_params["-b:a:0"] = a_bitrate @@ -610,7 +682,7 @@ def __handle_streams(self, input_params, output_params): streams = self.__params.pop("-streams", {}) output_params = self.__evaluate_streams(streams, output_params, bpp) - # define additional stream optimization parameters + # define additional streams optimization parameters if output_params["-vcodec"] in ["libx264", "libx264rgb"]: if not "-bf" in self.__params: output_params["-bf"] = 1 @@ -618,17 +690,18 @@ def __handle_streams(self, input_params, output_params): output_params["-sc_threshold"] = 0 if not "-keyint_min" in self.__params: output_params["-keyint_min"] = gop - if output_params["-vcodec"] in ["libx264", "libx264rgb", "libvpx-vp9"]: - if not "-g" in self.__params: - output_params["-g"] = gop + if ( + output_params["-vcodec"] in ["libx264", "libx264rgb", "libvpx-vp9"] + and not "-g" in self.__params + ): + output_params["-g"] = gop if output_params["-vcodec"] == "libx265": output_params["-core_x265"] = [ "-x265-params", "keyint={}:min-keyint={}".format(gop, gop), ] - # process given dash/hls stream - processed_params = None + # process given dash/hls stream and return it if self.__format == "dash": processed_params = self.__generate_dash_stream( input_params=input_params, @@ -639,7 +712,6 @@ def __handle_streams(self, input_params, output_params): input_params=input_params, output_params=output_params, ) - return processed_params def __evaluate_streams(self, streams, output_params, bpp): @@ -647,7 +719,7 @@ def __evaluate_streams(self, streams, output_params, bpp): Internal function that Extracts, Evaluates & Validates user-defined streams Parameters: - streams (dict): Indivisual streams formatted as list of dict. + streams (dict): Individual streams formatted as list of dict. output_params (dict): Output FFmpeg parameters """ # temporary streams count variable @@ -655,12 +727,13 @@ def __evaluate_streams(self, streams, output_params, bpp): # check if streams are empty if not streams: - logger.warning("No `-streams` are provided!") + logger.info("No additional `-streams` are provided.") return output_params # check if streams are valid if isinstance(streams, list) and all(isinstance(x, dict) for x in streams): - stream_count = 1 # keep track of streams + # keep track of streams + stream_count = 1 # calculate source aspect-ratio source_aspect_ratio = self.__inputwidth / self.__inputheight # log the process @@ -668,22 +741,25 @@ def __evaluate_streams(self, streams, output_params, bpp): "Processing {} streams.".format(len(streams)) ) # iterate over given streams - for stream in streams: - stream_copy = stream.copy() # make copy - intermediate_dict = {} # handles intermediate stream data as dictionary - + for idx, stream in enumerate(streams): + # log stream processing + self.__logging and logger.debug("Processing Stream: #{}".format(idx)) + # make copy + stream_copy = stream.copy() + # handle intermediate stream data as dictionary + intermediate_dict = {} # define and map stream to intermediate dict - if self.__format != "hls": - intermediate_dict["-core{}".format(stream_count)] = ["-map", "0"] - else: + if self.__format == "hls": intermediate_dict["-corev{}".format(stream_count)] = ["-map", "0:v"] if "-acodec" in output_params: intermediate_dict["-corea{}".format(stream_count)] = [ "-map", "{}:a".format(1 if "-core_audio" in output_params else 0), ] + else: + intermediate_dict["-core{}".format(stream_count)] = ["-map", "0"] - # extract resolution & indivisual dimension of stream + # extract resolution & individual dimension of stream resolution = stream.pop("-resolution", "") dimensions = ( resolution.lower().split("x") @@ -702,7 +778,7 @@ def __evaluate_streams(self, streams, output_params, bpp): ) if int(dimensions[0]) != expected_width: logger.warning( - "Given stream resolution `{}` is not in accordance with the Source Aspect-Ratio. Stream Output may appear Distorted!".format( + "The provided stream resolution '{}' does not align with the source aspect ratio. Output stream may appear distorted!".format( resolution ) ) @@ -711,7 +787,7 @@ def __evaluate_streams(self, streams, output_params, bpp): else: # otherwise log error and skip stream logger.error( - "Missing `-resolution` value, Stream `{}` Skipped!".format( + "Missing `-resolution` value. Invalid stream `{}` Skipped!".format( stream_copy ) ) @@ -740,7 +816,7 @@ def __evaluate_streams(self, streams, output_params, bpp): else: # If everything fails, log and skip the stream! logger.error( - "Unable to determine Video-Bitrate for the stream `{}`, Skipped!".format( + "Unable to determine Video-Bitrate for the stream `{}`. Skipped!".format( stream_copy ) ) @@ -767,9 +843,16 @@ def __evaluate_streams(self, streams, output_params, bpp): stream_copy.clear() # increment to next stream stream_count += 1 + # log stream processing + self.__logging and logger.debug( + "Processed #{} stream successfully.".format(idx) + ) + # store stream count output_params["stream_count"] = stream_count + # log streams processing self.__logging and logger.debug("All streams processed successfully!") else: + # skip and log logger.warning("Invalid type `-streams` skipped!") return output_params @@ -783,8 +866,6 @@ def __generate_hls_stream(self, input_params, output_params): input_params (dict): Input FFmpeg parameters output_params (dict): Output FFmpeg parameters """ - # Check if live-streaming or not? - # validate `hls_segment_type` default_hls_segment_type = self.__params.pop("-hls_segment_type", "mpegts") if isinstance( @@ -792,38 +873,77 @@ def __generate_hls_stream(self, input_params, output_params): ) and default_hls_segment_type.strip() in ["fmp4", "mpegts"]: output_params["-hls_segment_type"] = default_hls_segment_type.strip() else: + # otherwise reset to default + logger.warning("Invalid `-hls_segment_type` value skipped!") output_params["-hls_segment_type"] = "mpegts" - # gather required parameters if self.__livestreaming: - # `hls_list_size` must be greater than 0 + # `hls_list_size` must be greater than or equal to 0 default_hls_list_size = self.__params.pop("-hls_list_size", 6) - if isinstance(default_hls_list_size, int) and default_hls_list_size > 0: + if isinstance(default_hls_list_size, int) and default_hls_list_size >= 0: output_params["-hls_list_size"] = default_hls_list_size else: - # otherwise reset to default + # otherwise reset to default + logger.warning("Invalid `-hls_list_size` value skipped!") output_params["-hls_list_size"] = 6 - # default behaviour - output_params["-hls_init_time"] = self.__params.pop("-hls_init_time", 4) - output_params["-hls_time"] = self.__params.pop("-hls_time", 6) - output_params["-hls_flags"] = self.__params.pop( + # `hls_init_time` must be greater than or equal to 0 + default_hls_init_time = self.__params.pop("-hls_init_time", 4) + if isinstance(default_hls_init_time, int) and default_hls_init_time >= 0: + output_params["-hls_init_time"] = default_hls_init_time + else: + # otherwise reset to default + logger.warning("Invalid `-hls_init_time` value skipped!") + output_params["-hls_init_time"] = 4 + # `hls_time` must be greater than or equal to 0 + default_hls_time = self.__params.pop("-hls_time", 4) + if isinstance(default_hls_time, int) and default_hls_time >= 0: + output_params["-hls_time"] = default_hls_time + else: + # otherwise reset to default + logger.warning("Invalid `-hls_time` value skipped!") + output_params["-hls_time"] = 6 + # `hls_flags` must be string + default_hls_flags = self.__params.pop( "-hls_flags", "delete_segments+discont_start+split_by_time" ) + if isinstance(default_hls_flags, str): + output_params["-hls_flags"] = default_hls_flags + else: + # otherwise reset to default + logger.warning("Invalid `-hls_flags` value skipped!") + output_params["-hls_flags"] = ( + "delete_segments+discont_start+split_by_time" + ) # clean everything at exit? - output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) + remove_at_exit = self.__params.pop("-remove_at_exit", 0) + if isinstance(remove_at_exit, int) and remove_at_exit in [ + 0, + 1, + ]: + output_params["-remove_at_exit"] = remove_at_exit + else: + # otherwise reset to default + logger.warning("Invalid `-remove_at_exit` value skipped!") + output_params["-remove_at_exit"] = 0 else: # enforce "contain all the segments" output_params["-hls_list_size"] = 0 output_params["-hls_playlist_type"] = "vod" # handle base URL for absolute paths - output_params["-hls_base_url"] = self.__params.pop("-hls_base_url", "") + hls_base_url = self.__params.pop("-hls_base_url", "") + if isinstance(hls_base_url, str): + output_params["-hls_base_url"] = hls_base_url + else: + # otherwise reset to default + logger.warning("Invalid `-hls_base_url` value skipped!") + output_params["-hls_base_url"] = "" - # Finally, some hardcoded HLS parameters (Refer FFmpeg docs for more info.) + # Hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-allowed_extensions"] = "ALL" # Handling - # Here filenname will be based on `stream_count` dict parameter that - # would be used to check whether stream is multivariant(>1) or single(0-1) + # Here filename will be based on `stream_count` dict parameter that + # would be used to check whether stream is multi-variant(>1) or single(0-1) segment_template = ( "{}-stream%v-%03d.{}" if output_params["stream_count"] > 1 @@ -833,9 +953,11 @@ def __generate_hls_stream(self, input_params, output_params): os.path.join(os.path.dirname(self.__out_file), "chunk"), "m4s" if output_params["-hls_segment_type"] == "fmp4" else "ts", ) + # Hardcoded HLS parameters (Refer FFmpeg docs for more info.) output_params["-hls_allow_cache"] = 0 # enable hls formatting output_params["-f"] = "hls" + # return HLS params return (input_params, output_params) def __generate_dash_stream(self, input_params, output_params): @@ -850,20 +972,53 @@ def __generate_dash_stream(self, input_params, output_params): # Check if live-streaming or not? if self.__livestreaming: - output_params["-window_size"] = self.__params.pop("-window_size", 5) - output_params["-extra_window_size"] = self.__params.pop( - "-extra_window_size", 5 - ) + # `extra_window_size` must be greater than or equal to 0 + window_size = self.__params.pop("-window_size", 5) + if isinstance(window_size, int) and window_size >= 0: + output_params["-window_size"] = window_size + else: + # otherwise reset to default + logger.warning("Invalid `-window_size` value skipped!") + output_params["-window_size"] = 5 + # `extra_window_size` must be greater than or equal to 0 + extra_window_size = self.__params.pop("-extra_window_size", 5) + if isinstance(extra_window_size, int) and extra_window_size >= 0: + output_params["-extra_window_size"] = window_size + else: + # otherwise reset to default + logger.warning("Invalid `-extra_window_size` value skipped!") + output_params["-extra_window_size"] = 5 # clean everything at exit? - output_params["-remove_at_exit"] = self.__params.pop("-remove_at_exit", 0) - # default behaviour - output_params["-seg_duration"] = self.__params.pop("-seg_duration", 20) - # Disable (0) the use of a SegmentTimline inside a SegmentTemplate. + remove_at_exit = self.__params.pop("-remove_at_exit", 0) + if isinstance(remove_at_exit, int) and remove_at_exit in [ + 0, + 1, + ]: + output_params["-remove_at_exit"] = remove_at_exit + else: + # otherwise reset to default + logger.warning("Invalid `-remove_at_exit` value skipped!") + output_params["-remove_at_exit"] = 0 + # `seg_duration` must be greater than or equal to 0 + seg_duration = self.__params.pop("-seg_duration", 20) + if isinstance(seg_duration, int) and seg_duration >= 0: + output_params["-seg_duration"] = seg_duration + else: + # otherwise reset to default + logger.warning("Invalid `-seg_duration` value skipped!") + output_params["-seg_duration"] = 20 + # Disable (0) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 0 else: - # default behaviour - output_params["-seg_duration"] = self.__params.pop("-seg_duration", 5) - # Enable (1) the use of a SegmentTimline inside a SegmentTemplate. + # `seg_duration` must be greater than or equal to 0 + seg_duration = self.__params.pop("-seg_duration", 5) + if isinstance(seg_duration, int) and seg_duration >= 0: + output_params["-seg_duration"] = seg_duration + else: + # otherwise reset to default + logger.warning("Invalid `-seg_duration` value skipped!") + output_params["-seg_duration"] = 5 + # Enable (1) the use of a SegmentTimeline inside a SegmentTemplate. output_params["-use_timeline"] = 1 # Finally, some hardcoded DASH parameters (Refer FFmpeg docs for more info.) @@ -873,6 +1028,7 @@ def __generate_dash_stream(self, input_params, output_params): ) # enable dash formatting output_params["-f"] = "dash" + # return DASH params return (input_params, output_params) def __Build_n_Execute(self, input_params, output_params): @@ -884,13 +1040,11 @@ def __Build_n_Execute(self, input_params, output_params): output_params (dict): Output FFmpeg parameters """ # handle audio source if present - if "-core_asource" in output_params: - output_params.move_to_end("-core_asource", last=False) - - # finally handle `-i` - if "-i" in output_params: - output_params.move_to_end("-i", last=False) - + "-core_asource" in output_params and output_params.move_to_end( + "-core_asource", last=False + ) + # handle `-i` parameter + "-i" in output_params and output_params.move_to_end("-i", last=False) # copy streams count stream_count = output_params.pop("stream_count", 1) @@ -919,22 +1073,20 @@ def __Build_n_Execute(self, input_params, output_params): ] # log it if enabled - if self.__logging: - logger.debug( - "User-Defined Output parameters: `{}`".format( - " ".join(output_commands) if output_commands else None - ) + self.__logging and logger.debug( + "User-Defined Output parameters: `{}`".format( + " ".join(output_commands) if output_commands else None ) - logger.debug( - "Additional parameters: `{}`".format( - " ".join(stream_commands) if stream_commands else None - ) + ) + self.__logging and logger.debug( + "Additional parameters: `{}`".format( + " ".join(stream_commands) if stream_commands else None ) + ) # build FFmpeg command from parameters ffmpeg_cmd = None - hide_banner = ( - [] if self.__logging else ["-hide_banner"] - ) # ensuring less cluterring if specified + # ensuring less cluttering if silent mode + hide_banner = [] if self.__logging else ["-hide_banner"] # format commands if self.__video_source: ffmpeg_cmd = ( @@ -975,7 +1127,10 @@ def __Build_n_Execute(self, input_params, output_params): return_code = 0 pbar = None sec_prev = 0 - if not self.__logging: + if self.__logging: + self.__process.communicate() + return_code = self.__process.returncode + else: # iterate until stdout runs out while True: # read and process data @@ -983,41 +1138,36 @@ def __Build_n_Execute(self, input_params, output_params): if data: data = data.decode("utf-8") # extract duration and time-left - if pbar is None: - if "Duration:" in data: - sec_duration = extract_time(data) - # initate progress bar - pbar = tqdm( - total=sec_duration, - desc="Processing Frames", - unit="frame", - ) - else: - if "time=" in data: - sec_current = extract_time(data) - # update progress bar - if sec_current: - pbar.update(sec_current - sec_prev) - sec_prev = sec_current + if pbar is None and "Duration:" in data: + # extract time in seconds + sec_duration = extract_time(data) + # initiate progress bar + pbar = tqdm( + total=sec_duration, + desc="Processing Frames", + unit="frame", + ) + elif "time=" in data: + # extract time in seconds + sec_current = extract_time(data) + # update progress bar + if sec_current: + pbar.update(sec_current - sec_prev) + sec_prev = sec_current else: # poll if no data if self.__process.poll() is not None: break return_code = self.__process.poll() - else: - self.__process.communicate() - return_code = self.__process.returncode # close progress bar - if pbar: - pbar.close() + not (pbar is None) and pbar.close() # handle return_code - if return_code: + if return_code != 0: # log and raise error if return_code is `1` logger.error( "StreamGear failed to initiate stream for this video source!" ) - error = sp.CalledProcessError(return_code, ffmpeg_cmd) - raise error + raise sp.CalledProcessError(return_code, ffmpeg_cmd) else: # log if successful logger.critical( @@ -1038,27 +1188,46 @@ def __exit__(self, exc_type, exc_val, exc_tb): """ Handles exit with the `with` statement. See [PEP343 -- The 'with' statement'](https://peps.python.org/pep-0343/). """ - self.terminate() + self.close() + @deprecated( + message="The `terminate()` method will be removed in the next release. Kindly use `close()` method instead." + ) def terminate(self): """ - Safely terminates StreamGear. + !!! warning "[DEPRECATION NOTICE]: This method is now deprecated and will be removed in a future release." + + This function ensures backward compatibility for the `terminate()` method to maintain the API on existing systems. + It achieves this by calling the new `close()` method to terminate various + StreamGear processes. + """ + + self.close() + + def close(self): """ + Safely terminates various StreamGear process. + """ + # log termination + self.__logging and logger.debug("Terminating StreamGear Processes.") + # return if no process was initiated at first place if self.__process is None or not (self.__process.poll() is None): return # close `stdin` output - if self.__process.stdin: - self.__process.stdin.close() - # force terminate if external audio source - if isinstance(self.__audio, list): + self.__process.stdin and self.__process.stdin.close() + # close `stdout` output + self.__process.stdout and self.__process.stdout.close() + # forced termination if specified. + if self.__forced_termination: self.__process.terminate() - # wait if still process is still processing some information + # handle device audio streams + elif self.__audio and isinstance(self.__audio, list): + # send `CTRL_BREAK_EVENT` signal if Windows else `SIGINT` + self.__process.send_signal( + signal.CTRL_BREAK_EVENT if self.__os_windows else signal.SIGINT + ) + # wait if process is still processing self.__process.wait() + # discard process self.__process = None - # log it - logger.critical( - "Transcoding Ended. {} Streaming assets are successfully generated at specified path.".format( - self.__format.upper() - ) - ) diff --git a/vidgear/gears/writegear.py b/vidgear/gears/writegear.py index 895360ca2..2a8b7ee1d 100644 --- a/vidgear/gears/writegear.py +++ b/vidgear/gears/writegear.py @@ -22,6 +22,7 @@ import os import cv2 import time +import signal import platform import pathlib import logging as log @@ -165,9 +166,7 @@ def __init__( # cleans and reformat output parameters self.__output_parameters = { - str(k).strip(): ( - str(v).strip() if not isinstance(v, (list, tuple, int, float)) else v - ) + str(k).strip(): (v.strip() if isinstance(v, str) else v) for k, v in output_params.items() } # log it if specified @@ -761,8 +760,7 @@ def close(self): Safely terminates various WriteGear process. """ # log termination - if self.__logging: - logger.debug("Terminating WriteGear Processes.") + self.__logging and logger.debug("Terminating WriteGear Processes.") # handle termination separately if self.__compression: # when Compression Mode is enabled diff --git a/vidgear/tests/network_tests/test_netgear.py b/vidgear/tests/network_tests/test_netgear.py index 6875dc76a..462056577 100644 --- a/vidgear/tests/network_tests/test_netgear.py +++ b/vidgear/tests/network_tests/test_netgear.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -84,12 +85,9 @@ def test_playback(address, port): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close() - if not (client is None): - client.close() + not (stream is None) and stream.release() + not (server is None) and server.close() + not (client is None) and client.close() @pytest.mark.parametrize("receive_mode", [True, False]) @@ -119,10 +117,8 @@ def test_primary_mode(receive_mode): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (conn is None): - conn.close() + not (stream is None) and stream.stop() + not (conn is None) and conn.close() @pytest.mark.parametrize( @@ -170,12 +166,9 @@ def test_patterns(pattern): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -242,23 +235,22 @@ def test_compression(options_server): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.stop() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) test_data_class = [ - (0, 1, tempfile.gettempdir(), True), - (0, 1, ["invalid"], True), + (1, 1, tempfile.gettempdir(), True), + (1, 2, ["invalid"], True), ( 1, 2, - os.path.abspath(os.sep) - if platform.system() == "Linux" - else "unknown://invalid.com/", + ( + os.path.abspath(os.sep) + if platform.system() == "Linux" + else "unknown://invalid.com/" + ), False, ), ] @@ -276,6 +268,7 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer "secure_mode": security_mech, "custom_cert_location": custom_cert_location, "overwrite_cert": overwrite_cert, + "jpeg_compression": False, } # initialize frame_server = None @@ -286,8 +279,14 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer # open stream stream = cv2.VideoCapture(return_testvideo_path()) # define params - server = NetGear(pattern=pattern, logging=True, **options) - client = NetGear(pattern=pattern, receive_mode=True, logging=True, **options) + server = NetGear(address="127.0.0.1", pattern=pattern, logging=True, **options) + client = NetGear( + address="127.0.0.1", + pattern=pattern, + receive_mode=True, + logging=True, + **options + ) # select random input frame from stream i = 0 while i < random.randint(10, 100): @@ -307,12 +306,9 @@ def test_secure_mode(pattern, security_mech, custom_cert_location, overwrite_cer pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -421,12 +417,9 @@ def test_bidirectional_mode(pattern, target_data, options): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (server is None): - server.close(kill=True) - if not (client is None): - client.close(kill=True) + not (stream is None) and stream.stop() + not (server is None) and server.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -510,25 +503,25 @@ def test_multiserver_mode(pattern, options): # send frame from Server-1 to client and save it in dict server_1.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame # send frame from Server-2 to client and save it in dict server_2.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame # send frame from Server-3 to client and save it in dict server_3.send(frame_server) unique_address, frame = client.recv( - return_data="data" - if "bidirectional_mode" in options and pattern == 1 - else "", + return_data=( + "data" if "bidirectional_mode" in options and pattern == 1 else "" + ), ) client_frame_dict[unique_address] = frame @@ -543,16 +536,11 @@ def test_multiserver_mode(pattern, options): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server_1 is None): - server_1.close() - if not (server_2 is None): - server_2.close() - if not (server_3 is None): - server_3.close() - if not (client is None): - client.close() + not (stream is None) and stream.release() + not (server_1 is None) and server_1.close(kill=True) + not (server_2 is None) and server_2.close(kill=True) + not (server_3 is None) and server_3.close(kill=True) + not (client is None) and client.close(kill=True) @pytest.mark.parametrize("pattern", [0, 1]) @@ -628,16 +616,11 @@ def test_multiclient_mode(pattern): pytest.fail(str(e)) finally: # clean resources - if not (stream is None): - stream.stop() - if not (server is None): - server.close() - if not (client_1 is None): - client_1.close() - if not (client_2 is None): - client_1.close() - if not (client_3 is None): - client_1.close() + not (stream is None) and stream.stop() + not (server is None) and server.close(kill=True) + not (client_1 is None) and client_1.close(kill=True) + not (client_2 is None) and client_2.close(kill=True) + not (client_3 is None) and client_3.close(kill=True) @pytest.mark.parametrize( @@ -657,7 +640,7 @@ def test_multiclient_mode(pattern): {"subscriber_timeout": 4}, ], ) -def test_client_reliablity(options): +def test_client_reliability(options): """ Testing validation function of NetGear API """ @@ -684,8 +667,7 @@ def test_client_reliablity(options): logger.exception(str(e)) finally: # clean resources - if not (client is None): - client.close() + not (client is None) and client.close(kill=True) @pytest.mark.parametrize( @@ -713,7 +695,7 @@ def test_client_reliablity(options): }, ], ) -def test_server_reliablity(options): +def test_server_reliability(options): """ Testing validation function of NetGear API """ @@ -746,10 +728,8 @@ def test_server_reliablity(options): logger.exception(str(e)) finally: # clean resources - if not (stream is None): - stream.release() - if not (server is None): - server.close() + not (stream is None) and stream.release() + not (server is None) and server.close(kill=True) @pytest.mark.parametrize( diff --git a/vidgear/tests/streamer_tests/test_IO_rtf.py b/vidgear/tests/streamer_tests/test_IO_rtf.py index 187af1caf..a9afb08f7 100644 --- a/vidgear/tests/streamer_tests/test_IO_rtf.py +++ b/vidgear/tests/streamer_tests/test_IO_rtf.py @@ -17,10 +17,10 @@ limitations under the License. =============================================== """ -# import the necessary packages -import numpy as np +# import the necessary packages import pytest +import numpy as np from vidgear.gears import StreamGear @@ -39,13 +39,13 @@ def test_failedchannels(size): streamer = StreamGear("output.mpd", logging=True) streamer.stream(input_data_ch1) streamer.stream(input_data_ch3) - streamer.terminate() + streamer.close() else: random_data = np.random.random(size=size) * 255 input_data = random_data.astype(np.uint8) streamer = StreamGear("output.mpd", logging=True) streamer.stream(input_data) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=ValueError) @@ -66,7 +66,7 @@ def test_fail_framedimension(): streamer.stream(None) streamer.stream(input_data1) streamer.stream(input_data2) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=RuntimeError) @@ -77,11 +77,11 @@ def test_method_call_rtf(): stream_params = {"-video_source": 1234} # for CI testing only streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=ValueError) -@pytest.mark.parametrize("format", ["dash", "hls"]) +@pytest.mark.parametrize("format", ["dash", "hls", "invalid"]) def test_invalid_params_rtf(format): """ Invalid parameter Failure Test - Made to fail by calling invalid parameters @@ -91,7 +91,7 @@ def test_invalid_params_rtf(format): random_data = np.random.random(size=(480, 640, 3)) * 255 input_data = random_data.astype(np.uint8) - stream_params = {"-vcodec": "unknown"} + stream_params = {"-vcodec": "unknown", "-livestream": "invalid"} streamer = StreamGear( output="output{}".format(".mpd" if format == "dash" else ".m3u8"), format=format, @@ -100,4 +100,4 @@ def test_invalid_params_rtf(format): ) streamer.stream(input_data) streamer.stream(input_data) - streamer.terminate() + streamer.close() diff --git a/vidgear/tests/streamer_tests/test_IO_ss.py b/vidgear/tests/streamer_tests/test_IO_ss.py index 217ad6fe3..601f9aab6 100644 --- a/vidgear/tests/streamer_tests/test_IO_ss.py +++ b/vidgear/tests/streamer_tests/test_IO_ss.py @@ -17,10 +17,9 @@ limitations under the License. =============================================== """ -# import the necessary packages +# import the necessary packages import os -import numpy as np import pytest import tempfile import subprocess @@ -47,7 +46,7 @@ def test_failedextension(output): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output=output, logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() def test_failedextensionsource(): @@ -59,7 +58,7 @@ def test_failedextensionsource(): stream_params = {"-video_source": "garbage.garbage"} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() @pytest.mark.parametrize( @@ -76,7 +75,11 @@ def test_paths_ss(path, format): """ streamer = None try: - stream_params = {"-video_source": return_testvideo_path()} + stream_params = { + "-video_source": return_testvideo_path(), + "-ffmpeg_download_path": 12345, + "-livestream": "invalid", + } streamer = StreamGear(output=path, format=format, logging=True, **stream_params) except Exception as e: if isinstance(e, ValueError): @@ -85,7 +88,7 @@ def test_paths_ss(path, format): pytest.fail(str(e)) finally: if not streamer is None: - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=RuntimeError) @@ -96,7 +99,7 @@ def test_method_call_ss(): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.stream("garbage.garbage") - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=(AttributeError, RuntimeError)) @@ -107,7 +110,7 @@ def test_method_call_ss(): stream_params = {"-video_source": return_testvideo_path()} streamer = StreamGear(output="output.mpd", logging=True, **stream_params) streamer.stream("garbage.garbage") - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=subprocess.CalledProcessError) @@ -124,4 +127,4 @@ def test_invalid_params_ss(format): **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() diff --git a/vidgear/tests/streamer_tests/test_init.py b/vidgear/tests/streamer_tests/test_init.py index 06cec587a..4eeb1c030 100644 --- a/vidgear/tests/streamer_tests/test_init.py +++ b/vidgear/tests/streamer_tests/test_init.py @@ -63,7 +63,7 @@ def test_custom_ffmpeg(c_ffmpeg): Testing custom FFmpeg for StreamGear """ streamer = StreamGear(output="output.mpd", custom_ffmpeg=c_ffmpeg, logging=True) - streamer.terminate() + streamer.close() @pytest.mark.xfail(raises=(AssertionError, ValueError)) @@ -73,7 +73,7 @@ def test_formats(format): Testing different formats for StreamGear """ streamer = StreamGear(output="output.mpd", format=format, logging=True) - streamer.terminate() + streamer.close() @pytest.mark.parametrize( @@ -96,7 +96,7 @@ def test_outputs(output): logging=True, **stream_params ) - streamer.terminate() + streamer.close() except Exception as e: if output is None or output.endswith("m3u8"): pytest.xfail(str(e)) diff --git a/vidgear/tests/streamer_tests/test_streamgear_modes.py b/vidgear/tests/streamer_tests/test_streamgear_modes.py index 2aaa3c079..b979cc600 100644 --- a/vidgear/tests/streamer_tests/test_streamgear_modes.py +++ b/vidgear/tests/streamer_tests/test_streamgear_modes.py @@ -17,8 +17,8 @@ limitations under the License. =============================================== """ -# import the necessary packages +# import the necessary packages import os import cv2 import queue @@ -220,6 +220,10 @@ def test_ss_stream(format): try: stream_params = { "-video_source": return_testvideo_path(), + "-vcodec": "copy", + "-aspect": "4:3", + "-vf": "format=yuv420p", + "-streams": "invalid", "-clear_prev_assets": True, } if format == "hls": @@ -235,7 +239,7 @@ def test_ss_stream(format): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: @@ -257,13 +261,13 @@ def test_ss_livestream(format): stream_params = { "-video_source": return_testvideo_path(), "-livestream": True, + "-clear_prev_assets": "invalid", "-remove_at_exit": 1, } - streamer = StreamGear( + with StreamGear( output=assets_file_path, format=format, logging=True, **stream_params - ) - streamer.transcode_source() - streamer.terminate() + ) as streamer: + streamer.transcode_source() except Exception as e: pytest.fail(str(e)) @@ -297,18 +301,19 @@ def test_rtf_stream(conversion, format): + os.sep } ) - streamer = StreamGear(output=assets_file_path, format=format, **stream_params) - while True: - frame = stream.read() - # check if frame is None - if frame is None: - break - if conversion == "COLOR_BGR2RGBA": - streamer.stream(frame, rgb_mode=True) - else: - streamer.stream(frame) - stream.stop() - streamer.terminate() + with StreamGear( + output=assets_file_path, format=format, **stream_params + ) as streamer: + while True: + frame = stream.read() + # check if frame is None + if frame is None: + break + if conversion == "COLOR_BGR2RGBA": + streamer.stream(frame, rgb_mode=True) + else: + streamer.stream(frame) + stream.stop() asset_file = [ os.path.join(assets_file_path, f) for f in os.listdir(assets_file_path) @@ -337,6 +342,7 @@ def test_rtf_livestream(format): stream = CamGear(source=return_testvideo_path(), **options).start() stream_params = { "-livestream": True, + "-enable_force_termination": True, } streamer = StreamGear(output=assets_file_path, format=format, **stream_params) while True: @@ -367,6 +373,9 @@ def test_input_framerate_rtf(format): stream_params = { "-clear_prev_assets": True, "-input_framerate": test_framerate, + "-vcodec": "copy", + "-vf": "format=yuv420p", + "-aspect": "4:3", } if format == "hls": stream_params.update( @@ -386,7 +395,7 @@ def test_input_framerate_rtf(format): break streamer.stream(frame) stream.release() - streamer.terminate() + streamer.close() if format == "dash": meta_data = extract_meta_mpd(assets_file_path) assert meta_data and len(meta_data) > 0, "Test Failed!" @@ -416,6 +425,13 @@ def test_input_framerate_rtf(format): "-bpp": 0.2000, "-gop": 125, "-vcodec": "libx265", + "-hls_segment_type": "invalid", + "-hls_init_time": -223.2, + "-hls_flags": 94884, + "-hls_list_size": -4.3, + "-hls_time": -4758.56, + "-remove_at_exit": 4.56, + "-livestream": True, }, "hls", ), @@ -427,6 +443,7 @@ def test_input_framerate_rtf(format): "-s:v:0": "unknown", "-b:v:0": "unknown", "-b:a:0": "unknown", + "-enable_force_termination": "invalid", }, "hls", ), @@ -435,13 +452,21 @@ def test_input_framerate_rtf(format): "-clear_prev_assets": True, "-bpp": 0.2000, "-gop": 125, + "-audio": ["invalid"], "-vcodec": "libx265", + "-window_size": -456.4, + "-extra_window_size": -354.45, + "-remove_at_exit": -34.34, + "-seg_duration": -334.23, + "-livestream": True, }, "dash", ), ( { "-clear_prev_assets": True, + "-seg_duration": -346.67, + "-audio": "inv/\lid", "-bpp": "unknown", "-gop": "unknown", "-s:v:0": "unknown", @@ -480,11 +505,13 @@ def test_params(stream_params, format): break streamer.stream(frame) stream.release() - streamer.terminate() - if format == "dash": - assert check_valid_mpd(assets_file_path), "Test Failed!" - else: - assert extract_meta_video(assets_file_path), "Test Failed!" + streamer.close() + livestream = stream_params.pop("-livestream", False) + if not (livestream): + if format == "dash": + assert check_valid_mpd(assets_file_path), "Test Failed!" + else: + assert extract_meta_video(assets_file_path), "Test Failed!" except Exception as e: pytest.fail(str(e)) @@ -564,7 +591,7 @@ def test_audio(stream_params, format): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": assert check_valid_mpd(assets_file_path), "Test Failed!" else: @@ -698,7 +725,7 @@ def test_audio(stream_params, format): ) def test_multistreams(format, stream_params): """ - Testing Support for additional Secondary Streams of variable bitrates or spatial resolutions. + Testing Support for additional Secondary Streams of variable bitrate or spatial resolutions. """ assets_file_path = os.path.join( return_assets_path(False if format == "dash" else True), @@ -712,7 +739,7 @@ def test_multistreams(format, stream_params): output=assets_file_path, format=format, logging=True, **stream_params ) streamer.transcode_source() - streamer.terminate() + streamer.close() if format == "dash": metadata = extract_meta_mpd(assets_file_path) meta_videos = [x for x in metadata if x["mime_type"].startswith("video")] diff --git a/vidgear/tests/test_helper.py b/vidgear/tests/test_helper.py index 73882b9e2..1862ea5bd 100644 --- a/vidgear/tests/test_helper.py +++ b/vidgear/tests/test_helper.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -437,7 +438,7 @@ def test_create_blank_frame(frame, text): @pytest.mark.parametrize( "value, result", [ - ("Duration: 00:00:08.44, start: 0.000000, bitrate: 804 kb/s", 8), + ("Duration: 00:00:08.44, start: 0.000000, bitrate: 804 kb/s", 8.44), ("Duration: 00:07:08 , start: 0.000000, bitrate: 804 kb/s", 428), ("", False), ], @@ -537,7 +538,7 @@ def test_delete_ext_safe(ext, result): } streamer = StreamGear(output=mpd_file_path, **stream_params) streamer.transcode_source() - streamer.terminate() + streamer.close() assert check_valid_mpd(mpd_file_path) delete_ext_safe(path, ext, logging=True) assert not os.listdir(path), "`delete_ext_safe` Test failed!" diff --git a/vidgear/tests/writer_tests/test_compression_mode.py b/vidgear/tests/writer_tests/test_compression_mode.py index 262877ded..108b6d81c 100644 --- a/vidgear/tests/writer_tests/test_compression_mode.py +++ b/vidgear/tests/writer_tests/test_compression_mode.py @@ -17,6 +17,7 @@ limitations under the License. ================================================ """ + # import the necessary packages import os @@ -297,14 +298,13 @@ def test_WriteGear_compression(f_name, c_ffmpeg, output_params, result): """ try: stream = cv2.VideoCapture(return_testvideo_path()) # Open stream - writer = WriteGear(output=f_name, compression_mode=True, **output_params) - while True: - (grabbed, frame) = stream.read() - if not grabbed: - break - writer.write(frame) - stream.release() - writer.close() + with WriteGear(output=f_name, compression_mode=True, **output_params) as writer: + while True: + (grabbed, frame) = stream.read() + if not grabbed: + break + writer.write(frame) + stream.release() remove_file_safe(f_name) except Exception as e: if result: @@ -332,9 +332,11 @@ def test_WriteGear_compression(f_name, c_ffmpeg, output_params, result): ( ["wrong_input", "invalid_flag", "break_things"], True, - {"-ffmpeg_download_path": 53} - if (platform.system() == "Windows") - else {"-disable_force_termination": "OK"}, + ( + {"-ffmpeg_download_path": 53} + if (platform.system() == "Windows") + else {"-disable_force_termination": "OK"} + ), ), ( "wrong_input", diff --git a/vidgear/tests/writer_tests/test_non_compression_mode.py b/vidgear/tests/writer_tests/test_non_compression_mode.py index c9c45bfd2..b9f612610 100644 --- a/vidgear/tests/writer_tests/test_non_compression_mode.py +++ b/vidgear/tests/writer_tests/test_non_compression_mode.py @@ -17,6 +17,7 @@ limitations under the License. =============================================== """ + # import the necessary packages import os @@ -152,9 +153,11 @@ def test_write(conversion): ), ( "appsrc ! videoconvert ! avenc_mpeg4 bitrate=100000 ! mp4mux ! filesink location=foo.mp4", - {"-gst_pipeline_mode": True} - if platform.system() == "Linux" - else {"-gst_pipeline_mode": "invalid"}, + ( + {"-gst_pipeline_mode": True} + if platform.system() == "Linux" + else {"-gst_pipeline_mode": "invalid"} + ), True if platform.system() == "Linux" else False, ), ] @@ -167,16 +170,15 @@ def test_WriteGear_compression(f_name, output_params, result): """ try: stream = cv2.VideoCapture(return_testvideo_path()) - writer = WriteGear( + with WriteGear( output=f_name, compression_mode=False, logging=True, **output_params - ) - while True: - (grabbed, frame) = stream.read() - if not grabbed: - break - writer.write(frame) - stream.release() - writer.close() + ) as writer: + while True: + (grabbed, frame) = stream.read() + if not grabbed: + break + writer.write(frame) + stream.release() remove_file_safe( "foo.html" if "-gst_pipeline_mode" in output_params