Send downloaded mp4 as a streaming response instead of a file

This commit is contained in:
Nicolas Mowen 2024-10-14 14:11:23 -06:00
parent dd7a07bd0d
commit acc170e64d

View File

@ -7,6 +7,7 @@ import os
import subprocess as sp import subprocess as sp
import time import time
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path as FilePath
from urllib.parse import unquote from urllib.parse import unquote
import cv2 import cv2
@ -450,8 +451,27 @@ def recording_clip(
camera_name: str, camera_name: str,
start_ts: float, start_ts: float,
end_ts: float, end_ts: float,
download: bool = False,
): ):
def run_download(ffmpeg_cmd: list[str], file_path: str):
with sp.Popen(
ffmpeg_cmd,
stderr=sp.PIPE,
stdout=sp.PIPE,
text=False,
) as ffmpeg:
while True:
data = ffmpeg.stdout.read(1024)
if not data:
logger.error(f"the returncode is {ffmpeg.returncode}")
if ffmpeg.returncode and ffmpeg.returncode != 0:
logger.error(
f"Failed to generate clip, ffmpeg logs: {ffmpeg.stderr.read()}"
)
else:
FilePath(file_path).unlink(missing_ok=True)
break
yield data
recordings = ( recordings = (
Recordings.select( Recordings.select(
Recordings.path, Recordings.path,
@ -467,18 +487,18 @@ def recording_clip(
.order_by(Recordings.start_time.asc()) .order_by(Recordings.start_time.asc())
) )
playlist_lines = [] file_name = sanitize_filename(f"playlist_{camera_name}_{start_ts}-{end_ts}.txt")
clip: Recordings file_path = f"/tmp/cache/{file_name}"
for clip in recordings: with open(file_path, "w") as file:
playlist_lines.append(f"file '{clip.path}'") clip: Recordings
# if this is the starting clip, add an inpoint for clip in recordings:
if clip.start_time < start_ts: file.write(f"file '{clip.path}'\n")
playlist_lines.append(f"inpoint {int(start_ts - clip.start_time)}") # if this is the starting clip, add an inpoint
# if this is the ending clip, add an outpoint if clip.start_time < start_ts:
if clip.end_time > end_ts: file.write(f"inpoint {int(start_ts - clip.start_time)}\n")
playlist_lines.append(f"outpoint {int(end_ts - clip.start_time)}") # if this is the ending clip, add an outpoint
if clip.end_time > end_ts:
file_name = sanitize_filename(f"clip_{camera_name}_{start_ts}-{end_ts}.mp4") file.write(f"outpoint {int(end_ts - clip.start_time)}\n")
if len(file_name) > 1000: if len(file_name) > 1000:
return JSONResponse( return JSONResponse(
@ -489,67 +509,32 @@ def recording_clip(
status_code=403, status_code=403,
) )
path = os.path.join(CLIPS_DIR, f"cache/{file_name}")
config: FrigateConfig = request.app.frigate_config config: FrigateConfig = request.app.frigate_config
if not os.path.exists(path): ffmpeg_cmd = [
ffmpeg_cmd = [ config.ffmpeg.ffmpeg_path,
config.ffmpeg.ffmpeg_path, "-hide_banner",
"-hide_banner", "-y",
"-y", "-protocol_whitelist",
"-protocol_whitelist", "pipe,file",
"pipe,file", "-f",
"-f", "concat",
"concat", "-safe",
"-safe", "0",
"0", "-i",
"-i", file_path,
"/dev/stdin", "-c",
"-c", "copy",
"copy", "-movflags",
"-movflags", "frag_keyframe+empty_moov",
"+faststart", "-f",
path, "mp4",
] "pipe:",
p = sp.run( ]
ffmpeg_cmd,
input="\n".join(playlist_lines),
encoding="ascii",
capture_output=True,
)
if p.returncode != 0: return StreamingResponse(
logger.error(p.stderr) run_download(ffmpeg_cmd, file_path),
return JSONResponse(
content={
"success": False,
"message": "Could not create clip from recordings",
},
status_code=500,
)
else:
logger.debug(
f"Ignoring subsequent request for {path} as it already exists in the cache."
)
headers = {
"Content-Description": "File Transfer",
"Cache-Control": "no-cache",
"Content-Type": "video/mp4",
"Content-Length": str(os.path.getsize(path)),
# nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
"X-Accel-Redirect": f"/clips/cache/{file_name}",
}
if download:
headers["Content-Disposition"] = "attachment; filename=%s" % file_name
return FileResponse(
path,
media_type="video/mp4", media_type="video/mp4",
filename=file_name,
headers=headers,
) )
@ -1028,7 +1013,7 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False
@router.get("/events/{event_id}/clip.mp4") @router.get("/events/{event_id}/clip.mp4")
def event_clip(request: Request, event_id: str, download: bool = False): def event_clip(request: Request, event_id: str):
try: try:
event: Event = Event.get(Event.id == event_id) event: Event = Event.get(Event.id == event_id)
except DoesNotExist: except DoesNotExist:
@ -1048,7 +1033,7 @@ def event_clip(request: Request, event_id: str, download: bool = False):
end_ts = ( end_ts = (
datetime.now().timestamp() if event.end_time is None else event.end_time datetime.now().timestamp() if event.end_time is None else event.end_time
) )
return recording_clip(request, event.camera, event.start_time, end_ts, download) return recording_clip(request, event.camera, event.start_time, end_ts)
headers = { headers = {
"Content-Description": "File Transfer", "Content-Description": "File Transfer",
@ -1059,9 +1044,6 @@ def event_clip(request: Request, event_id: str, download: bool = False):
"X-Accel-Redirect": f"/clips/{file_name}", "X-Accel-Redirect": f"/clips/{file_name}",
} }
if download:
headers["Content-Disposition"] = "attachment; filename=%s" % file_name
return FileResponse( return FileResponse(
clip_path, clip_path,
media_type="video/mp4", media_type="video/mp4",