Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to restream birdseye #4761

Merged
merged 13 commits into from
Dec 31, 2022
3 changes: 3 additions & 0 deletions docs/docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ restream:
enabled: True
# Optional: Force audio compatibility with browsers (default: shown below)
force_audio: True
# Optional: Restream birdseye via RTSP (default: shown below)
# NOTE: Enabling this will set birdseye to run 24/7 which may increase CPU usage somewhat.
birdseye: False
# Optional: jsmpeg stream configuration for WebUI
jsmpeg:
# Optional: Set the height of the jsmpeg stream. (default: 720)
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/live.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Live view options can be selected while viewing the live stream. The options are
| Source | Latency | Frame Rate | Resolution | Audio | Requires Restream | Other Limitations |
| ------ | ------- | -------------------------------------- | -------------- | ---------------------------- | ----------------- | --------------------- |
| jsmpeg | low | same as `detect -> fps`, capped at 10 | same as detect | no | no | none |
| mse | low | native | native | yes (depends on audio codec) | yes | none |
| mse | low | native | native | yes (depends on audio codec) | yes | not supported on iOS |
| webrtc | lowest | native | native | yes (depends on audio codec) | yes | requires extra config |

### WebRTC extra configuration:
Expand Down
8 changes: 8 additions & 0 deletions docs/docs/configuration/restream.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ title: Restream

Frigate can restream your video feed as an RTSP feed for other applications such as Home Assistant to utilize it at `rtsp://<frigate_host>:8554/<camera_name>`. Port 8554 must be open. [This allows you to use a video feed for detection in frigate and Home Assistant live view at the same time without having to make two separate connections to the camera](#reduce-connections-to-camera). The video feed is copied from the original video feed directly to avoid re-encoding. This feed does not include any annotation by Frigate.

#### Force Audio

Different live view technologies (ex: MSE, WebRTC) support different audio codecs. The `restream -> force_audio` flag tells the restream to make multiple streams available so that all live view technologies are supported. Some camera streams don't work well with this, in which case `restream -> force_audio` should be disabled.

#### Birdseye Restream

Birdseye RTSP restream can be enabled at `restream -> birdseye` and accessed at `rtsp://<frigate_host>:8554/birdseye`. Enabling the restream will cause birdseye to run 24/7 which may increase CPU usage somewhat.

### RTMP (Deprecated)

In previous Frigate versions RTMP was used for re-streaming. RTMP has disadvantages however including being incompatible with H.265, high bitrates, and certain audio codecs. RTMP is deprecated and it is recommended to move to the new restream role.
Expand Down
1 change: 1 addition & 0 deletions frigate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ class RestreamConfig(FrigateBaseModel):
force_audio: bool = Field(
default=True, title="Force audio compatibility with the browser."
)
birdseye: bool = Field(default=False, title="Restream the birdseye feed via RTSP.")
jsmpeg: JsmpegStreamConfig = Field(
default_factory=JsmpegStreamConfig, title="Jsmpeg Stream Configuration."
)
Expand Down
1 change: 1 addition & 0 deletions frigate/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
BASE_DIR = "/media/frigate"
CLIPS_DIR = f"{BASE_DIR}/clips"
RECORD_DIR = f"{BASE_DIR}/recordings"
BIRDSEYE_PIPE = "/tmp/cache/birdseye"
CACHE_DIR = "/tmp/cache"
YAML_EXT = (".yaml", ".yml")
PLUS_ENV_VAR = "PLUS_API_KEY"
Expand Down
109 changes: 109 additions & 0 deletions frigate/ffmpeg_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,107 @@
],
}

PRESETS_HW_ACCEL_ENCODE = {
"preset-intel-vaapi": [
"-c:v",
"h264_vaapi",
"-g",
"50",
"-bf",
"0",
"-profile:v",
"high",
"-level:v",
"4.1",
"-sei:v",
"0",
],
"preset-intel-qsv-h264": [
"-c:v",
"h264_qsv",
"-g",
"50",
"-bf",
"0",
"-profile:v",
"high",
"-level:v",
"4.1",
"-async_depth:v",
"1",
],
"preset-intel-qsv-h265": [
"-c:v",
"h264_qsv",
"-g",
"50",
"-bf",
"0",
"-profile:v",
"high",
"-level:v",
"4.1",
"-async_depth:v",
"1",
],
"preset-amd-vaapi": [
"-c:v",
"h264_vaapi",
"-g",
"50",
"-bf",
"0",
"-profile:v",
"high",
"-level:v",
"4.1",
"-sei:v",
"0",
],
"preset-nvidia-h264": [
"-c:v",
"h264_nvenc",
"-g",
"50",
"-profile:v",
"high",
"-level:v",
"auto",
"-preset:v",
"p2",
"-tune:v",
"ll",
],
"preset-nvidia-h265": [
"-c:v",
"h264_nvenc",
"-g",
"50",
"-profile:v",
"high",
"-level:v",
"auto",
"-preset:v",
"p2",
"-tune:v",
"ll",
],
"default": [
"-c:v",
"libx264",
"-g",
"50",
"-profile:v",
"high",
"-level:v",
"4.1",
"-preset:v",
"superfast",
"-tune:v",
"zerolatency",
],
}


def parse_preset_hardware_acceleration_decode(arg: Any) -> list[str]:
"""Return the correct preset if in preset format otherwise return None."""
Expand Down Expand Up @@ -158,6 +259,14 @@ def parse_preset_hardware_acceleration_scale(
return scale


def parse_preset_hardware_acceleration_encode(arg: Any) -> list[str]:
"""Return the correct scaling preset or default preset if none is set."""
if not isinstance(arg, str):
return PRESETS_HW_ACCEL_ENCODE["default"]

return PRESETS_HW_ACCEL_ENCODE.get(arg, PRESETS_HW_ACCEL_ENCODE["default"])


PRESETS_INPUT = {
"preset-http-jpeg-generic": _user_agent_args
+ [
Expand Down
18 changes: 18 additions & 0 deletions frigate/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,24 @@ def latest_frame(camera_name):

frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)

ret, jpg = cv2.imencode(
".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
)
response = make_response(jpg.tobytes())
response.headers["Content-Type"] = "image/jpeg"
response.headers["Cache-Control"] = "no-store"
return response
elif camera_name == "birdseye" and current_app.frigate_config.restream.birdseye:
frame = cv2.cvtColor(
current_app.detected_frames_processor.get_current_frame(camera_name),
cv2.COLOR_YUV2BGR_I420,
)

height = int(request.args.get("h", str(frame.shape[0])))
width = int(height * frame.shape[1] / frame.shape[0])

frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA)

ret, jpg = cv2.imencode(
".jpg", frame, [int(cv2.IMWRITE_JPEG_QUALITY), resize_quality]
)
Expand Down
6 changes: 6 additions & 0 deletions frigate/object_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,12 @@ def get_best(self, camera, label):
return {}

def get_current_frame(self, camera, draw_options={}):
if camera == "birdseye":
return self.frame_manager.get(
"birdseye",
(self.config.birdseye.height * 3 // 2, self.config.birdseye.width),
)

return self.camera_states[camera].get_current_frame(draw_options)

def get_current_frame_time(self, camera) -> int:
Expand Down
88 changes: 76 additions & 12 deletions frigate/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import math
import multiprocessing as mp
import os
import queue
import signal
import subprocess as sp
Expand All @@ -21,17 +22,56 @@
from ws4py.websocket import WebSocket

from frigate.config import BirdseyeModeEnum, FrigateConfig
from frigate.const import BASE_DIR
from frigate.const import BASE_DIR, BIRDSEYE_PIPE
from frigate.util import SharedMemoryFrameManager, copy_yuv_to_position, get_yuv_crop

logger = logging.getLogger(__name__)


class FFMpegConverter:
def __init__(self, in_width, in_height, out_width, out_height, quality):
ffmpeg_cmd = f"ffmpeg -f rawvideo -pix_fmt yuv420p -video_size {in_width}x{in_height} -i pipe: -f mpegts -s {out_width}x{out_height} -codec:v mpeg1video -q {quality} -bf 0 pipe:".split(
" "
)
def __init__(
self,
in_width: int,
in_height: int,
out_width: int,
out_height: int,
quality: int,
birdseye_rtsp: bool = False,
):
if birdseye_rtsp:
if os.path.exists(BIRDSEYE_PIPE):
os.remove(BIRDSEYE_PIPE)

os.mkfifo(BIRDSEYE_PIPE, mode=0o777)
stdin = os.open(BIRDSEYE_PIPE, os.O_RDONLY | os.O_NONBLOCK)
self.bd_pipe = os.open(BIRDSEYE_PIPE, os.O_WRONLY)
os.close(stdin)
else:
self.bd_pipe = None

ffmpeg_cmd = [
"ffmpeg",
"-f",
"rawvideo",
"-pix_fmt",
"yuv420p",
"-video_size",
f"{in_width}x{in_height}",
"-i",
"pipe:",
"-f",
"mpegts",
"-s",
f"{out_width}x{out_height}",
"-codec:v",
"mpeg1video",
"-q",
f"{quality}",
"-bf",
"0",
"pipe:",
]

self.process = sp.Popen(
ffmpeg_cmd,
stdout=sp.PIPE,
Expand All @@ -40,16 +80,26 @@ def __init__(self, in_width, in_height, out_width, out_height, quality):
start_new_session=True,
)

def write(self, b):
def write(self, b) -> None:
self.process.stdin.write(b)

if self.bd_pipe:
try:
os.write(self.bd_pipe, b)
except BrokenPipeError:
# catch error when no one is listening
return

def read(self, length):
try:
return self.process.stdout.read1(length)
except ValueError:
return False

def exit(self):
if self.bd_pipe:
os.close(self.bd_pipe)

self.process.terminate()
try:
self.process.communicate(timeout=30)
Expand Down Expand Up @@ -88,7 +138,7 @@ def run(self):


class BirdsEyeFrameManager:
def __init__(self, config, frame_manager: SharedMemoryFrameManager):
def __init__(self, config: FrigateConfig, frame_manager: SharedMemoryFrameManager):
self.config = config
self.mode = config.birdseye.mode
self.frame_manager = frame_manager
Expand Down Expand Up @@ -386,6 +436,7 @@ def receiveSignal(signalNumber, frame):
config.birdseye.width,
config.birdseye.height,
config.birdseye.quality,
config.restream.birdseye,
)
broadcasters["birdseye"] = BroadcastThread(
"birdseye", converters["birdseye"], websocket_server
Expand All @@ -398,6 +449,12 @@ def receiveSignal(signalNumber, frame):

birdseye_manager = BirdsEyeFrameManager(config, frame_manager)

if config.restream.birdseye:
birdseye_buffer = frame_manager.create(
"birdseye",
birdseye_manager.yuv_shape[0] * birdseye_manager.yuv_shape[1],
)

while not stop_event.is_set():
try:
(
Expand All @@ -421,10 +478,12 @@ def receiveSignal(signalNumber, frame):
# write to the converter for the camera if clients are listening to the specific camera
converters[camera].write(frame.tobytes())

# update birdseye if websockets are connected
if config.birdseye.enabled and any(
ws.environ["PATH_INFO"].endswith("birdseye")
for ws in websocket_server.manager
if config.birdseye.enabled and (
config.restream.birdseye
or any(
ws.environ["PATH_INFO"].endswith("birdseye")
for ws in websocket_server.manager
)
):
if birdseye_manager.update(
camera,
Expand All @@ -433,7 +492,12 @@ def receiveSignal(signalNumber, frame):
frame_time,
frame,
):
converters["birdseye"].write(birdseye_manager.frame.tobytes())
frame_bytes = birdseye_manager.frame.tobytes()

if config.restream.birdseye:
birdseye_buffer[:] = frame_bytes

converters["birdseye"].write(frame_bytes)

if camera in previous_frames:
frame_manager.delete(f"{camera}{previous_frames[camera]}")
Expand Down
7 changes: 7 additions & 0 deletions frigate/restream.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from frigate.util import escape_special_characters

from frigate.config import FrigateConfig
from frigate.const import BIRDSEYE_PIPE
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -42,6 +44,11 @@ def add_cameras(self) -> None:
escape_special_characters(input.path)
)

if self.config.restream.birdseye:
self.relays[
"birdseye"
] = f"exec:ffmpeg -hide_banner -f rawvideo -pix_fmt yuv420p -video_size {self.config.birdseye.width}x{self.config.birdseye.height} -r 10 -i {BIRDSEYE_PIPE} {' '.join(parse_preset_hardware_acceleration_encode(self.config.ffmpeg.hwaccel_args))} -rtsp_transport tcp -f rtsp {{output}}"

for name, path in self.relays.items():
params = {"src": path, "name": name}
requests.put("http://127.0.0.1:1984/api/streams", params=params)
Loading