Skip to content

Commit

Permalink
v2.9.2 (#1210)
Browse files Browse the repository at this point in the history
* Tweak AV sync and ffmpeg cmd #1175 #1196 #1194 #1193 #1186

* Catch AccessTokenError

* don't drop timestamp #1175 #1196 #1194 #1193 #1186

* Don't use_wallclock_as_timestamps  #1175 #1196 #1194 #1193 #1186

* keep audio in sync #1175 #1196 #1194 #1193 #1186

* Ignore whitespaces in api key/id #1188

* Remove quotes from credentials #1158

* HA Add FORCE_FPS option #1161

* FORCE_FPS option for all cameras #1161

* changelog
  • Loading branch information
mrlt8 authored May 17, 2024
1 parent 907ad85 commit 987a698
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 46 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ You can then use the web interface at `http://localhost:5000` where localhost is

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.9.2

- Improved video connection stability and audio sync. #1175 #1196 #1194 #1193 #1186 Thanks @vipergts450!
- FIX: Remove quotes from credentials #1158
- NEW: `FORCE_FPS` option for all cameras #1161
- Home Assistant: Add `FORCE_FPS` option #1161
- Home Assistant: Ignore whitespaces in api key/id #1188 Thanks @richh1!


## What's Changed in v2.9.1

- FIX: Setting bitrate higher than 255 would not report correctly (#1185) Thanks @Anc0dia!
Expand Down Expand Up @@ -122,7 +131,8 @@ See [basic usage](#basic-usage) for additional information or visit the [wiki pa

[View previous changes](https://github.com/mrlt8/docker-wyze-bridge/releases)

> [!TIP] Home Assistant: you may need to re-add the repo if you cannot see the latest updates.
> [!TIP]
> Home Assistant: you may need to re-add the repo if you cannot see the latest updates.

## FAQ
Expand Down Expand Up @@ -297,6 +307,7 @@ 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.
* [carTloyal123/cryze](https://github.com/carTloyal123/cryze) - 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:

Expand Down
7 changes: 5 additions & 2 deletions app/wyzebridge/ffmpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_ffmpeg_cmd(
- list of str: complete ffmpeg command that is ready to run as subprocess.
"""

flags = "-fflags +flush_packets+nobuffer+genpts -flags +low_delay -use_wallclock_as_timestamps 1"
flags = "-fflags +flush_packets+nobuffer+genpts -flags +low_delay"
livestream = get_livestream_cmd(uri)
audio_in = "-f lavfi -i anullsrc=cl=mono" if livestream else ""
audio_out = "aac"
Expand All @@ -38,7 +38,7 @@ def get_ffmpeg_cmd(
audio_in = f"{thread_queue} -f {audio['codec']} -ac 1 -ar {audio['rate']} -i /tmp/{uri}_audio.pipe"
audio_out = audio["codec_out"] or "copy"
if audio and audio.get("codec", "").lower() == "aac_eld":
audio_in = f"{thread_queue} -f aac -ac 1 -i /tmp/{uri}_audio.pipe"
audio_in = f"{thread_queue} -f aac -ac 1 -re -i /tmp/{uri}_audio.pipe"
a_filter = env_bool("AUDIO_FILTER", "volume=5") + ",adelay=0|0"
a_options = ["-compression_level", "4", "-filter:a", a_filter]
rtsp_transport = "udp" if "udp" in env_bool("MTX_PROTOCOLS") else "tcp"
Expand All @@ -62,6 +62,9 @@ def get_ffmpeg_cmd(
+ re_encode_video(uri, is_vertical)
+ (["-map", "1:a", "-c:a", audio_out] if audio_in else [])
+ (a_options if audio and audio_out != "copy" else [])
+ ["-fps_mode", "passthrough", "-async", "1", "-flush_packets", "1"]
+ ["-muxdelay", "0"]
+ ["-rtbufsize", "1", "-max_interleave_delta", "10"]
+ ["-f", "tee"]
+ [rtsp_ss + get_record_cmd(uri, audio_out, record) + livestream]
)
Expand Down
2 changes: 2 additions & 0 deletions app/wyzebridge/hass.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def setup_hass(hass_token: Optional[str]) -> None:
environ[f"QUALITY_{cam_name}"] = str(cam["QUALITY"])
if "SUB_QUALITY" in cam:
environ[f"SUB_QUALITY_{cam_name}"] = str(cam["SUB_QUALITY"])
if "FORCE_FPS" in cam:
environ[f"FORCE_FPS_{cam_name}"] = str(cam["FORCE_FPS"])
if "LIVESTREAM" in cam:
environ[f"LIVESTREAM_{cam_name}"] = str(cam["LIVESTREAM"])
if "RECORD" in cam:
Expand Down
10 changes: 5 additions & 5 deletions app/wyzebridge/wyze_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ class WyzeCredentials:
__slots__ = "email", "password", "key_id", "api_key"

def __init__(self) -> None:
self.email: str = getenv("WYZE_EMAIL", "").strip()
self.password: str = getenv("WYZE_PASSWORD", "").strip()
self.key_id: str = getenv("API_ID", "").strip()
self.api_key: str = getenv("API_KEY", "").strip()
self.email: str = getenv("WYZE_EMAIL", "").strip("'\" \n\t\r")
self.password: str = getenv("WYZE_PASSWORD", "").strip("'\" \n\t\r")
self.key_id: str = getenv("API_ID", "").strip("'\" \n\t\r")
self.api_key: str = getenv("API_KEY", "").strip("'\" \n\t\r")

if not self.is_set:
logger.warning("[WARN] Credentials are NOT set")
Expand Down Expand Up @@ -215,7 +215,7 @@ def get_camera(self, uri: str, existing: bool = False) -> Optional[WyzeCamera]:
return next((c for c in self.cameras if c.name_uri == uri))

too_old = time() - self._last_pull > 120
with contextlib.suppress(TypeError):
with contextlib.suppress(TypeError, wyzecam.api.AccessTokenError):
for cam in self.get_cameras(fresh_data=too_old):
if cam.name_uri == uri:
return cam
Expand Down
2 changes: 1 addition & 1 deletion app/wyzebridge/wyze_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def get_video_params(sess: WyzeIOTCSession) -> tuple[str, int]:

fps = int(video_param.get("fps", 0))

if force_fps := int(env_bool(f"FORCE_FPS_{sess.camera.name_uri}", "0")):
if force_fps := int(env_cam("FORCE_FPS", sess.camera.name_uri, "0")):
logger.info(f"Attempting to force fps={force_fps}")
sess.update_frame_size_rate(fps=force_fps)
fps = force_fps
Expand Down
58 changes: 25 additions & 33 deletions app/wyzecam/iotc.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,15 @@ def sleep_interval(self) -> float:
return 0

if not self.frame_ts:
return 1 / 150
return 1 / 100

delta = max(time.time() - self.frame_ts, 0.0) + self._sleep_buffer
fps = 1 / self.preferred_frame_rate * 0.95
delta = max(time.time() - self.frame_ts, 0.0)
if self._sleep_buffer:
self._sleep_buffer = max(self._sleep_buffer - 0.05, 0)
delta += self._sleep_buffer
self._sleep_buffer = max(self._sleep_buffer - fps, 0)

return max((1 / self.preferred_frame_rate) - delta, 1 / 80)
return max(fps - delta, fps / 4)

@property
def pipe_name(self) -> str:
Expand Down Expand Up @@ -442,8 +444,8 @@ def recv_bridge_data(self) -> Iterator[bytes]:
have_key_frame = False
continue

if have_key_frame and self._video_frame_slow(frame_info):
continue
if have_key_frame:
self._video_frame_slow(frame_info)

if frame_info.is_keyframe:
have_key_frame = True
Expand Down Expand Up @@ -487,26 +489,22 @@ def _video_frame_slow(self, frame_info) -> Optional[bool]:
self.frame_ts = time.time()
return

frame_ts = float(f"{frame_info.timestamp}.{frame_info.timestamp_ms}")
gap = time.time() - frame_ts
self.frame_ts = float(f"{frame_info.timestamp}.{frame_info.timestamp_ms}")
gap = time.time() - self.frame_ts

if not frame_info.is_keyframe and gap > 3 and not self._sleep_buffer:
if not frame_info.is_keyframe and gap > 5:
logger.warning("[video] super slow")
self.clear_buffer()

return True

if gap >= 0.5:
logger.debug(f"[video] slow {gap=}")
if gap > 0:
self._sleep_buffer += gap

return

self.frame_ts = frame_ts
if gap >= 1:
logger.debug(f"[video] slow {gap=}")
self.flush_pipe("audio")

def _handle_frame_error(self, err_no: int) -> None:
"""Handle errors that occur when receiving frame data."""
time.sleep(1 / self.preferred_frame_rate * 0.8)
time.sleep(1 / 80)
if err_no == tutk.AV_ER_DATA_NOREADY or err_no >= 0:
return

Expand Down Expand Up @@ -559,7 +557,7 @@ def clear_buffer(self) -> None:
warnings.warn("clear buffer")
self.flush_pipe("audio")
self.sync_camera_time()
tutk.av_client_clean_buf(self.tutk_platform_lib, self.av_chan_id)
tutk.av_client_clean_local_buf(self.tutk_platform_lib, self.av_chan_id)

def flush_pipe(self, pipe_type: str = "audio"):
if pipe_type == "audio" and not self.audio_pipe_ready:
Expand Down Expand Up @@ -611,7 +609,7 @@ def recv_audio_pipe(self) -> None:
with contextlib.suppress(FileExistsError):
os.mkfifo(fifo_path)
try:
with open(fifo_path, "wb") as audio_pipe:
with open(fifo_path, "wb", buffering=0) as audio_pipe:
set_non_blocking(audio_pipe)
for frame_data, _ in self.recv_audio_data():
with contextlib.suppress(BlockingIOError):
Expand All @@ -632,25 +630,19 @@ def _audio_frame_slow(self, frame_info) -> Optional[bool]:
if frame_info.timestamp < 1591069888:
return

gap = self.frame_ts - float(f"{frame_info.timestamp}.{frame_info.timestamp_ms}")
gap = float(f"{frame_info.timestamp}.{frame_info.timestamp_ms}") - self.frame_ts

if abs(gap) > 5:
if abs(gap) > 10:
logger.debug(f"[audio] out of sync {gap=}")
self._sleep_buffer += abs(gap)
self.clear_buffer()
return

if gap <= -1:
logger.debug(f"[audio] rushing ahead of video.. {gap=}")
if gap < -1:
logger.debug(f"[audio] behind video.. {gap=}")
self.flush_pipe("audio")
self._sleep_buffer += abs(gap)

elif gap >= 1:
logger.debug(f"[audio] dragging behind video.. {gap=}")
self.flush_pipe("audio")
self.tutk_platform_lib.avClientCleanAudioBuf(self.av_chan_id)

return True
if gap > 1:
logger.debug(f"[audio] ahead of video.. {gap=}")
time.sleep(gap * 0.5)

def get_audio_sample_rate(self) -> int:
"""Attempt to get the audio sample rate."""
Expand Down
8 changes: 8 additions & 0 deletions home_assistant/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## What's Changed in v2.9.2

- Improved video connection stability and audio sync. #1175 #1196 #1194 #1193 #1186 Thanks @vipergts450!
- FIX: Remove quotes from credentials #1158
- NEW: `FORCE_FPS` option for all cameras #1161
- Home Assistant: Add `FORCE_FPS` option #1161
- Home Assistant: Ignore whitespaces in api key/id #1188 Thanks @richh1!

## What's Changed in v2.9.1

- FIX: Setting bitrate higher than 255 would not report correctly (#1185) Thanks @Anc0dia!
Expand Down
6 changes: 4 additions & 2 deletions home_assistant/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ options:
schema:
WYZE_EMAIL: email?
WYZE_PASSWORD: password?
API_ID: match([a-fA-F0-9-]{36})?
API_KEY: match([a-zA-Z0-9]{60})?
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?
Expand Down Expand Up @@ -93,6 +93,7 @@ schema:
URI_SEPARATOR: list(-|_|#)?
QUALITY: str?
SUB_QUALITY: str?
FORCE_FPS: int?
SUB_RECORD: bool?
FFMPEG_FLAGS: str?
FFMPEG_CMD: str?
Expand All @@ -117,6 +118,7 @@ schema:
ROTATE: bool?
QUALITY: str?
SUB_QUALITY: str?
FORCE_FPS: int?
RECORD: bool?
SUB_RECORD: bool?
SUBSTREAM: bool?
Expand Down
6 changes: 4 additions & 2 deletions home_assistant/dev/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ options:
schema:
WYZE_EMAIL: email?
WYZE_PASSWORD: password?
API_ID: match([a-fA-F0-9-]{36})?
API_KEY: match([a-zA-Z0-9]{60})?
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?
Expand Down Expand Up @@ -92,6 +92,7 @@ schema:
URI_SEPARATOR: list(-|_|#)?
QUALITY: str?
SUB_QUALITY: str?
FORCE_FPS: int?
SUB_RECORD: bool?
FFMPEG_FLAGS: str?
FFMPEG_CMD: str?
Expand All @@ -116,6 +117,7 @@ schema:
ROTATE: bool?
QUALITY: str?
SUB_QUALITY: str?
FORCE_FPS: int?
RECORD: bool?
SUB_RECORD: bool?
SUBSTREAM: bool?
Expand Down

0 comments on commit 987a698

Please sign in to comment.