mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-08 17:13:45 +08:00
Stream Mp4 downloads
This commit is contained in:
@@ -487,6 +487,46 @@ def test_video_cache_never_evicts_the_entry_being_written(monkeypatch, tmp_path)
|
||||
assert keep.exists()
|
||||
|
||||
|
||||
def test_combined_video_streams_fragmented_mp4_without_a_full_cache_file(monkeypatch, tmp_path):
|
||||
cache = tmp_path / "video_cache"
|
||||
first = tmp_path / "first.hevc"
|
||||
second = tmp_path / "second.hevc"
|
||||
first.write_bytes(b"first")
|
||||
second.write_bytes(b"second")
|
||||
monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
|
||||
captured = {}
|
||||
|
||||
class FakeProcess:
|
||||
def __init__(self):
|
||||
self.stdout = io.BytesIO(b"streamed-video")
|
||||
self.returncode = None
|
||||
|
||||
def wait(self, timeout=None):
|
||||
self.returncode = 0
|
||||
return 0
|
||||
|
||||
def poll(self):
|
||||
return self.returncode
|
||||
|
||||
def popen(command, **kwargs):
|
||||
captured["command"] = command
|
||||
list_path = Path(command[command.index("-i") + 1])
|
||||
captured["list_path"] = list_path
|
||||
captured["list_contents"] = list_path.read_text()
|
||||
return FakeProcess()
|
||||
|
||||
monkeypatch.setattr(utilities.subprocess, "Popen", popen)
|
||||
|
||||
payload = b"".join(utilities.ffmpeg_stream_concatenated_mp4([first, second], chunk_size=4))
|
||||
|
||||
assert payload == b"streamed-video"
|
||||
assert "frag_keyframe+empty_moov+default_base_moof" in captured["command"]
|
||||
assert captured["command"][-1] == "pipe:1"
|
||||
assert captured["list_contents"] == f"file '{first}'\nfile '{second}'\n"
|
||||
assert not captured["list_path"].exists()
|
||||
assert not list(cache.glob("*.mp4"))
|
||||
|
||||
|
||||
def _stub_remux(monkeypatch, tmp_path, payload=b"wrapped-video"):
|
||||
"""Stand in for the ffmpeg remux, returning a real file so send_file can stream it."""
|
||||
wrapped = tmp_path / "wrapped.mp4"
|
||||
@@ -501,7 +541,7 @@ def test_sparse_route_metadata_and_video_downloads(monkeypatch, tmp_path):
|
||||
(segment / "fcamera.hevc").write_bytes(b"hevc")
|
||||
monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
|
||||
_stub_remux(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(utilities, "ffmpeg_concat_segments_to_mp4", lambda paths, cache_key=None: io.BytesIO(b"combined-video"))
|
||||
monkeypatch.setattr(utilities, "ffmpeg_stream_concatenated_mp4", lambda paths: iter((b"combined-", b"video")))
|
||||
client = _make_client(monkeypatch, tmp_path)
|
||||
|
||||
metadata = client.get(f"/api/routes/{ROUTE_NAME}")
|
||||
@@ -519,6 +559,7 @@ def test_sparse_route_metadata_and_video_downloads(monkeypatch, tmp_path):
|
||||
assert combined_video.status_code == 200
|
||||
assert combined_video.mimetype == "video/mp4"
|
||||
assert combined_video.data == b"combined-video"
|
||||
assert combined_video.headers["X-Accel-Buffering"] == "no"
|
||||
|
||||
|
||||
def test_route_metadata_never_probes_segments_with_ffprobe(monkeypatch, tmp_path):
|
||||
|
||||
@@ -6482,8 +6482,10 @@ def setup(app):
|
||||
if not input_files:
|
||||
return {"error": "No video files found"}, 404
|
||||
|
||||
mp4_file = utilities.ffmpeg_concat_segments_to_mp4(input_files, cache_key=f"{name}-{camera}")
|
||||
return send_file(mp4_file, mimetype="video/mp4")
|
||||
response = Response(utilities.ffmpeg_stream_concatenated_mp4(input_files), mimetype="video/mp4")
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
response.headers["X-Accel-Buffering"] = "no"
|
||||
return response
|
||||
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
@@ -749,6 +750,50 @@ def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
|
||||
def ffmpeg_stream_concatenated_mp4(input_files, chunk_size=256 * 1024):
|
||||
"""Stream-copy camera segments as fragmented MP4 without building a full cache file."""
|
||||
if not input_files:
|
||||
raise ValueError("No input files provided for concatenation")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".txt", prefix="route-download-", dir=VIDEO_CACHE_PATH, delete=False) as list_file:
|
||||
list_path = Path(list_file.name)
|
||||
for segment in input_files:
|
||||
list_file.write(f"file '{Path(segment)}'\n")
|
||||
|
||||
process = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
[FFMPEG_BIN, "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_path), "-c", "copy", "-movflags", "frag_keyframe+empty_moov+default_base_moof",
|
||||
"-f", "mp4", "pipe:1"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
while True:
|
||||
chunk = process.stdout.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
if process.wait() != 0:
|
||||
raise ValueError("Could not stream the combined route video")
|
||||
finally:
|
||||
if process is not None:
|
||||
if process.stdout is not None:
|
||||
process.stdout.close()
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=2)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
try:
|
||||
list_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def ffmpeg_mp4_wrap_to_path(filename):
|
||||
"""Remux one raw .hevc segment to mp4 and return the cache path.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user