Skip to content

Commit

Permalink
Add HLS and YouTube Live support. (#60)
Browse files Browse the repository at this point in the history
Add support for HTTP Live Streaming (HLS) and YouTube Live videos (which is just HLS under the hood).

Also do some general cleanup:

remove unused imports
clean up motion detection documentation
support optional extra dependency installation
move from python 3.7 -> python 3.9 (oldest version that is not out of service)
---------

Co-authored-by: Auto-format Bot <[email protected]>
  • Loading branch information
tyler-romero and Auto-format Bot authored Nov 19, 2024
1 parent 09e7f98 commit d86b2d2
Show file tree
Hide file tree
Showing 11 changed files with 468 additions and 164 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/auto-format.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
with:
src: "src"
options: "--verbose --line-length 120"
- name: Check if any files were modikfied
- name: Check if any files were modified
id: git-check
run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi)
- name: Push changes if needed
Expand All @@ -34,4 +34,3 @@ jobs:
git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git commit -am "Automatically reformatting code with black and isort"
git push
8 changes: 3 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,9 @@ jobs:
fail-fast: false
matrix:
python-version: [
# "3.7",
"3.8",
#"3.9",
"3.10",
#"3.11",
"3.9",
"3.10",
"3.11",
]
steps:
- name: get code
Expand Down
127 changes: 92 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@ FrameGrab is an open-source Python library designed to make it easy to grab fram
FrameGrab also provides basic motion detection functionality. FrameGrab requires Python 3.7 or higher.

## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Examples](#examples)
- [Contributing](#contributing)
- [License](#license)

- [FrameGrab by Groundlight](#framegrab-by-groundlight)
- [A user-friendly library for grabbing images from cameras or streams](#a-user-friendly-library-for-grabbing-images-from-cameras-or-streams)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Optional Dependencies](#optional-dependencies)
- [Usage](#usage)
- [Command line interface (CLI)](#command-line-interface-cli)
- [Frame Grabbing](#frame-grabbing)
- [Configurations](#configurations)
- [Autodiscovery](#autodiscovery)
- [RTSP Discovery](#rtsp-discovery)
- [Motion Detection](#motion-detection)
- [Examples](#examples)
- [Generic USB](#generic-usb)
- [YouTube Live](#youtube-live)
- [Contributing](#contributing)
- [License](#license)

## Installation

Expand All @@ -21,11 +34,27 @@ pip install framegrab
```

## Optional Dependencies
Certain camera types have additional dependencies that must be installed separately. If you don't intend to use these camera types, you don't need to install these extra packages.
Certain camera types have additional dependencies that must be installed separately. If you don't intend to use these camera types, you don't need to install these extra packages.

- To use a Basler USB or GigE camera, you must separately install the `pypylon` package.
- To use Intel RealSense cameras, you must install `pyrealsense2`.
- To use a Raspberry Pi "CSI2" camera (connected with a ribbon cable), you must install the `picamera2` library. See install instructions at the [picamera2 github repository](https://github.com/raspberrypi/picamera2).
- To use a YouTube Live stream, you must install `streamlink`.

We provide optional extras to install these dependencies. For example, to install the Basler camera dependencies, run:
```
pip install framegrab[basler]
```

To install YouTube Live stream dependencies, run:
```
pip install framegrab[youtube]
```

To install all optional dependencies, run:
```
pip install framegrab[all]
```


## Usage
Expand All @@ -45,7 +74,7 @@ lists the sub-commands, including `autodiscover` and `preview`.
Frame Grabbers are defined by a configuration dict which is usually stored as YAML. The configuration combines the camera type, the camera ID, and the camera options. The configuration is passed to the `FrameGrabber.create_grabber` method to create a grabber object. The grabber object can then be used to grab frames from the camera.


`config` can contain many details and settings about your camera, but only `input_type` is required. Available `input_type` options are: `generic_usb`, `rtsp`, `realsense`, `basler`, and `rpi_csi2`.
`config` can contain many details and settings about your camera, but only `input_type` is required. Available `input_type` options are: `generic_usb`, `rtsp`, `realsense`, `basler`, `rpi_csi2`, `hls`, and `youtube_live`.

Here's an example of a single USB camera configured with several options:
```python
Expand Down Expand Up @@ -92,13 +121,13 @@ When you are done with the camera, release the resource by running:
grabber.release()
```

You might have several cameras that you want to use in the same application. In this case, you can load the configurations from a yaml file and use `FrameGrabber.create_grabbers`. Note that currently only a single Raspberry Pi CSI2 camera is supported, but these cameras can be used in conjunction with other types of cameras.
You might have several cameras that you want to use in the same application. In this case, you can load the configurations from a yaml file and use `FrameGrabber.create_grabbers`. Note that currently only a single Raspberry Pi CSI2 camera is supported, but these cameras can be used in conjunction with other types of cameras.

If you have multiple cameras of the same type plugged in, it's recommended that you include serial numbers in the configurations; this ensures that each configuration is paired with the correct camera. If you don't provide serial numbers in your configurations, configurations will be paired with cameras in a sequential manner.

Below is a sample yaml file containing configurations for three different cameras.
```yaml
image_sources:
image_sources:
- name: On Robot Arm
input_type: basler
id:
Expand Down Expand Up @@ -139,28 +168,29 @@ for grabber in grabbers.values():
```
### Configurations
The table below shows all available configurations and the cameras to which they apply.
| Configuration Name | Example | Generic USB | RTSP | Basler | Realsense | Raspberry Pi CSI2 |
|----------------------------|-----------------|------------|-----------|-----------|-----------|-----------|
| name | On Robot Arm | optional | optional | optional | optional | optional |
| input_type | generic_usb | required | required | required | required | required |
| id.serial_number | 23458234 | optional | - | optional | optional | - |
| id.rtsp_url | rtsp://… | - | required | - | - | - |
| options.resolution.height | 480 | optional | - | - | optional | - |
| options.resolution.width | 640 | optional | - | - | optional | - |
| options.zoom.digital | 1.3 | optional | optional | optional | optional | optional |
| options.crop.pixels.top | 100 | optional | optional | optional | optional | optional |
| options.crop.pixels.bottom | 400 | optional | optional | optional | optional | optional |
| options.crop.pixels.left | 100 | optional | optional | optional | optional | optional |
| options.crop.pixels.right | 400 | optional | optional | optional | optional | optional |
| options.crop.relative.top | 0.1 | optional | optional | optional | optional | optional |
| options.crop.relative.bottom | 0.9 | optional | optional | optional | optional | optional |
| options.crop.relative.left | 0.1 | optional | optional | optional | optional | optional |
| options.crop.relative.right | 0.9 | optional | optional | optional | optional | optional |
| options.depth.side_by_side | 1 | - | - | - | optional | - |
| options.num_90_deg_rotations | 2 | optional | optional | optional | optional | optional |
| options.keep_connection_open | True | - | optional | - | - | - |
| options.max_fps | 30 | - | optional | - | - | - |
| Configuration Name | Example | Generic USB | RTSP | Basler | Realsense | Raspberry Pi CSI2 | HLS | YouTube Live |
|----------------------------|-----------------|------------|-----------|-----------|-----------|-----------|-----------|-----------|
| name | On Robot Arm | optional | optional | optional | optional | optional | optional | optional |
| input_type | generic_usb | required | required | required | required | required | required | required |
| id.serial_number | 23458234 | optional | - | optional | optional | - | - | - |
| id.rtsp_url | rtsp://… | - | required | - | - | - | - | - |
| id.hls_url | https://.../*.m3u8 | - | - | - | - | - | required | - |
| id.youtube_url | https://www.youtube.com/watch?v=... | - | - | - | - | - | - | required |
| options.resolution.height | 480 | optional | - | - | optional | - | - | - |
| options.resolution.width | 640 | optional | - | - | optional | - | - | - |
| options.zoom.digital | 1.3 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.pixels.top | 100 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.pixels.bottom | 400 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.pixels.left | 100 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.pixels.right | 400 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.relative.top | 0.1 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.relative.bottom | 0.9 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.relative.left | 0.1 | optional | optional | optional | optional | optional | optional | optional |
| options.crop.relative.right | 0.9 | optional | optional | optional | optional | optional | optional | optional |
| options.depth.side_by_side | 1 | - | - | - | optional | - | - | - |
| options.num_90_deg_rotations | 2 | optional | optional | optional | optional | optional | optional | optional |
| options.keep_connection_open | True | - | optional | - | - | - | optional | optional |
| options.max_fps | 30 | - | optional | - | - | - | - | - |
Expand All @@ -185,7 +215,7 @@ RTSP cameras with support for ONVIF can be discovered on your local network in t

```python
from framegrab import RTSPDiscovery, ONVIFDeviceInfo
devices = RTSPDiscovery.discover_onvif_devices()
```

Expand All @@ -194,7 +224,7 @@ The `discover_onvif_devices()` will provide a list of devices that it finds in t
- off: No discovery.
- ip_only: Only discover the IP address of the camera.
- light: Only try first two usernames and passwords ("admin:admin" and no username/password).
- complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between.
- complete_fast: Try the entire DEFAULT_CREDENTIALS without delays in between.
- complete_slow: Try the entire DEFAULT_CREDENTIALS with a delay of 1 seconds in between.


Expand Down Expand Up @@ -240,6 +270,7 @@ if m.motion_detected(frame):

## Examples

### Generic USB
Here's an example of using the FrameGrab library to continuously capture frames and detect motion from a video stream:

```python
Expand All @@ -263,6 +294,34 @@ while True:
print("Motion detected!")
```

### YouTube Live
Here's an example of using FrameGrab to capture frames from a YouTube Live stream:

```python
from framegrab import FrameGrabber
import cv2
config = {
'input_type': 'youtube_live',
'id': {
'youtube_url': 'https://www.youtube.com/watch?v=your_video_id'
}
}
grabber = FrameGrabber.create_grabber(config)
frame = grabber.grab()
if frame is None:
raise Exception("No frame captured")
# Process the frame as needed
# For example, display it using cv2.imshow()
# For example, save it to a file
cv2.imwrite('youtube_frame.jpg', frame)
grabber.release()
```

## Contributing

We welcome contributions to FrameGrab! If you would like to contribute, please follow these steps:
Expand All @@ -275,5 +334,3 @@ We welcome contributions to FrameGrab! If you would like to contribute, please f
## License

FrameGrab is released under the MIT License. For more information, please refer to the [LICENSE.txt](https://github.com/groundlight/framegrab/blob/main/LICENSE.txt) file.


23 changes: 17 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "framegrab"
version = "0.7.0"
version = "0.8.0"
description = "Easily grab frames from cameras or streams"
authors = ["Groundlight <[email protected]>"]
license = "MIT"
Expand All @@ -9,19 +9,30 @@ homepage = "https://www.groundlight.ai/"
repository = "https://github.com/groundlight/framegrab"

[tool.poetry.dependencies]
python = "^3.7"
python = "^3.9"
opencv-python = "^4.4.0.46"
pyyaml = "^6.0.1"
pyyaml = "^6.0.2"
imgcat = "^0.5.0"
click = "^8.1.6"
ascii-magic = "^2.3.0"
wsdiscovery = "^2.0.0"
onvif-zeep = "^0.2.12"
pydantic = "^2.5.3"
pydantic = "^2.9.2"
pypylon = { version = ">=3.0.0", optional = true }
pyrealsense2 = { version = "^2.55.1.6486", optional = true }
picamera2 = { version = ">=0.3.21", optional = true }
streamlink = { version = "^7.0.0", optional = true }

[tool.poetry.extras]
basler = ["pypylon"]
realsense = ["pyrealsense2"]
raspberrypi = ["picamera2"]
youtube = ["streamlink"]
all = ["pypylon", "pyrealsense2", "picamera2", "streamlink"]

[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
pytest = "^7.0.1"
black = "^24.10.0"
pytest = "^8.3.3"

[build-system]
requires = ["poetry-core"]
Expand Down
10 changes: 7 additions & 3 deletions sample_scripts/sample_config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
image_sources:
- name: Front Door
input_type: generic_usb
options:
options:
zoom:
digital: 1.5
- name: Conference Room
input_type: rtsp
id:
id:
rtsp_url: rtsp://admin:[email protected]/cam/realmonitor?channel=1&subtype=0
options:
crop:
Expand All @@ -17,8 +17,12 @@ image_sources:
right: .9
- name: Workshop
input_type: basler
id:
id:
serial_number: 12345678
options:
basler:
ExposureTime: 60000
- name: NamibiaCam Live stream at the Okaukuejo waterhole in Etosha National Park
input_type: youtube_live
id:
youtube_url: https://www.youtube.com/watch?v=DAmFZj1y_a0
7 changes: 4 additions & 3 deletions src/framegrab/cli/autodiscover.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

import click
import yaml
from imgcat import imgcat

from framegrab import FrameGrabber
from framegrab.cli.clitools import (
PREVIEW_COMMAND_CHOICES,
PREVIEW_RTSP_COMMAND_CHOICES,
preview_image,
)
from framegrab.rtsp_discovery import AutodiscoverMode


@click.command()
Expand Down Expand Up @@ -47,7 +45,10 @@ def autodiscover(preview: str, rtsp_discover_mode: str = "off"):
click.echo(f"Failed to grab sample frame from {camera_name}.", err=True)
continue

click.echo(f"Grabbed sample frame from {camera_name} with shape {frame.shape}", err=True)
click.echo(
f"Grabbed sample frame from {camera_name} with shape {frame.shape}",
err=True,
)
click.echo(grabber.config, err=True)
preview_image(frame, camera_name, preview)

Expand Down
5 changes: 0 additions & 5 deletions src/framegrab/cli/preview.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import shutil
import traceback

import ascii_magic
import click
import cv2
import yaml
from imgcat import imgcat
from PIL import Image

from framegrab import FrameGrabber, preview_image

Expand Down
Loading

0 comments on commit d86b2d2

Please sign in to comment.