Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add WebRTC stream #84

Merged
merged 60 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f0122b1
feat: Add basic USB-Camera support
mryel00 Jan 29, 2023
d7cedac
refactor: refactor camera code
mryel00 Apr 3, 2024
b136f95
Merge branch 'expand_camera_control_ability' into add_usbcam_support
mryel00 Apr 4, 2024
84ff1f4
fix: fix multiple things
mryel00 Apr 4, 2024
6e59d1f
chore: adjust variables for better code highlighting
mryel00 Apr 4, 2024
69fc5b2
refactor: refactor camera code
mryel00 Apr 4, 2024
aa9c5da
chore: rearrange cli arguments
mryel00 Apr 4, 2024
6cacb33
fix: fix crash
mryel00 Apr 5, 2024
b485dab
fix: fix typo
mryel00 Apr 5, 2024
1c7a80f
Merge branch 'main' into add_usbcam_support
mryel00 Jun 14, 2024
1726a12
docs: update README.md with new cli parameter
mryel00 Jun 14, 2024
c4143ea
test: fix tests with new methods
mryel00 Jul 7, 2024
378c343
chore: remove empty file
mryel00 Jul 7, 2024
d356cd7
Merge branch 'main' into add_usbcam_support
mryel00 Jul 7, 2024
da012aa
fix: add missing config file parameter
mryel00 Aug 5, 2024
97e9225
refactor: use requests code instaed of numbers
mryel00 Aug 11, 2024
088eb9d
chore: update .gitignore
mryel00 Aug 11, 2024
adb4c82
chore: wip
mryel00 Aug 11, 2024
4ae8a21
chore: wip
mryel00 Aug 11, 2024
d785864
chore: add MediaMTX mention
mryel00 Aug 11, 2024
faaa6ba
fix: update requirements.txt
mryel00 Aug 11, 2024
cecde16
chore: wip
mryel00 Aug 11, 2024
93d25c4
chore: remove unused code
mryel00 Aug 13, 2024
5f3614e
Merge branch 'main' into webrtc_mediamtx
mryel00 Aug 19, 2024
6bd4539
feat: add webrtc path
mryel00 Aug 21, 2024
1640062
fix: add missing webrtc endpoint logging
mryel00 Aug 21, 2024
b63ca2c
refactor: change to inline format string for endpoint logging
mryel00 Aug 21, 2024
4174d7a
chore: delete example file of aiortc
mryel00 Aug 23, 2024
3ce9beb
fix: fix PicameraStreamTrack condition loop for python<3.10
mryel00 Sep 1, 2024
9a32d6c
fix: increase sleep timer to appropriate value
mryel00 Sep 1, 2024
f9f39e2
chore: update orientation_exif description
mryel00 Sep 4, 2024
03c5a25
feat: force H264 codec
mryel00 Sep 4, 2024
01868c0
feat: use h264 hw encoder of picamera2
mryel00 Sep 5, 2024
61d8081
fix: don't double use frames
mryel00 Sep 21, 2024
63eb7ff
fix: fix webrtc stream with multiple clients
mryel00 Sep 22, 2024
b9eec7c
fix: fix starting method of encoder
mryel00 Sep 22, 2024
1768626
fix: fix order of starting methods
mryel00 Sep 22, 2024
fe65864
refactor: move parsed_controls processing into parse_dictionary_to_ht…
mryel00 Dec 31, 2024
21512f2
feat: add webrtc_url parameter
mryel00 Jan 8, 2025
6404037
chore: fix indentation
mryel00 Jan 8, 2025
91aad53
fix: fix default url
mryel00 Jan 8, 2025
4d2f601
fix: fix crash if aiortc is missing and disable webrtc instead
mryel00 Jan 8, 2025
1d15bee
feat: add option to disable WebRTC
mryel00 Jan 8, 2025
cb8434e
chore: remove single comma
mryel00 Jan 8, 2025
f733f51
chore: change naming from deactivate to disable
mryel00 Jan 10, 2025
7f3bb6e
fix: add missing config option
mryel00 Jan 10, 2025
ed1797a
chore: rename AIORTC_AVAILABLE to WEBRTC_ENABLED
mryel00 Jan 10, 2025
428b357
fix: fix potential import issue
mryel00 Jan 10, 2025
b8f08a5
feat: update installer to setup venv
mryel00 Jan 10, 2025
1f5ae2a
chore: change wording
mryel00 Jan 10, 2025
9610ff7
Merge branch 'main' into webrtc_mediamtx
mryel00 Jan 11, 2025
00cc611
fix: remove requests package dependency
mryel00 Jan 12, 2025
c60f37d
Merge branch 'webrtc_mediamtx' of https://github.com/roamingthings/sp…
mryel00 Jan 12, 2025
d511c06
fix: move init_camera import to set WEBRTC_ENABLED correct first
mryel00 Jan 12, 2025
61e4945
fix: use http library for HTTPStatus codes
mryel00 Jan 12, 2025
ea1bc2a
test: fix tests
mryel00 Jan 12, 2025
2edee77
test: refactor code
mryel00 Jan 12, 2025
d0efecb
test: fix mocking of picamera2.outputs.Output
mryel00 Jan 13, 2025
480f3c1
feat: limit max simultaneous webrtc connections
mryel00 Jan 13, 2025
2bb3a33
docs: update readme with webrtc
mryel00 Feb 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ all:
$(MAKE) help

install: ## Install Spyglass as service
@if [ "$$(id -u)" -eq 0 ]; then \
echo "Please run without sudo/not as root"; \
exit 1; \
fi
@mkdir -p $(CONF_PATH)
@printf "\nInstall virtual environment ...\n"
@python -m venv --system-site-packages .venv
@. .venv/bin/activate && pip install -r requirements.txt
@printf "\nCopying systemd service file ...\n"
@sudo cp -f "${PWD}/resources/spyglass.service" $(SYSTEMD)
@sudo sed -i "s/%USER%/$(USER)/g" $(SYSTEMD)/spyglass.service
Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,20 @@ On startup the following arguments are supported:
| `-f`, `--fps` | Framerate in frames per second (fps). | `15` |
| `-st`, `--stream_url` | Sets the URL for the mjpeg stream. | `/stream` |
| `-sn`, `--snapshot_url` | Sets the URL for snapshots (single frame of stream). | `/snapshot` |
| `-w`, `--webrtc_url` | Sets the URL for WebRTC (H264 compressed stream). | `/webrtc` |
| `-af`, `--autofocus` | Autofocus mode. Supported modes: `manual`, `continuous`. | `continuous` |
| `-l`, `--lensposition` | Set focal distance. 0 for infinite focus, 0.5 for approximate 50cm. Only used with Autofocus manual. | `0.0` |
| `-s`, `--autofocusspeed` | Autofocus speed. Supported values: `normal`, `fast`. Only used with Autofocus continuous | `normal` |
| `-ud`, `--upsidedown` | Rotate the image by 180° (see below) | |
| `-fh`, `--flip_horizontal` | Mirror the image horizontally (see below) | |
| `-fv`, `--flip_vertical` | Mirror the image vertically (see below) | |
| `-or`, `--orientation_exif` | Set the image orientation using an EXIF header (see below) | |
| `-ud`, `--upsidedown` | Rotate the image by 180° (see [below](#image-orientation)) | |
| `-fh`, `--flip_horizontal` | Mirror the image horizontally (see [below](#image-orientation)) | |
| `-fv`, `--flip_vertical` | Mirror the image vertically (see [below](#image-orientation)) | |
| `-or`, `--orientation_exif` | Set the image orientation using an EXIF header (see [below](#image-orientation)) | |
| `-c`, `--controls` | Define camera controls to start spyglass with. Can be used multiple times. This argument expects the format \<control\>=\<value\>. | |
| `--list-controls` | List all available libcamera controls onto the console. Those can be used with `--controls` | |
| `-tf`, `--tuning_filter` | Set a tuning filter file name. | |
| `-tfd`, `--tuning_filter_dir` | Set the directory to look for tuning filters. | |
| `-n`, `--camera_num` | Camera number to be used. All cameras with their number can be shown with `libcamera-hello`. | `0` |
| `--disable_webrtc` | Disables WebRTC encoding (recommended on Pi5). | |
| `--list-controls` | List all available libcamera controls onto the console. Those can be used with `--controls` | |

Starting the server without any argument is the same as

Expand Down Expand Up @@ -123,6 +125,14 @@ If you want to use Spyglass as a webcam source for [Mainsail]() add a webcam wit
- URL Snapshot: `/webcam/snapshot`
- Service: `V4L-MJPEG`

Alternatively you can use WebRTC. This will take less network bandwidth and might help to fix low fps:

- URL Stream: `/webcam/webrtc`
- URL Snapshot: `/webcam/snapshot`
- Service: `WebRTC (MediaMTX)`

WebRTC needs [aiortc](https://github.com/aiortc/aiortc) installed. This gets automatically installed with `make install` for further instructions, please see the [install](#install) chapter below.

## Install as application

If you want to install Spyglass globally on your machine you can use `python -m pip install .` to do so.
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
aiortc~=1.9.0
setuptools~=75.1.0
6 changes: 6 additions & 0 deletions resources/spyglass.conf
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ SNAPSHOT_URL="/snapshot"
#### NOTE: use format as shown below to stay MJPG-Streamer URL compatible
## SNAPSHOT_URL="/?action=snapshot"

#### WebRTC URL (STRING)[default: /webrtc]
WEBRTC_URL="/webrtc"

#### Disable WebRTC
#DISABLE_WEBRTC="true"

#### Autofocus behavior (STRING:manual,continuous)[default: continuous]
AUTO_FOCUS="continuous"

Expand Down
14 changes: 11 additions & 3 deletions scripts/spyglass
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set -Eeou pipefail

### Global Variables
BASE_SPY_PATH="$(dirname "$(readlink -f "${0}")")"
PY_BIN="$(command -v python)"
PY_BIN="$(. .venv/bin/activate && command -v python)"
SPYGLASS_CFG="${HOME}/printer_data/config/spyglass.conf"

### Helper Messages
Expand Down Expand Up @@ -93,14 +93,22 @@ run_spyglass() {
bind_adress="0.0.0.0"
fi

if [[ "${DISABLE_WEBRTC:-false}" == "true" ]]; then
disable_webrtc="--disable_webrtc"
else
disable_webrtc=""
fi

"${PY_BIN}" "$(dirname "${BASE_SPY_PATH}")/run.py" \
--camera_num "${CAMERA_NUM:-0}" \
--bindaddress "${bind_adress}" \
--port "${HTTP_PORT:-8080}" \
--resolution "${RESOLUTION:-640x480}" \
--fps "${FPS:-15}" \
--stream_url "${STREAM_URL:-\/stream}" \
--snapshot_url "${SNAPSHOT_URL:-\/snapshot}" \
--stream_url "${STREAM_URL:-/stream}" \
--snapshot_url "${SNAPSHOT_URL:-/snapshot}" \
--webrtc_url "${WEBRTC_URL:-/webrtc}" \
${disable_webrtc} \
--autofocus "${AUTO_FOCUS:-continuous}" \
--lensposition "${FOCAL_DIST:-0.0}" \
--autofocusspeed "${AF_SPEED:-normal}" \
Expand Down
6 changes: 6 additions & 0 deletions spyglass/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""init py module."""
import logging
import importlib.util

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

if importlib.util.find_spec("aiortc"):
WEBRTC_ENABLED=True
else:
WEBRTC_ENABLED=False
26 changes: 19 additions & 7 deletions spyglass/camera/camera.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import libcamera
import threading

from abc import ABC, abstractmethod
from picamera2 import Picamera2

from spyglass import logger
from spyglass import logger, WEBRTC_ENABLED
from spyglass.exif import create_exif_header
from spyglass.camera_options import process_controls
from spyglass.server import StreamingServer, StreamingHandler
from spyglass.server.http_server import StreamingServer, StreamingHandler
from spyglass.server.webrtc_whep import PicameraStreamTrack

class Camera(ABC):
def __init__(self, picam2: Picamera2):
self.picam2 = picam2
self.media_track = PicameraStreamTrack()

def create_controls(self, fps: int, autofocus: str, lens_position: float, autofocus_speed: str):
controls = {}

if 'FrameRate' in self.picam2.camera_controls:
if 'FrameDurationLimits' in self.picam2.camera_controls:
controls['FrameRate'] = fps

if 'AfMode' in self.picam2.camera_controls:
Expand Down Expand Up @@ -63,21 +66,29 @@ def _run_server(self,
get_frame,
stream_url='/stream',
snapshot_url='/snapshot',
webrtc_url='/webrtc',
orientation_exif=0):
logger.info('Server listening on %s:%d', bind_address, port)
logger.info('Streaming endpoint: %s', stream_url)
logger.info('Snapshot endpoint: %s', snapshot_url)
logger.info('Controls endpoint: %s', '/controls')
logger.info(f'Server listening on {bind_address}:{port}')
logger.info(f'Streaming endpoint: {stream_url}')
logger.info(f'Snapshot endpoint: {snapshot_url}')
if WEBRTC_ENABLED:
logger.info(f'WebRTC endpoint: {webrtc_url}')
logger.info(f'Controls endpoint: /controls')
address = (bind_address, port)
streaming_handler.picam2 = self.picam2
streaming_handler.media_track = self.media_track
streaming_handler.get_frame = get_frame
streaming_handler.stream_url = stream_url
streaming_handler.snapshot_url = snapshot_url
streaming_handler.webrtc_url = webrtc_url

if orientation_exif > 0:
streaming_handler.exif_header = create_exif_header(orientation_exif)
else:
streaming_handler.exif_header = None
current_server = StreamingServer(address, streaming_handler)
async_loop = threading.Thread(target=StreamingHandler.loop.run_forever)
async_loop.start()
current_server.serve_forever()

@abstractmethod
Expand All @@ -86,6 +97,7 @@ def start_and_run_server(self,
port,
stream_url='/stream',
snapshot_url='/snapshot',
webrtc_url='/webrtc',
orientation_exif=0):
pass

Expand Down
13 changes: 9 additions & 4 deletions spyglass/camera/csi.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import io

from picamera2.encoders import MJPEGEncoder
from picamera2.encoders import MJPEGEncoder, H264Encoder
from picamera2.outputs import FileOutput
from threading import Condition

from spyglass import camera
from spyglass.server import StreamingHandler
from spyglass import camera, WEBRTC_ENABLED
from spyglass.server.http_server import StreamingHandler

class CSI(camera.Camera):
def start_and_run_server(self,
bind_address,
port,
stream_url='/stream',
snapshot_url='/snapshot',
webrtc_url='/webrtc',
orientation_exif=0):

class StreamingOutput(io.BufferedIOBase):
Expand All @@ -30,7 +31,10 @@ def get_frame(inner_self):
output.condition.wait()
return output.frame

self.picam2.start_recording(MJPEGEncoder(), FileOutput(output))
self.picam2.start_encoder(MJPEGEncoder(), FileOutput(output))
if WEBRTC_ENABLED:
self.picam2.start_encoder(H264Encoder(), self.media_track)
self.picam2.start()

self._run_server(
bind_address,
Expand All @@ -39,6 +43,7 @@ def get_frame(inner_self):
get_frame,
stream_url=stream_url,
snapshot_url=snapshot_url,
webrtc_url=webrtc_url,
orientation_exif=orientation_exif
)

Expand Down
4 changes: 3 additions & 1 deletion spyglass/camera/usb.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from spyglass import camera
from spyglass.server import StreamingHandler
from spyglass.server.http_server import StreamingHandler

class USB(camera.Camera):
def start_and_run_server(self,
bind_address,
port,
stream_url='/stream',
snapshot_url='/snapshot',
webrtc_url='/webrtc',
orientation_exif=0):
def get_frame(inner_self):
#TODO: Cuts framerate in 1/n with n streams open, add some kind of buffer
Expand All @@ -21,6 +22,7 @@ def get_frame(inner_self):
get_frame,
stream_url=stream_url,
snapshot_url=snapshot_url,
webrtc_url=webrtc_url,
orientation_exif=orientation_exif
)

Expand Down
6 changes: 5 additions & 1 deletion spyglass/camera_options.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import libcamera
import ast

def parse_dictionary_to_html_page(camera, parsed_controls='None', processed_controls='None'):
def parse_dictionary_to_html_page(camera, parsed_controls={}, processed_controls={}):
if not parsed_controls:
parsed_controls = 'None'
if not processed_controls:
processed_controls = 'None'
html = """
<!DOCTYPE html>
<html lang="en">
Expand Down
18 changes: 14 additions & 4 deletions spyglass/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
import sys
import libcamera

from spyglass import camera_options, logger
from spyglass import camera_options, logger, WEBRTC_ENABLED
from spyglass.exif import option_to_exif_orientation
from spyglass.__version__ import __version__
from spyglass.camera import init_camera


MAX_WIDTH = 1920
MAX_HEIGHT = 1920


def main(args=None):
global WEBRTC_ENABLED
"""Entry point for hello cli.

The setup_py entry_point wraps this in sys.exit already so this effectively
Expand All @@ -45,6 +45,11 @@ def main(args=None):
if parsed_args.controls_string:
controls += [c.split('=') for c in parsed_args.controls_string.split(',')]

WEBRTC_ENABLED = WEBRTC_ENABLED and not parsed_args.disable_webrtc

# Has to be imported after WEBRTC_ENABLED got set correctly
from spyglass.camera import init_camera

cam = init_camera(
parsed_args.camera_num,
parsed_args.tuning_filter,
Expand All @@ -59,12 +64,13 @@ def main(args=None):
controls,
parsed_args.upsidedown,
parsed_args.flip_horizontal,
parsed_args.flip_vertical,)
parsed_args.flip_vertical)
try:
cam.start_and_run_server(parsed_args.bindaddress,
parsed_args.port,
parsed_args.stream_url,
parsed_args.snapshot_url,
parsed_args.webrtc_url,
parsed_args.orientation_exif)
finally:
cam.stop()
Expand Down Expand Up @@ -144,6 +150,10 @@ def get_parser():
help='Sets the URL for the mjpeg stream')
parser.add_argument('-sn', '--snapshot_url', type=str, default='/snapshot',
help='Sets the URL for snapshots (single frame of stream)')
parser.add_argument('-w', '--webrtc_url', type=str, default='/webrtc',
help='Sets the URL for the WebRTC stream')
parser.add_argument('--disable_webrtc', action='store_true',
help='Disables WebRTC encoding (recommended on Pi5)')
parser.add_argument('-af', '--autofocus', type=str, default='continuous', choices=['manual', 'continuous'],
help='Autofocus mode')
parser.add_argument('-l', '--lensposition', type=float, default=0.0,
Expand All @@ -158,7 +168,7 @@ def get_parser():
parser.add_argument('-fv', '--flip_vertical', action='store_true',
help='Mirror the image vertically (sensor level)')
parser.add_argument('-or', '--orientation_exif', type=orientation_type, default='h',
help='Set the image orientation using an EXIF header:\n'
help='Set the image orientation using an EXIF header. This does not work with WebRTC:\n'
' h - Horizontal (normal)\n'
' mh - Mirror horizontal\n'
' r180 - Rotate 180\n'
Expand Down
Loading