Skip to content

Commit

Permalink
started mjpeg support (keshavdv#231, keshavdv#347) but unable to test…
Browse files Browse the repository at this point in the history
… (maybe keshavdv#199)
  • Loading branch information
Klikini committed Jun 16, 2024
1 parent f0eb457 commit cd26119
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 12 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG version=3.9
ARG version=3.10
ARG tag=${version}-alpine3.17

FROM python:${tag} as builder
Expand Down
2 changes: 2 additions & 0 deletions unifi/cams/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unifi.cams.dahua import DahuaCam
from unifi.cams.frigate import FrigateCam
from unifi.cams.hikvision import HikvisionCam
from unifi.cams.mjpeg import MJPEGCam
from unifi.cams.reolink import Reolink
from unifi.cams.reolink_nvr import ReolinkNVRCam
from unifi.cams.rtsp import RTSPCam
Expand All @@ -9,6 +10,7 @@
"FrigateCam",
"HikvisionCam",
"DahuaCam",
"MJPEGCam",
"RTSPCam",
"Reolink",
"ReolinkNVRCam",
Expand Down
24 changes: 13 additions & 11 deletions unifi/cams/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,24 +919,26 @@ def get_base_ffmpeg_args(self, stream_index: str = "") -> str:

return " ".join(base_args)

async def get_ffmpeg_stream_command(self, stream_index: str, stream_name: str, destination: tuple[str, int]):
return (
"ffmpeg -nostdin -loglevel error -y"
f" {self.get_base_ffmpeg_args(stream_index)} -rtsp_transport"
f' {self.args.rtsp_transport} -i "{await self.get_stream_source(stream_index)}"'
f" {self.get_extra_ffmpeg_args(stream_index)} -metadata"
f" streamName={stream_name} -f flv - | {sys.executable} -m"
" unifi.clock_sync"
f" {'--write-timestamps' if self._needs_flv_timestamps else ''} | nc"
f" {destination[0]} {destination[1]}"
)

async def start_video_stream(
self, stream_index: str, stream_name: str, destination: tuple[str, int]
):
has_spawned = stream_index in self._ffmpeg_handles
is_dead = has_spawned and self._ffmpeg_handles[stream_index].poll() is not None

if not has_spawned or is_dead:
source = await self.get_stream_source(stream_index)
cmd = (
"ffmpeg -nostdin -loglevel error -y"
f" {self.get_base_ffmpeg_args(stream_index)} -rtsp_transport"
f' {self.args.rtsp_transport} -i "{source}"'
f" {self.get_extra_ffmpeg_args(stream_index)} -metadata"
f" streamName={stream_name} -f flv - | {sys.executable} -m"
" unifi.clock_sync"
f" {'--write-timestamps' if self._needs_flv_timestamps else ''} | nc"
f" {destination[0]} {destination[1]}"
)
cmd = await self.get_ffmpeg_stream_command(stream_index, stream_name, destination)

if is_dead:
self.logger.warn(f"Previous ffmpeg process for {stream_index} died.")
Expand Down
81 changes: 81 additions & 0 deletions unifi/cams/mjpeg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import argparse
import logging
import subprocess
import sys
import tempfile
from pathlib import Path

from unifi.cams.base import UnifiCamBase


class MJPEGCam(UnifiCamBase):
def __init__(self, args: argparse.Namespace, logger: logging.Logger) -> None:
super().__init__(args, logger)
self.args = args
self.event_id = 0
self.snapshot_dir = tempfile.mkdtemp()
self.snapshot_stream = None

if not self.args.snapshot_url:
self.start_snapshot_stream()

@classmethod
def add_parser(cls, parser: argparse.ArgumentParser) -> None:
super().add_parser(parser)
parser.add_argument(
"--source",
"-s",
required=True,
help="HTTP URL to stream MJPEG from",
)
parser.add_argument(
"--snapshot-url",
"-i",
default=None,
type=str,
required=False,
help="HTTP URL to fetch JPEG snapshot image from",
)

async def get_ffmpeg_stream_command(self, stream_index: str, stream_name: str, destination: tuple[str, int]):
if stream_index != "video1":
raise ValueError("MJPEG cameras only support 1 streaming quality")

return (
f"ffmpeg -f mjpeg -nostdin -loglevel error -y {self.get_base_ffmpeg_args(stream_index)}"
f' -i "{await self.get_stream_source(stream_index)}"'
f" {self.get_extra_ffmpeg_args(stream_index)} -metadata streamName={stream_name} -f flv -vcodec flv - "
f"| {sys.executable} -m unifi.clock_sync {'--write-timestamps' if self._needs_flv_timestamps else ''} "
f"| nc {destination[0]} {destination[1]}"
)

def start_snapshot_stream(self) -> None:
if not self.snapshot_stream or self.snapshot_stream.poll() is not None:
cmd = (
f"ffmpeg -f mjpeg -nostdin -y -re "
f'-i "{self.args.source}" -r 1 '
f"-update 1 {self.snapshot_dir}/screen.jpg"
)
self.logger.info(f"Spawning stream for snapshots: {cmd}")
self.snapshot_stream = subprocess.Popen(
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, shell=True
)

async def get_snapshot(self) -> Path:
img_file = Path(self.snapshot_dir, "screen.jpg")

if self.args.snapshot_url:
await self.fetch_to_file(self.args.snapshot_url, img_file)
else:
self.start_snapshot_stream()

return img_file

async def close(self) -> None:
await super().close()

if self.snapshot_stream:
self.snapshot_stream.kill()

async def get_stream_source(self, stream_index: str) -> str:
return self.args.source
2 changes: 2 additions & 0 deletions unifi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DahuaCam,
FrigateCam,
HikvisionCam,
MJPEGCam,
Reolink,
ReolinkNVRCam,
RTSPCam,
Expand All @@ -23,6 +24,7 @@
"dahua": DahuaCam,
"frigate": FrigateCam,
"hikvision": HikvisionCam,
"mjpeg": MJPEGCam,
"lorex": DahuaCam,
"reolink": Reolink,
"reolink_nvr": ReolinkNVRCam,
Expand Down

0 comments on commit cd26119

Please sign in to comment.