2023-07-06 08:28:50 -06:00
"""Utilities for services."""
2023-07-26 04:55:08 -06:00
import asyncio
2023-07-06 08:28:50 -06:00
import json
import logging
import os
import re
2025-07-11 08:11:35 -05:00
import resource
2025-08-22 14:48:27 -05:00
import shutil
2023-07-06 08:28:50 -06:00
import signal
import subprocess as sp
2025-10-17 07:06:41 -06:00
import time
2023-07-06 08:28:50 -06:00
import traceback
2025-02-10 09:38:56 -06:00
from datetime import datetime
2025-05-13 16:27:20 +02:00
from typing import Any , List , Optional , Tuple
2023-07-06 08:28:50 -06:00
import cv2
import psutil
import py3nvml.py3nvml as nvml
2024-01-01 09:36:40 -06:00
import requests
2023-07-06 08:28:50 -06:00
2024-02-20 15:21:54 -08:00
from frigate.const import (
DRIVER_AMD ,
DRIVER_ENV_VAR ,
FFMPEG_HWACCEL_NVIDIA ,
FFMPEG_HWACCEL_VAAPI ,
2025-08-22 14:48:27 -05:00
SHM_FRAMES_VAR ,
2024-02-20 15:21:54 -08:00
)
2024-01-03 07:30:25 -06:00
from frigate.util.builtin import clean_camera_user_pass , escape_special_characters
2023-07-06 08:28:50 -06:00
logger = logging . getLogger ( __name__ )
def restart_frigate ():
proc = psutil . Process ( 1 )
# if this is running via s6, sigterm pid 1
if proc . name () == "s6-svscan" :
proc . terminate ()
# otherwise, just try and exit frigate
else :
2024-06-06 18:54:38 -05:00
os . kill ( os . getpid (), signal . SIGINT )
2023-07-06 08:28:50 -06:00
def print_stack ( sig , frame ):
traceback . print_stack ( frame )
def listen ():
signal . signal ( signal . SIGUSR1 , print_stack )
def get_cgroups_version () -> str :
"""Determine what version of cgroups is enabled."""
cgroup_path = "/sys/fs/cgroup"
if not os . path . ismount ( cgroup_path ):
logger . debug ( f " { cgroup_path } is not a mount point." )
return "unknown"
try :
with open ( "/proc/mounts" , "r" ) as f :
mounts = f . readlines ()
for mount in mounts :
mount_info = mount . split ()
if mount_info [ 1 ] == cgroup_path :
fs_type = mount_info [ 2 ]
if fs_type == "cgroup2fs" or fs_type == "cgroup2" :
return "cgroup2"
elif fs_type == "tmpfs" :
return "cgroup"
else :
logger . debug (
f "Could not determine cgroups version: unhandled filesystem { fs_type } "
)
break
except Exception as e :
logger . debug ( f "Could not determine cgroups version: { e } " )
return "unknown"
def get_docker_memlimit_bytes () -> int :
"""Get mem limit in bytes set in docker if present. Returns -1 if no limit detected."""
# check running a supported cgroups version
if get_cgroups_version () == "cgroup2" :
memlimit_path = "/sys/fs/cgroup/memory.max"
try :
with open ( memlimit_path , "r" ) as f :
value = f . read () . strip ()
if value . isnumeric ():
return int ( value )
elif value . lower () == "max" :
return - 1
except Exception as e :
logger . debug ( f "Unable to get docker memlimit: { e } " )
return - 1
def get_cpu_stats () -> dict [ str , dict ]:
"""Get cpu usages for each process id"""
usages = {}
docker_memlimit = get_docker_memlimit_bytes () / 1024
total_mem = os . sysconf ( "SC_PAGE_SIZE" ) * os . sysconf ( "SC_PHYS_PAGES" ) / 1024
2024-02-21 19:27:02 -07:00
system_cpu = psutil . cpu_percent (
interval = None
) # no interval as we don't want to be blocking
system_mem = psutil . virtual_memory ()
usages [ "frigate.full_system" ] = {
"cpu" : str ( system_cpu ),
"mem" : str ( system_mem . percent ),
}
2023-07-06 08:28:50 -06:00
for process in psutil . process_iter ([ "pid" , "name" , "cpu_percent" , "cmdline" ]):
2024-02-21 19:27:02 -07:00
pid = str ( process . info [ "pid" ])
2023-07-06 08:28:50 -06:00
try :
cpu_percent = process . info [ "cpu_percent" ]
cmdline = process . info [ "cmdline" ]
with open ( f "/proc/ { pid } /stat" , "r" ) as f :
stats = f . readline () . split ()
utime = int ( stats [ 13 ])
stime = int ( stats [ 14 ])
2024-09-17 18:41:46 +03:00
start_time = int ( stats [ 21 ])
2023-07-06 08:28:50 -06:00
with open ( "/proc/uptime" ) as f :
system_uptime_sec = int ( float ( f . read () . split ()[ 0 ]))
clk_tck = os . sysconf ( os . sysconf_names [ "SC_CLK_TCK" ])
process_utime_sec = utime // clk_tck
process_stime_sec = stime // clk_tck
2024-09-17 18:41:46 +03:00
process_start_time_sec = start_time // clk_tck
2023-07-06 08:28:50 -06:00
2024-09-17 18:41:46 +03:00
process_elapsed_sec = system_uptime_sec - process_start_time_sec
2023-07-06 08:28:50 -06:00
process_usage_sec = process_utime_sec + process_stime_sec
cpu_average_usage = process_usage_sec * 100 // process_elapsed_sec
with open ( f "/proc/ { pid } /statm" , "r" ) as f :
mem_stats = f . readline () . split ()
mem_res = int ( mem_stats [ 1 ]) * os . sysconf ( "SC_PAGE_SIZE" ) / 1024
if docker_memlimit > 0 :
mem_pct = round (( mem_res / docker_memlimit ) * 100 , 1 )
else :
mem_pct = round (( mem_res / total_mem ) * 100 , 1 )
usages [ pid ] = {
"cpu" : str ( cpu_percent ),
"cpu_average" : str ( round ( cpu_average_usage , 2 )),
"mem" : f " { mem_pct } " ,
2024-01-03 07:30:25 -06:00
"cmdline" : clean_camera_user_pass ( " " . join ( cmdline )),
2023-07-06 08:28:50 -06:00
}
except Exception :
continue
return usages
def get_physical_interfaces ( interfaces ) -> list :
2023-09-21 04:26:22 -06:00
if not interfaces :
return []
2023-07-06 08:28:50 -06:00
with open ( "/proc/net/dev" , "r" ) as file :
lines = file . readlines ()
physical_interfaces = []
for line in lines :
if ":" in line :
interface = line . split ( ":" )[ 0 ] . strip ()
for int in interfaces :
if interface . startswith ( int ):
physical_interfaces . append ( interface )
return physical_interfaces
def get_bandwidth_stats ( config ) -> dict [ str , dict ]:
"""Get bandwidth usages for each ffmpeg process id"""
usages = {}
top_command = [ "nethogs" , "-t" , "-v0" , "-c5" , "-d1" ] + get_physical_interfaces (
config . telemetry . network_interfaces
)
p = sp . run (
top_command ,
encoding = "ascii" ,
capture_output = True ,
)
if p . returncode != 0 :
2023-09-21 04:26:22 -06:00
logger . error ( f "Error getting network stats :: { p . stderr } " )
2023-07-06 08:28:50 -06:00
return usages
else :
lines = p . stdout . split ( " \n " )
for line in lines :
stats = list ( filter ( lambda a : a != "" , line . strip () . split ( " \t " )))
try :
if re . search (
r "(^ffmpeg|\/go2rtc|frigate\.detector\.[a-z]+)/([0-9]+)/" , stats [ 0 ]
):
process = stats [ 0 ] . split ( "/" )
usages [ process [ len ( process ) - 2 ]] = {
"bandwidth" : round ( float ( stats [ 1 ]) + float ( stats [ 2 ]), 1 ),
}
except ( IndexError , ValueError ):
continue
return usages
2024-02-20 15:21:54 -08:00
def is_vaapi_amd_driver () -> bool :
# Use the explicitly configured driver, if available
driver = os . environ . get ( DRIVER_ENV_VAR )
if driver :
return driver == DRIVER_AMD
# Otherwise, ask vainfo what is has autodetected
p = vainfo_hwaccel ()
if p . returncode != 0 :
logger . error ( f "Unable to poll vainfo: { p . stderr } " )
return False
else :
output = p . stdout . decode ( "unicode_escape" ) . split ( " \n " )
# VA Info will print out the friendly name of the driver
return any ( "AMD Radeon Graphics" in line for line in output )
2025-05-13 16:27:20 +02:00
def get_amd_gpu_stats () -> Optional [ dict [ str , str ]]:
2023-07-06 08:28:50 -06:00
"""Get stats using radeontop."""
radeontop_command = [ "radeontop" , "-d" , "-" , "-l" , "1" ]
p = sp . run (
radeontop_command ,
encoding = "ascii" ,
capture_output = True ,
)
if p . returncode != 0 :
logger . error ( f "Unable to poll radeon GPU stats: { p . stderr } " )
return None
else :
usages = p . stdout . split ( "," )
results : dict [ str , str ] = {}
for hw in usages :
if "gpu" in hw :
results [ "gpu" ] = f " { hw . strip () . split ( ' ' )[ 1 ] . replace ( '%' , '' ) } %"
elif "vram" in hw :
results [ "mem" ] = f " { hw . strip () . split ( ' ' )[ 1 ] . replace ( '%' , '' ) } %"
return results
2025-07-14 05:11:25 -07:00
def get_intel_gpu_stats ( intel_gpu_device : Optional [ str ]) -> Optional [ dict [ str , str ]]:
2023-07-06 08:28:50 -06:00
"""Get stats using intel_gpu_top."""
2024-10-25 06:47:56 -06:00
def get_stats_manually ( output : str ) -> dict [ str , str ]:
"""Find global stats via regex when json fails to parse."""
reading = "" . join ( output )
results : dict [ str , str ] = {}
# render is used for qsv
render = []
for result in re . findall ( r '"Render/3D/0":{[a-z":\d.,%]+}' , reading ):
packet = json . loads ( result [ 14 :])
single = packet . get ( "busy" , 0.0 )
render . append ( float ( single ))
if render :
render_avg = sum ( render ) / len ( render )
else :
render_avg = 1
# video is used for vaapi
video = []
for result in re . findall ( r '"Video/\d":{[a-z":\d.,%]+}' , reading ):
packet = json . loads ( result [ 10 :])
single = packet . get ( "busy" , 0.0 )
video . append ( float ( single ))
if video :
video_avg = sum ( video ) / len ( video )
else :
video_avg = 1
results [ "gpu" ] = f " { round (( video_avg + render_avg ) / 2 , 2 ) } %"
results [ "mem" ] = "-%"
return results
2023-07-06 08:28:50 -06:00
intel_gpu_top_command = [
"timeout" ,
"0.5s" ,
"intel_gpu_top" ,
"-J" ,
"-o" ,
"-" ,
"-s" ,
2025-09-09 06:17:56 -06:00
"1000" , # Intel changed this from seconds to milliseconds in 2024+ versions
2023-07-06 08:28:50 -06:00
]
2025-07-14 05:11:25 -07:00
if intel_gpu_device :
intel_gpu_top_command += [ "-d" , intel_gpu_device ]
2025-01-03 09:43:44 -06:00
2025-06-17 13:11:19 -06:00
try :
p = sp . run (
intel_gpu_top_command ,
encoding = "ascii" ,
capture_output = True ,
)
except UnicodeDecodeError :
return None
2023-07-06 08:28:50 -06:00
# timeout has a non-zero returncode when timeout is reached
if p . returncode != 124 :
logger . error ( f "Unable to poll intel GPU stats: { p . stderr } " )
return None
else :
2024-10-21 16:19:34 -06:00
output = "" . join ( p . stdout . split ())
2024-10-21 08:38:48 -06:00
try :
2024-10-21 16:19:34 -06:00
data = json . loads ( f "[ { output } ]" )
2024-10-21 08:38:48 -06:00
except json . JSONDecodeError :
2024-10-25 06:47:56 -06:00
return get_stats_manually ( output )
2024-10-21 08:38:48 -06:00
2023-07-06 08:28:50 -06:00
results : dict [ str , str ] = {}
2024-09-30 15:41:14 -06:00
render = { "global" : []}
video = { "global" : []}
2023-07-06 08:28:50 -06:00
2024-09-30 15:41:14 -06:00
for block in data :
global_engine = block . get ( "engines" )
2023-07-06 08:28:50 -06:00
2024-09-30 15:41:14 -06:00
if global_engine :
render_frame = global_engine . get ( "Render/3D/0" , {}) . get ( "busy" )
video_frame = global_engine . get ( "Video/0" , {}) . get ( "busy" )
2023-07-06 08:28:50 -06:00
2024-09-30 15:41:14 -06:00
if render_frame is not None :
render [ "global" ] . append ( float ( render_frame ))
2023-07-06 08:28:50 -06:00
2024-09-30 15:41:14 -06:00
if video_frame is not None :
video [ "global" ] . append ( float ( video_frame ))
2023-07-06 08:28:50 -06:00
2024-09-30 15:41:14 -06:00
clients = block . get ( "clients" , {})
if clients and len ( clients ):
for client_block in clients . values ():
key = client_block [ "pid" ]
if render . get ( key ) is None :
render [ key ] = []
video [ key ] = []
client_engine = client_block . get ( "engine-classes" , {})
render_frame = client_engine . get ( "Render/3D" , {}) . get ( "busy" )
video_frame = client_engine . get ( "Video" , {}) . get ( "busy" )
if render_frame is not None :
render [ key ] . append ( float ( render_frame ))
if video_frame is not None :
video [ key ] . append ( float ( video_frame ))
2025-03-06 17:21:16 -05:00
if render [ "global" ] and video [ "global" ]:
2024-10-10 07:09:12 -06:00
results [ "gpu" ] = (
f " { round ((( sum ( render [ 'global' ]) / len ( render [ 'global' ])) + ( sum ( video [ 'global' ]) / len ( video [ 'global' ]))) / 2 , 2 ) } %"
)
results [ "mem" ] = "-%"
2024-09-30 15:41:14 -06:00
if len ( render . keys ()) > 1 :
results [ "clients" ] = {}
for key in render . keys ():
2024-10-21 08:38:48 -06:00
if key == "global" or not render [ key ] or not video [ key ]:
2024-09-30 15:41:14 -06:00
continue
results [ "clients" ][ key ] = (
f " { round ((( sum ( render [ key ]) / len ( render [ key ])) + ( sum ( video [ key ]) / len ( video [ key ]))) / 2 , 2 ) } %"
)
2023-07-06 08:28:50 -06:00
return results
2025-10-17 07:06:41 -06:00
def get_openvino_npu_stats () -> Optional [ dict [ str , str ]]:
"""Get NPU stats using openvino."""
NPU_RUNTIME_PATH = "/sys/devices/pci0000:00/0000:00:0b.0/power/runtime_active_time"
try :
with open ( NPU_RUNTIME_PATH , "r" ) as f :
initial_runtime = float ( f . read () . strip ())
initial_time = time . time ()
# Sleep for 1 second to get an accurate reading
time . sleep ( 1.0 )
# Read runtime value again
with open ( NPU_RUNTIME_PATH , "r" ) as f :
current_runtime = float ( f . read () . strip ())
current_time = time . time ()
# Calculate usage percentage
runtime_diff = current_runtime - initial_runtime
time_diff = ( current_time - initial_time ) * 1000.0 # Convert to milliseconds
if time_diff > 0 :
usage = min ( 100.0 , max ( 0.0 , ( runtime_diff / time_diff * 100.0 )))
else :
usage = 0.0
return { "npu" : f " { round ( usage , 2 ) } " , "mem" : "-" }
except ( FileNotFoundError , PermissionError , ValueError ):
return None
2025-05-13 16:27:20 +02:00
def get_rockchip_gpu_stats () -> Optional [ dict [ str , str ]]:
2025-04-19 15:34:05 -06:00
"""Get GPU stats using rk."""
try :
with open ( "/sys/kernel/debug/rkrga/load" , "r" ) as f :
content = f . read ()
except FileNotFoundError :
return None
load_values = []
for line in content . splitlines ():
match = re . search ( r "load = (\d+)%" , line )
if match :
load_values . append ( int ( match . group ( 1 )))
if not load_values :
return None
average_load = f " { round ( sum ( load_values ) / len ( load_values ), 2 ) } %"
return { "gpu" : average_load , "mem" : "-" }
2025-05-13 16:27:20 +02:00
def get_rockchip_npu_stats () -> Optional [ dict [ str , float | str ]]:
2025-04-19 15:34:05 -06:00
"""Get NPU stats using rk."""
2025-04-19 08:20:22 -06:00
try :
with open ( "/sys/kernel/debug/rknpu/load" , "r" ) as f :
npu_output = f . read ()
2025-05-04 09:33:27 -06:00
if "Core0:" in npu_output :
# multi core NPU
core_loads = re . findall ( r "Core\d+:\s*(\d+)%" , npu_output )
else :
# single core NPU
core_loads = re . findall ( r "NPU load:\s+(\d+)%" , npu_output )
2025-04-19 08:20:22 -06:00
except FileNotFoundError :
core_loads = None
if not core_loads :
return None
percentages = [ int ( load ) for load in core_loads ]
mean = round ( sum ( percentages ) / len ( percentages ), 2 )
return { "npu" : mean , "mem" : "-" }
2023-07-06 08:28:50 -06:00
def try_get_info ( f , h , default = "N/A" ):
try :
2024-10-07 20:15:31 -06:00
if h :
v = f ( h )
else :
v = f ()
2023-07-06 08:28:50 -06:00
except nvml . NVMLError_NotSupported :
v = default
return v
def get_nvidia_gpu_stats () -> dict [ int , dict ]:
2025-01-08 16:17:57 -07:00
names : dict [ str , int ] = {}
2023-07-06 08:28:50 -06:00
results = {}
try :
nvml . nvmlInit ()
deviceCount = nvml . nvmlDeviceGetCount ()
for i in range ( deviceCount ):
handle = nvml . nvmlDeviceGetHandleByIndex ( i )
2025-01-08 16:17:57 -07:00
gpu_name = nvml . nvmlDeviceGetName ( handle )
# handle case where user has multiple of same GPU
if gpu_name in names :
names [ gpu_name ] += 1
gpu_name += f " ( { names . get ( gpu_name ) } )"
else :
names [ gpu_name ] = 1
2023-07-06 08:28:50 -06:00
meminfo = try_get_info ( nvml . nvmlDeviceGetMemoryInfo , handle )
util = try_get_info ( nvml . nvmlDeviceGetUtilizationRates , handle )
2023-10-13 08:44:18 -06:00
enc = try_get_info ( nvml . nvmlDeviceGetEncoderUtilization , handle )
dec = try_get_info ( nvml . nvmlDeviceGetDecoderUtilization , handle )
2024-10-07 20:15:31 -06:00
pstate = try_get_info ( nvml . nvmlDeviceGetPowerState , handle , default = None )
2023-07-06 08:28:50 -06:00
if util != "N/A" :
gpu_util = util . gpu
else :
gpu_util = 0
if meminfo != "N/A" :
gpu_mem_util = meminfo . used / meminfo . total * 100
else :
gpu_mem_util = - 1
2023-10-13 08:44:18 -06:00
if enc != "N/A" :
enc_util = enc [ 0 ]
else :
enc_util = - 1
if dec != "N/A" :
dec_util = dec [ 0 ]
else :
dec_util = - 1
2023-07-06 08:28:50 -06:00
results [ i ] = {
2025-01-08 16:17:57 -07:00
"name" : gpu_name ,
2023-07-06 08:28:50 -06:00
"gpu" : gpu_util ,
"mem" : gpu_mem_util ,
2023-10-13 08:44:18 -06:00
"enc" : enc_util ,
"dec" : dec_util ,
2024-10-07 20:15:31 -06:00
"pstate" : pstate or "unknown" ,
2023-07-06 08:28:50 -06:00
}
except Exception :
pass
finally :
return results
2025-05-13 16:27:20 +02:00
def get_jetson_stats () -> Optional [ dict [ int , dict ]]:
2023-07-26 06:50:41 -04:00
results = {}
try :
results [ "mem" ] = "-" # no discrete gpu memory
2026-01-15 07:08:49 -07:00
if os . path . exists ( "/sys/devices/gpu.0/load" ):
with open ( "/sys/devices/gpu.0/load" , "r" ) as f :
gpuload = float ( f . readline ()) / 10
results [ "gpu" ] = f " { gpuload } %"
elif os . path . exists ( "/sys/devices/platform/gpu.0/load" ):
with open ( "/sys/devices/platform/gpu.0/load" , "r" ) as f :
gpuload = float ( f . readline ()) / 10
results [ "gpu" ] = f " { gpuload } %"
else :
results [ "gpu" ] = "-"
2023-07-26 06:50:41 -04:00
except Exception :
return None
return results
2025-10-13 11:52:08 -05:00
def ffprobe_stream ( ffmpeg , path : str , detailed : bool = False ) -> sp . CompletedProcess :
2023-07-06 08:28:50 -06:00
"""Run ffprobe on stream."""
clean_path = escape_special_characters ( path )
2025-10-13 11:52:08 -05:00
# Base entries that are always included
stream_entries = "codec_long_name,width,height,bit_rate,duration,display_aspect_ratio,avg_frame_rate"
# Additional detailed entries
if detailed :
stream_entries += ",codec_name,profile,level,pix_fmt,channels,sample_rate,channel_layout,r_frame_rate"
format_entries = "format_name,size,bit_rate,duration"
else :
format_entries = None
2023-07-06 08:28:50 -06:00
ffprobe_cmd = [
2024-09-13 14:14:51 -06:00
ffmpeg . ffprobe_path ,
2023-07-06 08:28:50 -06:00
"-timeout" ,
"1000000" ,
"-print_format" ,
"json" ,
"-show_entries" ,
2025-10-13 11:52:08 -05:00
f "stream= { stream_entries } " ,
2023-07-06 08:28:50 -06:00
]
2025-10-13 11:52:08 -05:00
# Add format entries for detailed mode
if detailed and format_entries :
ffprobe_cmd . extend ([ "-show_entries" , f "format= { format_entries } " ])
2025-10-23 08:34:52 -05:00
ffprobe_cmd . extend ([ "-loglevel" , "error" , clean_path ])
2025-10-13 11:52:08 -05:00
2023-07-06 08:28:50 -06:00
return sp . run ( ffprobe_cmd , capture_output = True )
def vainfo_hwaccel ( device_name : Optional [ str ] = None ) -> sp . CompletedProcess :
"""Run vainfo."""
ffprobe_cmd = (
[ "vainfo" ]
if not device_name
else [ "vainfo" , "--display" , "drm" , "--device" , f "/dev/dri/ { device_name } " ]
)
return sp . run ( ffprobe_cmd , capture_output = True )
2025-05-13 16:27:20 +02:00
def get_nvidia_driver_info () -> dict [ str , Any ]:
2024-10-07 20:15:31 -06:00
"""Get general hardware info for nvidia GPU."""
results = {}
try :
nvml . nvmlInit ()
deviceCount = nvml . nvmlDeviceGetCount ()
for i in range ( deviceCount ):
handle = nvml . nvmlDeviceGetHandleByIndex ( i )
driver = try_get_info ( nvml . nvmlSystemGetDriverVersion , None , default = None )
cuda_compute = try_get_info (
nvml . nvmlDeviceGetCudaComputeCapability , handle , default = None
)
vbios = try_get_info ( nvml . nvmlDeviceGetVbiosVersion , handle , default = None )
results [ i ] = {
"name" : nvml . nvmlDeviceGetName ( handle ),
"driver" : driver or "unknown" ,
"cuda_compute" : cuda_compute or "unknown" ,
"vbios" : vbios or "unknown" ,
}
except Exception :
pass
finally :
return results
2024-01-01 09:36:40 -06:00
def auto_detect_hwaccel () -> str :
"""Detect hwaccel args by default."""
try :
cuda = False
vaapi = False
2024-01-31 05:29:18 -07:00
resp = requests . get ( "http://127.0.0.1:1984/api/ffmpeg/hardware" , timeout = 3 )
2024-01-01 09:36:40 -06:00
if resp . status_code == 200 :
data : dict [ str , list [ dict [ str , str ]]] = resp . json ()
for source in data . get ( "sources" , []):
if "cuda" in source . get ( "url" , "" ) and source . get ( "name" ) == "OK" :
cuda = True
if "vaapi" in source . get ( "url" , "" ) and source . get ( "name" ) == "OK" :
vaapi = True
except requests . RequestException :
pass
if cuda :
logger . info ( "Automatically detected nvidia hwaccel for video decoding" )
return FFMPEG_HWACCEL_NVIDIA
if vaapi :
logger . info ( "Automatically detected vaapi hwaccel for video decoding" )
return FFMPEG_HWACCEL_VAAPI
logger . warning (
"Did not detect hwaccel, using a GPU for accelerated video decoding is highly recommended"
)
return ""
2024-09-13 14:14:51 -06:00
async def get_video_properties (
ffmpeg , url : str , get_duration : bool = False
2025-05-13 16:27:20 +02:00
) -> dict [ str , Any ]:
2025-09-28 11:52:14 -05:00
async def probe_with_ffprobe (
url : str ,
) -> tuple [ bool , int , int , Optional [ str ], float ]:
"""Fallback using ffprobe: returns (valid, width, height, codec, duration)."""
cmd = [
ffmpeg . ffprobe_path ,
"-v" ,
"quiet" ,
"-print_format" ,
"json" ,
"-show_format" ,
"-show_streams" ,
url ,
]
try :
proc = await asyncio . create_subprocess_exec (
* cmd , stdout = asyncio . subprocess . PIPE , stderr = asyncio . subprocess . PIPE
2023-07-26 04:55:08 -06:00
)
2025-09-28 11:52:14 -05:00
stdout , _ = await proc . communicate ()
if proc . returncode != 0 :
return False , 0 , 0 , None , - 1
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
data = json . loads ( stdout . decode ())
video_streams = [
s for s in data . get ( "streams" , []) if s . get ( "codec_type" ) == "video"
]
if not video_streams :
return False , 0 , 0 , None , - 1
2023-07-26 04:55:08 -06:00
2025-09-28 11:52:14 -05:00
v = video_streams [ 0 ]
width = int ( v . get ( "width" , 0 ))
height = int ( v . get ( "height" , 0 ))
codec = v . get ( "codec_name" )
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
duration_str = data . get ( "format" , {}) . get ( "duration" )
duration = float ( duration_str ) if duration_str else - 1.0
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
return True , width , height , codec , duration
except ( json . JSONDecodeError , ValueError , KeyError , asyncio . SubprocessError ):
return False , 0 , 0 , None , - 1
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
def probe_with_cv2 ( url : str ) -> tuple [ bool , int , int , Optional [ str ], float ]:
"""Primary attempt using cv2: returns (valid, width, height, fourcc, duration)."""
cap = cv2 . VideoCapture ( url )
if not cap . isOpened ():
cap . release ()
return False , 0 , 0 , None , - 1
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
width = int ( cap . get ( cv2 . CAP_PROP_FRAME_WIDTH ))
height = int ( cap . get ( cv2 . CAP_PROP_FRAME_HEIGHT ))
valid = width > 0 and height > 0
fourcc = None
duration = - 1.0
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
if valid :
fourcc_int = int ( cap . get ( cv2 . CAP_PROP_FOURCC ))
fourcc = fourcc_int . to_bytes ( 4 , "little" ) . decode ( "latin-1" ) . strip ()
2023-07-06 08:28:50 -06:00
2025-09-28 11:52:14 -05:00
if get_duration :
fps = cap . get ( cv2 . CAP_PROP_FPS )
total_frames = int ( cap . get ( cv2 . CAP_PROP_FRAME_COUNT ))
if fps > 0 and total_frames > 0 :
duration = total_frames / fps
cap . release ()
return valid , width , height , fourcc , duration
# try cv2 first
has_video , width , height , fourcc , duration = probe_with_cv2 ( url )
# fallback to ffprobe if needed
if not has_video or ( get_duration and duration < 0 ):
has_video , width , height , fourcc , duration = await probe_with_ffprobe ( url )
result : dict [ str , Any ] = { "has_valid_video" : has_video }
if has_video :
result . update ({ "width" : width , "height" : height })
if fourcc :
result [ "fourcc" ] = fourcc
2023-07-06 08:28:50 -06:00
if get_duration :
2025-09-28 11:52:14 -05:00
result [ "duration" ] = duration
2023-07-06 08:28:50 -06:00
return result
2025-02-10 09:38:56 -06:00
def process_logs (
contents : str ,
service : Optional [ str ] = None ,
start : Optional [ int ] = None ,
end : Optional [ int ] = None ,
) -> Tuple [ int , List [ str ]]:
log_lines = []
last_message = None
last_timestamp = None
repeat_count = 0
for raw_line in contents . splitlines ():
clean_line = raw_line . strip ()
if len ( clean_line ) < 10 :
continue
# Handle cases where S6 does not include date in log line
if " " not in clean_line :
clean_line = f " { datetime . now () } { clean_line } "
2025-02-23 07:25:50 -06:00
try :
# Find the position of the first double space to extract timestamp and message
date_end = clean_line . index ( " " )
timestamp = clean_line [: date_end ]
full_message = clean_line [ date_end :] . strip ()
2025-02-10 09:38:56 -06:00
2025-02-23 07:25:50 -06:00
# For frigate, remove the date part from message comparison
if service == "frigate" :
# Skip the date at the start of the message if it exists
date_parts = full_message . split ( "]" , 1 )
if len ( date_parts ) > 1 :
message_part = date_parts [ 1 ] . strip ()
else :
message_part = full_message
else :
message_part = full_message
2025-02-10 09:38:56 -06:00
2025-02-23 07:25:50 -06:00
if message_part == last_message :
repeat_count += 1
continue
else :
if repeat_count > 0 :
# Insert a deduplication message formatted the same way as logs
dedup_message = f " { last_timestamp } [LOGGING] Last message repeated { repeat_count } times"
log_lines . append ( dedup_message )
repeat_count = 0
log_lines . append ( clean_line )
last_timestamp = timestamp
last_message = message_part
except ValueError :
# If we can't parse the line properly, just add it as is
2025-02-10 09:38:56 -06:00
log_lines . append ( clean_line )
2025-02-23 07:25:50 -06:00
continue
2025-02-10 09:38:56 -06:00
# If there were repeated messages at the end, log the count
if repeat_count > 0 :
dedup_message = (
f " { last_timestamp } [LOGGING] Last message repeated { repeat_count } times"
)
log_lines . append ( dedup_message )
return len ( log_lines ), log_lines [ start : end ]
2025-07-11 20:20:31 -05:00
2025-07-11 08:11:35 -05:00
def set_file_limit () -> None :
# Newer versions of containerd 2.X+ impose a very low soft file limit of 1024
# This applies to OSs like HA OS (see https://github.com/home-assistant/operating-system/issues/4110)
# Attempt to increase this limit
soft_limit = int ( os . getenv ( "SOFT_FILE_LIMIT" , "65536" ) or "65536" )
current_soft , current_hard = resource . getrlimit ( resource . RLIMIT_NOFILE )
2025-07-11 20:20:31 -05:00
logger . debug ( f "Current file limits - Soft: { current_soft } , Hard: { current_hard } " )
2025-07-11 08:11:35 -05:00
new_soft = min ( soft_limit , current_hard )
resource . setrlimit ( resource . RLIMIT_NOFILE , ( new_soft , current_hard ))
2025-07-11 20:20:31 -05:00
logger . debug (
2025-07-11 08:11:35 -05:00
f "File limit set. New soft limit: { new_soft } , Hard limit remains: { current_hard } "
)
2025-08-22 14:48:27 -05:00
def get_fs_type ( path : str ) -> str :
bestMatch = ""
fsType = ""
for part in psutil . disk_partitions ( all = True ):
if path . startswith ( part . mountpoint ) and len ( bestMatch ) < len ( part . mountpoint ):
fsType = part . fstype
bestMatch = part . mountpoint
return fsType
def calculate_shm_requirements ( config ) -> dict :
try :
storage_stats = shutil . disk_usage ( "/dev/shm" )
except ( FileNotFoundError , OSError ):
return {}
total_mb = round ( storage_stats . total / pow ( 2 , 20 ), 1 )
used_mb = round ( storage_stats . used / pow ( 2 , 20 ), 1 )
free_mb = round ( storage_stats . free / pow ( 2 , 20 ), 1 )
# required for log files + nginx cache
min_req_shm = 40 + 10
if config . birdseye . restream :
min_req_shm += 8
available_shm = total_mb - min_req_shm
cam_total_frame_size = 0.0
for camera in config . cameras . values ():
if camera . enabled_in_config and camera . detect . width and camera . detect . height :
cam_total_frame_size += round (
( camera . detect . width * camera . detect . height * 1.5 + 270480 ) / 1048576 ,
1 ,
)
# leave room for 2 cameras that are added dynamically, if a user wants to add more cameras they may need to increase the SHM size and restart after adding them.
cam_total_frame_size += 2 * round (
( 1280 * 720 * 1.5 + 270480 ) / 1048576 ,
1 ,
)
shm_frame_count = min (
int ( os . environ . get ( SHM_FRAMES_VAR , "50" )),
int ( available_shm / cam_total_frame_size ),
)
# minimum required shm recommendation
min_shm = round ( min_req_shm + cam_total_frame_size * 20 )
return {
"total" : total_mb ,
"used" : used_mb ,
"free" : free_mb ,
"mount_type" : get_fs_type ( "/dev/shm" ),
"available" : round ( available_shm , 1 ),
"camera_frame_size" : cam_total_frame_size ,
"shm_frame_count" : shm_frame_count ,
"min_shm" : min_shm ,
}