From b1f5f4ee612905ede23743b23aec2a7f632ed6d4 Mon Sep 17 00:00:00 2001 From: mnerv <24420859+mnerv@users.noreply.github.com> Date: Sun, 1 Mar 2026 05:21:51 +0100 Subject: [PATCH] add mic recording, rename scripts, extend camera dump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename main.py → play_speaker.py - Add record_mic.py: record audio from camera mic via RTSP to a timestamped WAV in data/ - Add private API camera dump to dump_camera.py - Update README scripts section --- README.md | 6 ++-- main.py => play_speaker.py | 0 record_mic.py | 62 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) rename main.py => play_speaker.py (100%) create mode 100644 record_mic.py diff --git a/README.md b/README.md index 6615009..2d3d3b0 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,10 @@ access to settings not exposed by the official API, consider [uiprotect](https://github.com/uilibs/uiprotect) — an unofficial Python library wrapping the private UniFi Protect API. -## Run +## Scripts ```sh -uv run main.py +uv run play_speaker.py # play hello.wav through the camera speaker +uv run record_mic.py # record from the camera mic (Ctrl+C to stop) +uv run dump_camera.py # dump camera data from integration + private API ``` diff --git a/main.py b/play_speaker.py similarity index 100% rename from main.py rename to play_speaker.py diff --git a/record_mic.py b/record_mic.py new file mode 100644 index 0000000..af5125d --- /dev/null +++ b/record_mic.py @@ -0,0 +1,62 @@ +import json +import os +import subprocess +from datetime import datetime + +import httpx +from dotenv import load_dotenv + +load_dotenv() + +HOST = os.environ["HOST"] +PRIVATE_BASE = f"https://{HOST}/proxy/protect/api" +USERNAME = os.environ["UNIFI_USERNAME"] +PASSWORD = os.environ["UNIFI_PASSWORD"] + +CAMERA_NAME = "G6 Pro 360" +OUTPUT_DIR = "./data" + + +def main(): + body = json.dumps({"username": USERNAME, "password": PASSWORD, "rememberMe": False}).encode() + + # Accept-Encoding must be suppressed at request level — server returns 403 when present + with httpx.Client(verify=False, http2=True) as client: + client.post( + f"https://{HOST}/api/auth/login", + content=body, + headers={"Content-Type": "application/json", "Accept-Encoding": ""}, + ).raise_for_status() + + cameras = client.get(f"{PRIVATE_BASE}/cameras").raise_for_status().json() + camera = next(c for c in cameras if CAMERA_NAME in c["name"]) + + rtsp_alias = next(ch["rtspAlias"] for ch in camera["channels"] if ch.get("isRtspEnabled")) + rtsp_url = f"rtsps://{HOST}:7441/{rtsp_alias}?enableSrtp" + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + output = f"{OUTPUT_DIR}/recording_{timestamp}.wav" + + print(f"Recording from: {camera['name']}") + print(f"Output: {output}") + print("Press Ctrl+C to stop.") + + proc = subprocess.Popen([ + "ffmpeg", "-y", + "-rtsp_transport", "tcp", + "-i", rtsp_url, + "-vn", + "-acodec", "pcm_s16le", + output, + ]) + + try: + proc.wait() + except KeyboardInterrupt: + proc.terminate() + proc.wait() + print(f"\nSaved to {output}") + + +if __name__ == "__main__": + main()