This repository has been archived on 2024-03-03. You can view files and clone it, but cannot push or open issues or pull requests.
unifi-cam-proxy/unifi/cams/reolink.py
Liam e8f72ea949
Some checks failed
📚 Deploy Documentation / deploy (push) Has been cancelled
bob
2024-03-02 11:37:04 +00:00

149 lines
5.5 KiB
Python

import argparse
import json
import logging
import tempfile
from pathlib import Path
import aiohttp
import reolinkapi
from yarl import URL
from unifi.cams.base import UnifiCamBase
class Reolink(UnifiCamBase):
def __init__(self, args: argparse.Namespace, logger: logging.Logger) -> None:
super().__init__(args, logger)
self.snapshot_dir: str = tempfile.mkdtemp()
self.motion_in_progress: bool = False
self.substream = args.substream
self.cam = reolinkapi.Camera(
ip=args.ip,
username=args.username,
password=args.password,
)
self.stream_fps = self.get_stream_info(self.cam)
@classmethod
def add_parser(cls, parser: argparse.ArgumentParser) -> None:
super().add_parser(parser)
parser.add_argument("--username", "-u", required=True, help="Camera username")
parser.add_argument("--password", "-p", required=True, help="Camera password")
parser.add_argument(
"--channel",
"-c",
default=0,
help="Camera channel (not needed, leaving for possible future)",
)
parser.add_argument(
"--stream",
"-m",
default="main",
type=str,
choices=["main", "sub"],
help="Stream profile to use for the higher quality stream",
)
parser.add_argument(
"--substream",
"-s",
default="sub",
type=str,
choices=["main", "sub"],
help="Stream profile to use for the lower quality stream",
)
def get_stream_info(self, camera) -> tuple[int, int]:
info = camera.get_recording_encoding()
return (
info[0]["value"]["Enc"]["mainStream"]["frameRate"],
info[0]["value"]["Enc"]["subStream"]["frameRate"],
)
async def get_snapshot(self) -> Path:
img_file = Path(self.snapshot_dir, "screen.jpg")
url = (
f"http://{self.args.ip}"
f"/cgi-bin/api.cgi?cmd=Snap&channel={self.args.channel}"
f"&rs=6PHVjvf0UntSLbyT&user={self.args.username}"
f"&password={self.args.password}"
)
self.logger.info(f"Grabbing snapshot: {url}")
await self.fetch_to_file(url, img_file)
return img_file
async def run(self) -> None:
url = (
f"http://{self.args.ip}"
f"/api.cgi?cmd=GetMdState&user={self.args.username}"
f"&password={self.args.password}"
)
encoded_url = URL(url, encoded=True)
body = (
f'[{{ "cmd":"GetMdState", "param":{{ "channel":{self.args.channel} }} }}]'
)
while True:
self.logger.info(f"Connecting to motion events API: {url}")
try:
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(None)
) as session:
while True:
async with session.post(encoded_url, data=body) as resp:
data = await resp.read()
try:
json_body = json.loads(data)
if "value" in json_body[0]:
if json_body[0]["value"]["state"] == 1:
if not self.motion_in_progress:
self.motion_in_progress = True
self.logger.info("Trigger motion start")
await self.trigger_motion_start()
elif json_body[0]["value"]["state"] == 0:
if self.motion_in_progress:
self.motion_in_progress = False
self.logger.info("Trigger motion end")
await self.trigger_motion_stop()
else:
self.logger.error(
"Motion API request responded with "
"unexpected JSON, retrying. "
f"JSON: {data}"
)
except json.JSONDecodeError as err:
self.logger.error(
"Motion API request returned invalid "
"JSON, retrying. "
f"Error: {err}, "
f"Response: {data}"
)
except aiohttp.ClientError as err:
self.logger.error(f"Motion API request failed, retrying. Error: {err}")
def get_extra_ffmpeg_args(self, stream_index: str) -> str:
if stream_index == "video1":
fps = self.stream_fps[0]
else:
fps = self.stream_fps[1]
return (
"-ar 32000 -ac 1 -codec:a aac -b:a 32k -c:v copy -vbsf"
f' "h264_metadata=tick_rate={fps*2}"'
)
async def get_stream_source(self, stream_index: str) -> str:
if stream_index == "video1":
stream = self.args.stream
else:
stream = self.args.substream
return (
f"rtmp://{self.args.username}:{self.args.password}@{self.args.ip}:1935"
f"//h264Preview_{int(self.args.channel) + 1:02}_{stream}"
)