Skip to content

Commit

Permalink
Motion Events and Audio Fix (#1006)
Browse files Browse the repository at this point in the history
* audio latency #388

* Motion Events

* Log number of cams from API #974

* Store and reuse s3 thumbnail from events #970

* better error handling

* Increase MTX_WRITEQUEUESIZE #984

* MOTION_START for HA

* limit login attempts

* pass exceptions upstream

* motion images for webhooks/ntfy in header

* REST API motion endpoint

* Additional audio codecs

* keep stream alive if livestream enabled #985

* Catch RuntimeError #994

* assert bitrate value

* refactor av_send_io_ctrl

* Refactor API client and use battery from device_info

* motion_ts endpoint

* use .pipe instead of .wav for audio pipe

* update frame size if not preferred_frame_size

* Exclude battery cams from RTSP snapshots #970

* revert changes

* buffer mtx event data #990

* Match all 11.x or newer firmware #975

* MOTION_WEBHOOKS

* clean

* revert changes

* Media MTX changes

* ffmpeg v6

* Use ffmpeg-for-homebridge in QSV build #736

* remove unneeded packages

* don't return none

* Adjust queue size and compression #388

* Adjust sleep, queue, and fifo for lag

* Catch other HTTPErrors for events

* Revert "Adjust sleep, queue, and fifo for lag"

* Update ffmpeg.py

* Update tutk.py

* adjust thread_queue_size #388

* Add build date time

* Update ffmpeg.py

* Additional queue tweaks and fifo options #388

* Catch RequestException in motion events

* Update MediaMTX version from v1.1.0 to v1.1.1 (#1007) (#1008)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: mrlt8 <[email protected]>

* Fix ETag header for MediaMTX 1.1.1

* Don't use wallclock  #388

* remove asetpts

* audio #388

* Update ffmpeg.py

* allow wallclock flag in FFMPEG_FLAGS #388

* changelog

* Update .env
  • Loading branch information
mrlt8 authored Oct 10, 2023
1 parent 05c24b5 commit 27fd845
Show file tree
Hide file tree
Showing 31 changed files with 669 additions and 324 deletions.
43 changes: 29 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,35 @@ 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.3.17

* NEW REST/MQTT Commands:
* `battery` GET current battery level on outdoor cams. (#864)
* `battery_usage` GET current battery usage times. (#864)
* `hor_flip` GET/SET horizontal video flip. (#976)
* `ver_flip` GET/SET vertical video flip. (#976)
* FIXES:
* Wrong bitrate error on newer `4.36.11.x` firmware which were not returning the correct bitrate info. (#975)
* Typo in `quick_response` REST/MQTT topic.
* invalid escape sequence warning.
* UPDATES:
* MediaMTX version from v1.0.0 to v1.0.3 (#979)

## What's Changed in v2.4.0

* Motion Events!
* Pulls events from the wyze API, so it doesn't require an active connection to the camera to detect motion - great for battery cams.
* Motion status and timestamp available via MQTT and REST API:
* MQTT topics: `wyzebridge/{cam-name}/motion` or `wyzebridge/{cam-name}/motion_ts`
* REST endpoint: `/api/{cam-name}/motion` or `/api/{cam-name}/motion_ts`
* Webhooks ready and works with ntfy.sh `triggers`.
* See [Camera Motion wiki](https://github.com/mrlt8/docker-wyze-bridge/wiki/Camera-Motion) for more information.
* Other fixes and changes:
* Potential improvements for audio sync. Audio will still lag on frame drops. (#388)
* Using **wallclock** seems to help in some situations:
`- FFMPEG_FLAGS=-use_wallclock_as_timestamps 1`
* UPDATE FFmpeg to [v6.0](https://github.com/homebridge/ffmpeg-for-homebridge/releases/tag/v2.1.0)
* UPDATE MediaMTX version from v1.0.3 to v1.1.0 (#1002)
* Store and reuse s3 thumbnail from events to reduce calls to the wyze api (#970)
* Increase default `MTX_WRITEQUEUESIZE` (#984)
* keep stream alive if livestream enabled (#985)
* Catch RuntimeError if libseccomp2 is missing (#994)
* Refactored API client to better handle exceptions and limit connections.
* Check bitrate from videoParams for all 11.x or newer firmware (#975)
* buffer mtx event data (#990)
* Exclude battery cams from scheduled RTSP snapshots (#970)
* New ENV/Options:
* `MOTION_API=True` to enable motion events. (Default: False)
* `MOTION_INT=<float>` number of seconds between motion checks. (Default: 1.5)
* `MOTION_START=True` to have the bridge initiate a connection to the camera on a motion event. (Default: False)
* `MOTION_WEBHOOK=<str>` webhooks url. Can use `{cam_name}` in the url to make a request to a url with the camera name. Image url and title are available in the request header.
* `MOTION_WEBHOOK_<CAM-NAME>=<str>` Same as `MOTION_WEBHOOK` but for a specific camera.

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

Expand Down
3 changes: 2 additions & 1 deletion app/.env
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
VERSION=2.3.17
MTX_TAG=1.1.1
IOS_VERSION=16.5
IOS_VERSION=17.0.3
APP_VERSION=2.44.5.3
MTX_HLSVARIANT=fmp4
MTX_PROTOCOLS=tcp
MTX_READTIMEOUT=20s
MTX_LOGLEVEL=warn
MTX_WEBRTCICEUDPMUXADDRESS=:8189
MTX_WRITEQUEUESIZE=1024
v3=mt0hBFxNQ4CQaWPjTonDk3mHaDEK2hhI
SDK_KEY=AQAAAIZ44fijz5pURQiNw4xpEfV9ZysFH8LYBPDxiONQlbLKaDeb7n26TSOPSGHftbRVo25k3uz5of06iGNB4pSfmvsCvm/tTlmML6HKS0vVxZnzEuK95TPGEGt+aE15m6fjtRXQKnUav59VSRHwRj9Z1Kjm1ClfkSPUF5NfUvsb3IAbai0WlzZE1yYCtks7NFRMbTXUMq3bFtNhEERD/7oc504b
30 changes: 30 additions & 0 deletions app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
## What's Changed in v2.4.0

* Motion Events!
* Pulls events from the wyze API, so it doesn't require an active connection to the camera to detect motion - great for battery cams.
* Motion status and timestamp available via MQTT and REST API:
* MQTT topics: `wyzebridge/{cam-name}/motion` or `wyzebridge/{cam-name}/motion_ts`
* REST endpoint: `/api/{cam-name}/motion` or `/api/{cam-name}/motion_ts`
* Webhooks ready and works with ntfy.sh `triggers`.
* See [Camera Motion wiki](https://github.com/mrlt8/docker-wyze-bridge/wiki/Camera-Motion) for more information.
* Other fixes and changes:
* Potential improvements for audio sync. Audio will still lag on frame drops. (#388)
* Using **wallclock** seems to help in some situations:
`- FFMPEG_FLAGS=-use_wallclock_as_timestamps 1`
* UPDATE FFmpeg to [v6.0](https://github.com/homebridge/ffmpeg-for-homebridge/releases/tag/v2.1.0)
* UPDATE MediaMTX version from v1.0.3 to v1.1.0 (#1002)
* Store and reuse s3 thumbnail from events to reduce calls to the wyze api (#970)
* Increase default `MTX_WRITEQUEUESIZE` (#984)
* keep stream alive if livestream enabled (#985)
* Catch RuntimeError if libseccomp2 is missing (#994)
* Refactored API client to better handle exceptions and limit connections.
* Check bitrate from videoParams for all 11.x or newer firmware (#975)
* buffer mtx event data (#990)
* Exclude battery cams from scheduled RTSP snapshots (#970)
* New ENV/Options:
* `MOTION_API=True` to enable motion events. (Default: False)
* `MOTION_INT=<float>` number of seconds between motion checks. (Default: 1.5)
* `MOTION_START=True` to have the bridge initiate a connection to the camera on a motion event. (Default: False)
* `MOTION_WEBHOOK=<str>` webhooks url. Can use `{cam_name}` in the url to make a request to a url with the camera name. Image url and title are available in the request header.
* `MOTION_WEBHOOK_<CAM-NAME>=<str>` Same as `MOTION_WEBHOOK` but for a specific camera.

## What's Changed in v2.3.17

* NEW REST/MQTT Commands:
Expand Down
8 changes: 5 additions & 3 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM base as builder
ARG ARM
ARG LIB_ARCH=${ARM:+arm}
ARG MTX_ARCH=${ARM:+armv7}
ARG FFMPEG_ARCH=${ARM:+armv7l}
ARG FFMPEG_ARCH=${ARM:+arm32v7}
RUN apt-get update \
&& apt-get install -y curl tar ${ARM:+gcc} \
&& apt-get clean \
Expand All @@ -18,12 +18,14 @@ RUN pip3 install --disable-pip-version-check --prefix=/build/usr/local -r /build
RUN cd /build \
&& . app/.env \
&& mkdir -p tokens img \
&& curl -SL https://github.com/homebridge/ffmpeg-for-homebridge/releases/latest/download/ffmpeg-debian-${FFMPEG_ARCH:-x86_64}.tar.gz \
&& curl -SL https://github.com/homebridge/ffmpeg-for-homebridge/releases/latest/download/ffmpeg-alpine-${FFMPEG_ARCH:-x86_64}.tar.gz \
| tar xzf - -C . \
&& curl -SL https://github.com/bluenviron/mediamtx/releases/download/v${MTX_TAG}/mediamtx_v${MTX_TAG}_linux_${MTX_ARCH:-amd64}.tar.gz \
| tar xzf - -C app \
&& cp app/${LIB_ARCH:-amd}.lib usr/local/lib/libIOTCAPIs_ALL.so \
&& rm app/*.txt app/*.lib
&& rm app/*.txt app/*.lib \
&& echo BUILD_DATE=$(date) > .build_date


FROM base
ARG BUILD
Expand Down
3 changes: 2 additions & 1 deletion app/Dockerfile.hwaccel
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ RUN cd /build \
| tar -xzf - -C app --wildcards 'mediamtx*' \
&& cp app/amd.lib usr/local/lib/libIOTCAPIs_ALL.so \
&& if [ -n "$QSV" ]; then cp -R /usr/lib/x86_64-linux-gnu/ usr/lib/; fi \
&& rm app/*.txt app/*.lib
&& rm app/*.txt app/*.lib \
&& echo BUILD_DATE=$(date) > .build_date

FROM base
ARG BUILD
Expand Down
7 changes: 4 additions & 3 deletions app/Dockerfile.multiarch
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ FROM base_$TARGETARCH as builder
ARG ARM
ARG LIB_ARCH=${ARM:+arm}
ARG MTX_ARCH=${ARM:+armv7}
ARG FFMPEG_ARCH=${ARM:+armv7l}
ARG FFMPEG_ARCH=${ARM:+arm32v7}
RUN apt-get update \
&& apt-get install -y curl tar ${ARM:+gcc} \
&& apt-get clean \
Expand All @@ -18,12 +18,13 @@ RUN pip3 install --disable-pip-version-check --prefix=/build/usr/local -r /build
RUN cd /build \
&& . app/.env \
&& mkdir -p tokens img \
&& curl -SL https://github.com/homebridge/ffmpeg-for-homebridge/releases/latest/download/ffmpeg-debian-${FFMPEG_ARCH:-x86_64}.tar.gz \
&& curl -SL https://github.com/homebridge/ffmpeg-for-homebridge/releases/latest/download/ffmpeg-alpine-${FFMPEG_ARCH:-x86_64}.tar.gz \
| tar xzf - -C . \
&& curl -SL https://github.com/bluenviron/mediamtx/releases/download/v${MTX_TAG}/mediamtx_v${MTX_TAG}_linux_${MTX_ARCH:-amd64}.tar.gz \
| tar xzf - -C app \
&& cp app/${LIB_ARCH:-amd}.lib usr/local/lib/libIOTCAPIs_ALL.so \
&& rm app/*.txt app/*.lib
&& rm app/*.txt app/*.lib \
&& echo BUILD_DATE=$(date) > .build_date

FROM base_$TARGETARCH
ARG BUILD
Expand Down
9 changes: 5 additions & 4 deletions app/Dockerfile.qsv
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FROM base as builder
ARG QSV
RUN if [ -n "$QSV" ]; then echo 'deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware' >/etc/apt/sources.list.d/debian-testing.list; fi \
&& apt-get update \
&& apt-get install -y curl tar xz-utils \
&& apt-get install -y curl tar \
${QSV:+i965-va-driver intel-gpu-tools intel-media-va-driver-non-free intel-opencl-icd libmfx1 libva-drm2 libx11-6 vainfo} \
&& if [ -n "$QSV" ]; then apt-get install -y i965-va-driver-shaders; fi \
&& apt-get clean \
Expand All @@ -16,13 +16,14 @@ RUN pip3 install --disable-pip-version-check --prefix=/build/usr/local -r /build
RUN cd /build \
&& . app/.env \
&& mkdir -p tokens img ${QSV:+usr/lib} \
&& curl -SL https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz \
| tar -Jxf - --strip-components=1 -C usr/local --wildcards '*ffmpeg' \
&& curl -SL https://github.com/homebridge/ffmpeg-for-homebridge/releases/latest/download/ffmpeg-alpine-x86_64.tar.gz \
| tar xzf - -C . \
&& curl -SL https://github.com/bluenviron/mediamtx/releases/download/v${MTX_TAG}/mediamtx_v${MTX_TAG}_linux_amd64.tar.gz \
| tar -xzf - -C app --wildcards 'mediamtx*' \
&& cp app/amd.lib usr/local/lib/libIOTCAPIs_ALL.so \
&& if [ -n "$QSV" ]; then cp -R /usr/lib/x86_64-linux-gnu/ usr/lib/ && cp -R /usr/bin/ usr/bin; fi \
&& rm app/*.txt app/*.lib
&& rm app/*.txt app/*.lib \
&& echo BUILD_DATE=$(date) > .build_date

FROM base
ARG BUILD
Expand Down
22 changes: 10 additions & 12 deletions app/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,8 @@
"IMG_DIR": "media/wyze/img/",
"RECORD_PATH": "media/wyze/{CAM_NAME}"
},
"map": [
"config:rw",
"media:rw",
"ssl:ro"
],
"services": [
"mqtt:want"
],
"map": ["config:rw", "media:rw", "ssl:ro"],
"services": ["mqtt:want"],
"ingress": true,
"ingress_port": 5000,
"panel_icon": "mdi:bridge",
Expand All @@ -52,6 +46,7 @@
"SNAPSHOT": "Disable",
"MQTT_DTOPIC": "homeassistant",
"ENABLE_AUDIO": false,
"MOTION_API": false,
"ON_DEMAND": true,
"SUBSTREAM": false,
"CAM_OPTIONS": []
Expand All @@ -73,6 +68,10 @@
"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?",
Expand Down Expand Up @@ -128,12 +127,11 @@
"SUB_QUALITY": "str?",
"RECORD": "bool?",
"SUB_RECORD": "bool?",
"SUBSTREAM": "bool?"
"SUBSTREAM": "bool?",
"MOTION_WEBHOOKS": "str?"
}
],
"MEDIAMTX": [
"match(^\\w+=.*)?"
],
"MEDIAMTX": ["match(^\\w+=.*)?"],
"WB_HLS_URL": "str?",
"WB_RTMP_URL": "str?",
"WB_RTSP_URL": "str?",
Expand Down
11 changes: 8 additions & 3 deletions app/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ def verify_password(username, password):
def create_app():
app = Flask(__name__)
wb = WyzeBridge()
wb.start()
try:
wb.start()
except RuntimeError as ex:
print(ex)
print("Please ensure your host is up to date.")
exit()

@app.route("/login", methods=["GET", "POST"])
def wyze_login():
if not wb.api.creds.login_req:
if wb.api.creds.is_set:
return redirect("/")
if request.method == "GET":
return render_template(
Expand All @@ -57,7 +62,7 @@ def wyze_login():
@app.route("/")
@auth.login_required
def index():
if wb.api.creds.login_req:
if not wb.api.creds.is_set:
return redirect("/login")
if not (columns := request.args.get("columns")):
columns = request.cookies.get("number_of_columns", "2")
Expand Down
110 changes: 71 additions & 39 deletions app/static/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,52 +442,84 @@ document.addEventListener("DOMContentLoaded", () => {
});
}
})
sse.addEventListener("message", (e) => {
Object.entries(JSON.parse(e.data)).forEach(([cam, status]) => {
sse.addEventListener("message", (event) => {
const data = JSON.parse(event.data);

for (const [cam, messageData] of Object.entries(data)) {
const card = document.getElementById(cam);
const statusIcon = card.querySelector(".status i.fas");
const preview = card.querySelector(`img.refresh_img,video[data-cam='${cam}']`);
const connected = (card.dataset.connected.toLowerCase() === "true")
const preview = card.querySelector(`img.refresh_img, video[data-cam='${cam}']`);
const motionIcon = card.querySelector(".icon.motion");
const connected = card.dataset.connected.toLowerCase() === "true";

card.dataset.connected = false;
statusIcon.setAttribute("class", "fas")
statusIcon.parentElement.title = ""
statusIcon.className = "fas";
statusIcon.parentElement.title = "";

if (preview) {
if (status == "connected") { preview.classList.add("connected") } else { preview.classList.remove("connected") }
if (status == "disabled") { preview.classList.remove("enabled") } else { preview.classList.add("enabled") }
preview.classList.toggle("connected", messageData.status === "connected");
preview.classList.toggle("enabled", messageData.status !== "disabled");
}
if (status == "connected") {
if (!connected) { sendNotification('Connected', `Connected to ${cam}`, "success"); }
card.dataset.connected = true;
statusIcon.classList.add("fa-circle-play", "has-text-success");
statusIcon.parentElement.title = "Click/tap to pause";
autoplay();
let noPreview = card.querySelector('.no-preview')
if (noPreview) {
let fig = noPreview.parentElement
let preview = document.createElement("img")
preview.classList.add("refresh_img", "loading-preview", "connected")
preview.dataset.cam = cam
preview.src = "static/loading.svg"
noPreview.replaceWith(preview)
loadPreview(fig.querySelector("img"))
}
} else if (status == "connecting") {
statusIcon.classList.add("fa-satellite-dish", "has-text-warning");
statusIcon.parentElement.title = "Click/tap to pause";
} else if (status == "stopped") {
if (connected) { sendNotification('Disconnected', `Disconnected from ${cam}`, "danger"); }
statusIcon.classList.add("fa-circle-pause");
statusIcon.parentElement.title = "Click/tap to play";
} else if (status == "offline") {
if (connected) { sendNotification('Offline', `${cam} is offline`, "danger"); }
statusIcon.classList.add("fa-ghost");
statusIcon.parentElement.title = "Camera offline";

if (messageData.motion) {
motionIcon.classList.remove("is-hidden");
sendNotification('Motion', `Motion detected on ${cam}`, "info");
} else {
if (connected) { sendNotification('Disconnected', `Disconnected from ${cam}`, "danger"); }
statusIcon.setAttribute("class", "fas fa-circle-exclamation")
statusIcon.parentElement.title = "Not Connected";
motionIcon.classList.add("is-hidden");
}
});

switch (messageData.status) {
case "connected":
if (!connected) {
sendNotification('Connected', `Connected to ${cam}`, "success");
}
card.dataset.connected = true;
statusIcon.classList.add("fa-circle-play", "has-text-success");
statusIcon.parentElement.title = "Click/tap to pause";
autoplay();

const noPreview = card.querySelector('.no-preview');
if (noPreview) {
const fig = noPreview.parentElement;
const newPreview = document.createElement("img");
newPreview.classList.add("refresh_img", "loading-preview", "connected");
newPreview.dataset.cam = cam;
newPreview.src = "static/loading.svg";
fig.replaceChild(newPreview, noPreview);
loadPreview(fig.querySelector("img"));
}
break;

case "connecting":
statusIcon.classList.add("fa-satellite-dish", "has-text-warning");
statusIcon.parentElement.title = "Click/tap to pause";
break;

case "stopped":
if (connected) {
sendNotification('Disconnected', `Disconnected from ${cam}`, "danger");
}
statusIcon.classList.add("fa-circle-pause");
statusIcon.parentElement.title = "Click/tap to play";
break;

case "offline":
if (connected) {
sendNotification('Offline', `${cam} is offline`, "danger");
}
statusIcon.classList.add("fa-ghost");
statusIcon.parentElement.title = "Camera offline";
break;

default:
if (connected) {
sendNotification('Disconnected', `Disconnected from ${cam}`, "danger");
}
statusIcon.className = "fas fa-circle-exclamation";
statusIcon.parentElement.title = "Not Connected";
break;
}
}
});

// Toggle Camera details
Expand Down
2 changes: 1 addition & 1 deletion app/static/webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class Receiver {
})
.then((res) => {
if (res.status !== 201) { throw new Error('Bad status code'); }
this.eTag = res.headers.get('E-Tag');
this.eTag = res.headers.get('ETag');
return res.text();
})
.then((sdp) => this.onRemoteDescription(sdp))
Expand Down
Loading

0 comments on commit 27fd845

Please sign in to comment.