Fix MP4 mux for web playback: add faststart and genpts
Some checks are pending
GPU Worker CI/CD / test (push) Waiting to run
GPU Worker CI/CD / deploy (push) Blocked by required conditions

- Add -movflags +faststart to move moov atom to start
- Add -fflags +genpts for proper timestamp generation
- Fixes jerky playback and video/audio desync

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-04 00:32:45 +00:00
parent 9151d2c2a8
commit 0bd8ee71c7
3 changed files with 39 additions and 26 deletions

View File

@@ -402,25 +402,31 @@ async def get_run(
# Detect media type using magic bytes, fall back to database item_type # Detect media type using magic bytes, fall back to database item_type
output_cid = run["output_cid"] output_cid = run["output_cid"]
media_type = None media_type = None
try:
from ..services.run_service import detect_media_type # Streaming runs (with ipfs_cid) are always video/mp4
cache_path = get_cache_manager().get_by_cid(output_cid) if run.get("ipfs_cid"):
if cache_path and cache_path.exists(): media_type = "video/mp4"
simple_type = detect_media_type(cache_path) output_media_type = media_type
media_type = type_to_mime(simple_type) else:
output_media_type = media_type
except Exception:
pass
# Fall back to database item_type if local detection failed
if not media_type:
try: try:
import database from ..services.run_service import detect_media_type
item_types = await database.get_item_types(output_cid, run.get("actor_id")) cache_path = get_cache_manager().get_by_cid(output_cid)
if item_types: if cache_path and cache_path.exists():
media_type = type_to_mime(item_types[0].get("type")) simple_type = detect_media_type(cache_path)
media_type = type_to_mime(simple_type)
output_media_type = media_type output_media_type = media_type
except Exception: except Exception:
pass pass
# Fall back to database item_type if local detection failed
if not media_type:
try:
import database
item_types = await database.get_item_types(output_cid, run.get("actor_id"))
if item_types:
media_type = type_to_mime(item_types[0].get("type"))
output_media_type = media_type
except Exception:
pass
artifacts.append({ artifacts.append({
"cid": output_cid, "cid": output_cid,
"step_name": "Output", "step_name": "Output",
@@ -568,13 +574,17 @@ async def list_runs(
for run in runs: for run in runs:
# Add output media info # Add output media info
if run.get("output_cid"): if run.get("output_cid"):
try: # Streaming runs (with ipfs_cid) are always video/mp4
cache_path = cache_manager.get_by_cid(run["output_cid"]) if run.get("ipfs_cid"):
if cache_path and cache_path.exists(): run["output_media_type"] = "video/mp4"
simple_type = detect_media_type(cache_path) else:
run["output_media_type"] = type_to_mime(simple_type) try:
except Exception: cache_path = cache_manager.get_by_cid(run["output_cid"])
pass if cache_path and cache_path.exists():
simple_type = detect_media_type(cache_path)
run["output_media_type"] = type_to_mime(simple_type)
except Exception:
pass
# Add input media info (first 3 inputs) # Add input media info (first 3 inputs)
input_previews = [] input_previews = []

View File

@@ -575,7 +575,7 @@
{# Inline media preview - prefer IPFS URLs when available #} {# Inline media preview - prefer IPFS URLs when available #}
<div class="mb-4"> <div class="mb-4">
{% if output_media_type and output_media_type.startswith('image/') %} {% if output_media_type and output_media_type.startswith('image/') %}
<a href="/cache/{{ run.output_cid }}" class="block"> <a href="{% if run.ipfs_cid %}/ipfs/{{ run.ipfs_cid }}{% else %}/cache/{{ run.output_cid }}{% endif %}" class="block">
<img src="{% if run.ipfs_cid %}/ipfs/{{ run.ipfs_cid }}{% else %}/cache/{{ run.output_cid }}/raw{% endif %}" alt="Output" <img src="{% if run.ipfs_cid %}/ipfs/{{ run.ipfs_cid }}{% else %}/cache/{{ run.output_cid }}/raw{% endif %}" alt="Output"
class="max-w-full max-h-96 rounded-lg mx-auto"> class="max-w-full max-h-96 rounded-lg mx-auto">
</a> </a>
@@ -593,14 +593,15 @@
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<a href="/cache/{{ run.output_cid }}" class="font-mono text-sm text-blue-400 hover:text-blue-300"> <a href="{% if run.ipfs_cid %}/ipfs/{{ run.ipfs_cid }}{% else %}/cache/{{ run.output_cid }}{% endif %}"
{{ run.output_cid }} class="font-mono text-sm text-blue-400 hover:text-blue-300">
{% if run.ipfs_cid %}{{ run.ipfs_cid }}{% else %}{{ run.output_cid }}{% endif %}
</a> </a>
{% if run.ipfs_cid %} {% if run.ipfs_cid %}
<a href="https://ipfs.io/ipfs/{{ run.ipfs_cid }}" <a href="https://ipfs.io/ipfs/{{ run.ipfs_cid }}"
target="_blank" target="_blank"
class="text-gray-400 hover:text-white text-sm"> class="text-gray-400 hover:text-white text-sm">
IPFS: {{ run.ipfs_cid[:16] }}... View on IPFS Gateway
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@@ -411,6 +411,8 @@ def run_stream(
"ffmpeg", "-y", "ffmpeg", "-y",
"-i", str(hls_playlist), "-i", str(hls_playlist),
"-c", "copy", # Just copy streams, no re-encoding "-c", "copy", # Just copy streams, no re-encoding
"-movflags", "+faststart", # Move moov atom to start for web playback
"-fflags", "+genpts", # Generate proper timestamps
str(final_mp4) str(final_mp4)
] ]
logger.info(f"Muxing HLS to MP4: {' '.join(mux_cmd)}") logger.info(f"Muxing HLS to MP4: {' '.join(mux_cmd)}")