Fix MP4 mux for web playback: add faststart and genpts
- 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:
@@ -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 = []
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user