diff --git a/README.md b/README.md index 6f7c42d5..19fd1a6e 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,18 @@ You can then use the web interface at `http://localhost:5050` where `localhost` See [basic usage](#basic-usage) for additional information or visit the [wiki page](/~https://github.com/mrlt8/docker-wyze-bridge/wiki/Home-Assistant) for additional information on using the bridge as a Home Assistant Add-on. +## What's Changed in v2.10.3 + +- FIX: Increased `MTX_WRITEQUEUESIZE` to prevent issues with higher bitrates. +- FIX: Restart RTMP livestream on fail (#1333) +- FIX: Restore user data on bridge restart (#1334) +- NEW: `SNAPSHOT_KEEP` Option to delete old snapshots when saving snapshots with a timelapse-like custom format with `SNAPSHOT_FORMAT`. (#1330) + - Example for 3 min: `SNAPSHOT_KEEP=180`, `SNAPSHOT_KEEP=180s`, `SNAPSHOT_KEEP=3m` + - Example for 3 days: `SNAPSHOT_KEEP=72h`, `SNAPSHOT_KEEP=3d` + - Example for 3 weeks: `SNAPSHOT_KEEP=21d`, `SNAPSHOT_KEEP=3w` +- NEW: `RESTREAMIO` option for livestreaming via [restream.io](https://restream.io). (#1333) + - Example `RESTREAMIO_FRONT_DOOR=re_My_Custom_Key123` + ## What's Changed in v2.10.2 - FIX: day/night FPS slowdown for V4 cameras (#1287) Thanks @cdoolin and @Answer-1! @@ -288,7 +300,9 @@ Honorable Mentions: Video Streaming: * [gtxaspec/wz_mini_hacks](/~https://github.com/gtxaspec/wz_mini_hacks) - Firmware level modification for Ingenic based cameras with an RTSP server and [self-hosted mode](/~https://github.com/gtxaspec/wz_mini_hacks/wiki/Configuration-File#self-hosted--isolated-mode) to use the cameras without the wyze services. +* [thingino](/~https://github.com/themactep/thingino-firmware) - Advanced custom firmware for some Ingenic-based wyze cameras. * [carTloyal123/cryze](/~https://github.com/carTloyal123/cryze) - Stream video from wyze cameras (Gwell cameras) that use the Iotvideo SDK from Tencent Cloud. +* [xerootg/cryze_v2](/~https://github.com/xerootg/cryze_v2) - Stream video from wyze cameras (Gwell cameras) that use the Iotvideo SDK from Tencent Cloud. * [mnakada/atomcam_tools](/~https://github.com/mnakada/atomcam_tools) - Video streaming for Wyze v3. General Wyze: diff --git a/app/.env b/app/.env index ce4ceb75..5aeb52af 100644 --- a/app/.env +++ b/app/.env @@ -6,5 +6,6 @@ MTX_HLSVARIANT=mpegts MTX_PROTOCOLS=tcp MTX_READTIMEOUT=20s MTX_LOGLEVEL=warn +MTX_WRITEQUEUESIZE=1024 MTX_WEBRTCICEUDPMUXADDRESS=:8189 SDK_KEY=AQAAAIZ44fijz5pURQiNw4xpEfV9ZysFH8LYBPDxiONQlbLKaDeb7n26TSOPSGHftbRVo25k3uz5of06iGNB4pSfmvsCvm/tTlmML6HKS0vVxZnzEuK95TPGEGt+aE15m6fjtRXQKnUav59VSRHwRj9Z1Kjm1ClfkSPUF5NfUvsb3IAbai0WlzZE1yYCtks7NFRMbTXUMq3bFtNhEERD/7oc504b diff --git a/app/wyze_bridge.py b/app/wyze_bridge.py index ff83a202..5f2ecfdb 100644 --- a/app/wyze_bridge.py +++ b/app/wyze_bridge.py @@ -34,7 +34,7 @@ def run(self, fresh_data: bool = False) -> None: def _initialize(self, fresh_data: bool = False) -> None: self.api.login(fresh_data=fresh_data) - WbAuth.set_email(email=self.api.creds.email, force=fresh_data) + WbAuth.set_email(email=self.api.get_user().email, force=fresh_data) self.mtx.setup_auth(WbAuth.api, STREAM_AUTH) self.setup_streams() if self.streams.total < 1: diff --git a/app/wyzebridge/auth.py b/app/wyzebridge/auth.py index b4de82f2..3bc27bfe 100644 --- a/app/wyzebridge/auth.py +++ b/app/wyzebridge/auth.py @@ -68,7 +68,7 @@ def set_email(cls, email: str, force: bool = False): cls._update_credentials(email, force) logger.info(f"[AUTH] WB_USERNAME={cls.username}") - logger.info(f"[AUTH] WB_PASSWORD={cls._pass[0]}{'*'*(len(cls._pass)-1)}") + logger.info(f"[AUTH] WB_PASSWORD={redact_password(cls._pass)}") logger.info(f"[AUTH] WB_API={cls.api}") @classmethod @@ -83,4 +83,8 @@ def _update_credentials(cls, email: str, force: bool = False) -> None: cls.api = get_credential("wb_api") or gen_api_key(email) +def redact_password(password: Optional[str]): + return f"{password[0]}{'*' * (len(password) - 1)}" if password else "NOT SET" + + STREAM_AUTH: str = env_bool("STREAM_AUTH", style="original") diff --git a/app/wyzebridge/bridge_utils.py b/app/wyzebridge/bridge_utils.py index 518beb3a..9ead56f5 100644 --- a/app/wyzebridge/bridge_utils.py +++ b/app/wyzebridge/bridge_utils.py @@ -4,6 +4,13 @@ from wyzecam.api_models import WyzeCamera +LIVESTREAM_PLATFORMS = { + "YouTube": "rtmp://a.rtmp.youtube.com/live2/", + "Facebook": "rtmps://live-api-s.facebook.com:443/rtmp/", + "RestreamIO": "rtmp://live.restream.io/live/", + "Livestream": "", +} + def env_cam(env: str, uri: str, default="", style="") -> str: return env_bool( @@ -61,9 +68,7 @@ def split_int_str(env_value: str, min: int = 0, default: int = 0) -> tuple[str, def is_livestream(uri: str) -> bool: - services = {"youtube", "facebook", "livestream"} - - return any(env_bool(f"{service}_{uri}") for service in services) + return any(env_bool(f"{service}_{uri}") for service in LIVESTREAM_PLATFORMS) def migrate_path(old: str, new: str): diff --git a/app/wyzebridge/ffmpeg.py b/app/wyzebridge/ffmpeg.py index a3006de4..ed90dfb9 100644 --- a/app/wyzebridge/ffmpeg.py +++ b/app/wyzebridge/ffmpeg.py @@ -1,7 +1,10 @@ import os -from datetime import datetime +import shutil +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional -from wyzebridge.bridge_utils import env_bool, env_cam +from wyzebridge.bridge_utils import LIVESTREAM_PLATFORMS, env_bool, env_cam from wyzebridge.config import IMG_PATH, SNAPSHOT_FORMAT from wyzebridge.logging import logger @@ -155,37 +158,66 @@ def re_encode_video(uri: str, is_vertical: bool) -> list[str]: def get_livestream_cmd(uri: str) -> str: - """ - Check if livestream is enabled and return ffmpeg tee cmd. - Parameters: - - uri (str): uri of the stream used to lookup ENV parameters. + flv = "|[f=flv:flvflags=no_duration_filesize:use_fifo=1:fifo_options=attempt_recovery=1\\\:drop_pkts_on_overflow=1:onfail=abort]" + + for platform, api in LIVESTREAM_PLATFORMS.items(): + key = env_bool(f"{platform}_{uri}", style="original") + if len(key) > 5: + logger.info(f"📺 Livestream to {platform if api else key} enabled") + return f"{flv}{api}{key}" + + return "" - Returns: - - str: ffmpeg compatible str to be used for the tee command. - """ - cmd = "" - flv = "|[f=flv:flvflags=no_duration_filesize:use_fifo=1]" - if len(key := env_bool(f"YOUTUBE_{uri}", style="original")) > 5: - logger.info("📺 YouTube livestream enabled") - cmd += f"{flv}rtmp://a.rtmp.youtube.com/live2/{key}" - if len(key := env_bool(f"FACEBOOK_{uri}", style="original")) > 5: - logger.info("📺 Facebook livestream enabled") - cmd += f"{flv}rtmps://live-api-s.facebook.com:443/rtmp/{key}" - if len(key := env_bool(f"LIVESTREAM_{uri}", style="original")) > 5: - logger.info(f"📺 Custom ({key}) livestream enabled") - cmd += f"{flv}{key}" - return cmd + +def purge_old(base_path: str, extension: str, keep_time: Optional[timedelta]): + if not keep_time: + return + threshold = datetime.now() - keep_time + for filepath in Path(base_path).rglob(f"*{extension}"): + if filepath.stat().st_mtime > threshold.timestamp(): + continue + filepath.unlink() + logger.debug(f"[ffmpeg] Deleted: {filepath}") + + if not any(filepath.parent.iterdir()): + shutil.rmtree(filepath.parent) + logger.debug(f"[ffmpeg] Deleted empty directory: {filepath.parent}") + + +def parse_timedelta(env_key: str) -> Optional[timedelta]: + value = env_bool(env_key) + if not value: + return + + time_map = {"s": "seconds", "m": "minutes", "h": "hours", "d": "days", "w": "weeks"} + if value.isdigit(): + value += "s" + + try: + amount, unit = int(value[:-1]), value[-1] + if unit not in time_map or amount < 1: + return + return timedelta(**{time_map[unit]: amount}) + except (ValueError, TypeError): + return def rtsp_snap_cmd(cam_name: str, interval: bool = False): - img = f"{IMG_PATH}{cam_name}.{env_bool('IMG_TYPE','jpg')}" + ext = env_bool("IMG_TYPE", "jpg") + img = f"{IMG_PATH}{cam_name}.{ext}" if interval and SNAPSHOT_FORMAT: file = datetime.now().strftime(f"{IMG_PATH}{SNAPSHOT_FORMAT}") - img = file.format(cam_name=cam_name, CAM_NAME=cam_name.upper()) + base, _ext = os.path.splitext(file) + ext = _ext.lstrip(".") or ext + img = f"{base}.{ext}".format(cam_name=cam_name, CAM_NAME=cam_name.upper()) os.makedirs(os.path.dirname(img), exist_ok=True) + keep_time = parse_timedelta("SNAPSHOT_KEEP") + if keep_time and SNAPSHOT_FORMAT: + purge_old(IMG_PATH, ext, keep_time) + rotation = [] if rotate_img := env_bool(f"ROTATE_IMG_{cam_name}"): transpose = rotate_img if rotate_img in {"0", "1", "2", "3"} else "clock" diff --git a/home_assistant/CHANGELOG.md b/home_assistant/CHANGELOG.md index 8b3d9cf0..44650f98 100644 --- a/home_assistant/CHANGELOG.md +++ b/home_assistant/CHANGELOG.md @@ -1,3 +1,15 @@ +## What's Changed in v2.10.3 + +- FIX: Increased `MTX_WRITEQUEUESIZE` to prevent issues with higher bitrates. +- FIX: Restart RTMP livestream on fail (#1333) +- FIX: Restore user data on bridge restart (#1334) +- NEW: `SNAPSHOT_KEEP` Option to delete old snapshots when saving snapshots with a timelapse-like custom format with `SNAPSHOT_FORMAT`. (#1330) + - Example for 3 min: `SNAPSHOT_KEEP=180`, `SNAPSHOT_KEEP=180s`, `SNAPSHOT_KEEP=3m` + - Example for 3 days: `SNAPSHOT_KEEP=72h`, `SNAPSHOT_KEEP=3d` + - Example for 3 weeks: `SNAPSHOT_KEEP=21d`, `SNAPSHOT_KEEP=3w` +- NEW: `RESTREAMIO` option for livestreaming via [restream.io](https://restream.io). (#1333) + - Example `RESTREAMIO_FRONT_DOOR=re_My_Custom_Key123` + ## What's Changed in v2.10.2 - FIX: day/night FPS slowdown for V4 cameras (#1287) Thanks @cdoolin and @Answer-1! diff --git a/home_assistant/config.yml b/home_assistant/config.yml index efb134f7..1173478b 100644 --- a/home_assistant/config.yml +++ b/home_assistant/config.yml @@ -58,6 +58,7 @@ schema: NET_MODE: list(LAN|P2P|ANY)? SNAPSHOT: list(API|RTSP|RTSP15|RTSP30|RTSP60|RTSP180|RTSP300|Disable)? SNAPSHOT_FORMAT: str? + SNAPSHOT_KEEP: str? IMG_TYPE: list(jpg|png)? IMG_DIR: str? ENABLE_AUDIO: bool? diff --git a/home_assistant/previous/config.json b/home_assistant/previous/config.json deleted file mode 100644 index b03f07f2..00000000 --- a/home_assistant/previous/config.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "name": "Docker Wyze Bridge (2.6.0 build)", - "description": "Previous build. Use the REBUILD button to pull latest image.", - "slug": "docker_wyze_bridge_previous", - "url": "/~https://github.com/mrlt8/docker-wyze-bridge", - "version": "2.6.0", - "stage": "deprecated", - "arch": ["armv7", "aarch64", "amd64"], - "startup": "application", - "boot": "auto", - "apparmor": false, - "hassio_api": true, - "ports": { - "1935/tcp": 1935, - "8554/tcp": 8554, - "8888/tcp": 8888, - "8189/udp": 8189, - "8889/tcp": 8889, - "5000/tcp": 5000 - }, - "ports_description": { - "1935/tcp": "RTMP streams", - "8554/tcp": "RTSP streams", - "8888/tcp": "HLS streams", - "8189/udp": "WebRTC ICE", - "8889/tcp": "WebRTC streams", - "5000/tcp": "Web-UI/Snapshots" - }, - "environment": { - "IMG_DIR": "media/wyze/img/", - "RECORD_PATH": "media/wyze/{CAM_NAME}" - }, - "map": ["addon_config:rw", "media:rw", "ssl:ro"], - "services": ["mqtt:want"], - "ingress": true, - "ingress_port": 5000, - "panel_icon": "mdi:bridge", - "options": { - "WYZE_EMAIL": null, - "WYZE_PASSWORD": null, - "API_ID": null, - "API_KEY": null, - "NET_MODE": "ANY", - "SNAPSHOT": "Disable", - "MQTT_DTOPIC": "homeassistant", - "ENABLE_AUDIO": false, - "MOTION_API": false, - "ON_DEMAND": true, - "SUBSTREAM": false, - "CAM_OPTIONS": [] - }, - "schema": { - "WYZE_EMAIL": "email?", - "WYZE_PASSWORD": "password?", - "API_ID": "str?", - "API_KEY": "str?", - "WB_IP": "str?", - "MFA_TYPE": "list(TotpVerificationCode|PrimaryPhone|Email)?", - "TOTP_KEY": "str?", - "REFRESH_TOKEN": "str?", - "ACCESS_TOKEN": "str?", - "NET_MODE": "list(LAN|P2P|ANY)", - "SNAPSHOT": "list(API|RTSP|RTSP15|RTSP30|RTSP60|RTSP180|RTSP300|Disable)?", - "SNAPSHOT_FORMAT": "str?", - "IMG_TYPE": "list(jpg|png)?", - "IMG_DIR": "str?", - "ON_DEMAND": "bool?", - "ENABLE_AUDIO": "bool?", - "MOTION_API": "bool?", - "MOTION_INT": "float(1.1,)?", - "MOTION_START": "bool?", - "MOTION_WEBHOOKS": "str?", - "SUBSTREAM": "bool?", - "AUDIO_CODEC": "list(COPY|AAC|MP3|LIBOPUS)?", - "AUDIO_FILTER": "str?", - "LLHLS": "bool?", - "DISABLE_CONTROL": "bool?", - "RTSP_FW": "bool?", - "RECORD_ALL": "bool?", - "RECORD_LENGTH": "int?", - "RECORD_PATH": "str?", - "RECORD_FILE_NAME": "str?", - "MQTT_HOST": "str?", - "MQTT_AUTH": "str?", - "MQTT_TOPIC": "str?", - "MQTT_DTOPIC": "str?", - "MQTT_RETRIES": "int?", - "FILTER_NAMES": "str?", - "FILTER_MODELS": "str?", - "FILTER_MACS": "str?", - "FILTER_BLOCK": "bool?", - "ROTATE_DOOR": "bool?", - "H264_ENC": "str?", - "FORCE_ENCODE": "bool?", - "IGNORE_OFFLINE": "bool?", - "OFFLINE_IFTTT": "match(^\\w+:\\w+)?", - "FRESH_DATA": "bool?", - "URI_MAC": "bool?", - "URI_SEPARATOR": "list(-|_|#)?", - "QUALITY": "str?", - "SUB_QUALITY": "str?", - "SUB_RECORD": "bool?", - "FFMPEG_FLAGS": "str?", - "FFMPEG_CMD": "str?", - "LOG_LEVEL": "list(INFO|DEBUG)?", - "LOG_FILE": "bool?", - "LOG_TIME": "bool?", - "FFMPEG_LOGLEVEL": "list(quiet|panic|fatal|error|warning|info|verbose|debug)?", - "IGNORE_RES": "int?", - "BOA_ENABLED": "bool?", - "BOA_INTERVAL": "int?", - "BOA_TAKE_PHOTO": "bool?", - "BOA_PHOTO": "bool?", - "BOA_ALARM": "bool?", - "BOA_MOTION": "str?", - "BOA_COOLDOWN": "int?", - "CAM_OPTIONS": [ - { - "CAM_NAME": "str?", - "AUDIO": "bool?", - "FFMPEG": "str?", - "LIVESTREAM": "str?", - "NET_MODE": "str?", - "ROTATE": "bool?", - "QUALITY": "str?", - "SUB_QUALITY": "str?", - "RECORD": "bool?", - "SUB_RECORD": "bool?", - "SUBSTREAM": "bool?", - "MOTION_WEBHOOKS": "str?" - } - ], - "MEDIAMTX": ["match(^\\w+=.*)?"], - "WB_HLS_URL": "str?", - "WB_RTMP_URL": "str?", - "WB_RTSP_URL": "str?", - "WB_WEBRTC_URL": "str?" - } -} diff --git a/home_assistant/previous/config.yml b/home_assistant/previous/config.yml new file mode 100644 index 00000000..d9bc058e --- /dev/null +++ b/home_assistant/previous/config.yml @@ -0,0 +1,137 @@ +name: Docker Wyze Bridge (2.9.12 build) +description: Previous build. Use the REBUILD button to pull latest image. +slug: docker_wyze_bridge_previous +url: /~https://github.com/mrlt8/docker-wyze-bridge +image: mrlt8/wyze-bridge +version: 2.9.12 +stage: deprecated +arch: + - armv7 + - aarch64 + - amd64 +startup: application +boot: auto +apparmor: false +hassio_api: true +ports: + 1935/tcp: 1935 + 8554/tcp: 8554 + 8888/tcp: 8888 + 8189/udp: 8189 + 8889/tcp: 8889 + 5000/tcp: 5000 +ports_description: + 1935/tcp: RTMP streams + 8554/tcp: RTSP streams + 8888/tcp: HLS streams + 8189/udp: WebRTC ICE + 8889/tcp: WebRTC streams + 5000/tcp: Web-UI/Snapshots +environment: + IMG_DIR: media/wyze/img/ + RECORD_PATH: media/wyze/{CAM_NAME} + MQTT_DTOPIC: homeassistant +map: + - addon_config:rw + - media:rw + - ssl:ro +services: + - mqtt:want +ingress: true +ingress_port: 5000 +panel_icon: mdi:bridge +options: + ENABLE_AUDIO: true + ON_DEMAND: true + WB_AUTH: true + MOTION_API: true + MQTT: true + CAM_OPTIONS: [] +schema: + WYZE_EMAIL: email? + WYZE_PASSWORD: password? + API_ID: match(\s*[a-fA-F0-9-]{36}\s*)? + API_KEY: match(\s*[a-zA-Z0-9]{60}\s*)? + WB_IP: str? + REFRESH_TOKEN: str? + ACCESS_TOKEN: str? + NET_MODE: list(LAN|P2P|ANY)? + SNAPSHOT: list(API|RTSP|RTSP15|RTSP30|RTSP60|RTSP180|RTSP300|Disable)? + SNAPSHOT_FORMAT: str? + IMG_TYPE: list(jpg|png)? + IMG_DIR: str? + ENABLE_AUDIO: bool? + ON_DEMAND: bool? + MOTION_API: bool? + MOTION_INT: float(1.1,)? + MOTION_START: bool? + MOTION_WEBHOOKS: str? + SUBSTREAM: bool? + AUDIO_CODEC: list(COPY|AAC|LIBOPUS|MP3|PCM_MULAW|PCM_ALAW)? + AUDIO_FILTER: str? + LLHLS: bool? + DISABLE_CONTROL: bool? + RTSP_FW: bool? + RECORD_ALL: bool? + RECORD_LENGTH: int? + RECORD_PATH: str? + RECORD_FILE_NAME: str? + MQTT: bool + MQTT_HOST: str? + MQTT_AUTH: str? + MQTT_TOPIC: str? + MQTT_DTOPIC: str? + MQTT_RETRIES: int? + FILTER_NAMES: str? + FILTER_MODELS: str? + FILTER_MACS: str? + FILTER_BLOCK: bool? + ROTATE_DOOR: bool? + H264_ENC: str? + FORCE_ENCODE: bool? + IGNORE_OFFLINE: bool? + OFFLINE_WEBHOOKS: bool? + FRESH_DATA: bool? + URI_MAC: bool? + URI_SEPARATOR: list(-|_|#)? + QUALITY: str? + SUB_QUALITY: str? + FORCE_FPS: int? + SUB_RECORD: bool? + FFMPEG_FLAGS: str? + FFMPEG_CMD: str? + LOG_LEVEL: list(INFO|DEBUG)? + LOG_FILE: bool? + LOG_TIME: bool? + FFMPEG_LOGLEVEL: list(quiet|panic|fatal|error|warning|info|verbose|debug)? + IGNORE_RES: int? + BOA_ENABLED: bool? + BOA_INTERVAL: int? + BOA_TAKE_PHOTO: bool? + BOA_PHOTO: bool? + BOA_ALARM: bool? + BOA_MOTION: str? + BOA_COOLDOWN: int? + CAM_OPTIONS: + - CAM_NAME: str? + AUDIO: bool? + FFMPEG: str? + LIVESTREAM: str? + NET_MODE: str? + ROTATE: bool? + QUALITY: str? + SUB_QUALITY: str? + FORCE_FPS: int? + RECORD: bool? + SUB_RECORD: bool? + SUBSTREAM: bool? + MOTION_WEBHOOKS: str? + MEDIAMTX: + - match(^\w+=.*)? + WB_HLS_URL: str? + WB_RTMP_URL: str? + WB_RTSP_URL: str? + WB_WEBRTC_URL: str? + WB_AUTH: bool? + WB_USERNAME: str? + WB_PASSWORD: str?