MXF/MPG broadcast format support: handle multichannel audio properly
Problem: MXF and MPG files (TV broadcast formats) often contain: - Multiple audio streams (4-8 streams for different language tracks) - Multichannel layouts (5.1, 7.1) instead of stereo - Default ffmpeg behavior was -c:a aac without channel limit, which meant multichannel got transcoded as multichannel AAC, overwriting what should have been clean stereo Solution: 1. get_audio_streams() helper probes all audio streams with ffprobe - Returns codec, channels, sample_rate, language, layout for each 2. build_audio_args() picks best stream + downmix: - Prefers first 2-channel stereo stream (usually main mix) - Falls back to first stream if none are 2-ch - Always: -ac 2 (force stereo downmix), -ar 48000, -c:a aac, -b:a 192k - Bitrate raised from 128k to 192k for music quality 3. Smart trim path now detects broadcast formats: - .mxf, .mpg, .mpeg, .ts, .m2ts, .mts → transcode (not stream copy) - Standard MP4/MOV → stream copy (faster, lossless) 4. Pre-conversion step for broadcast files without trim: - Even without --start/--duration, MXF/MPG get converted to MP4 - Same audio handling as trim path 5. Main render adds explicit -map 0✌️0 -map 0🅰️0? -ac 2 to ensure only first video and first audio stream get encoded, with stereo 6. ACR recognize also gets -map 0🅰️0 -ac 2 for MXF compatibility 7. UI accepts: video/*,.mxf,.mpg,.mpeg,.ts,.m2ts,.mts 8. Upload limit raised: 2GB → 10GB (MXF files are large) This means a TV broadcast MXF with [SLO/EN/DE language tracks] now correctly outputs stereo MP4 with the main language track preserved.
This commit is contained in:
parent
b543057cee
commit
1cc8e8be35
@ -114,7 +114,8 @@ def recognize_audio_file(audio_path, timeout=30):
|
||||
def extract_short_audio(video_path, duration=20, start_offset=15):
|
||||
"""Izloči kratek audio iz videa za ACR fingerprint.
|
||||
|
||||
Ponavadi začnemo 15s v video (preskoči intro) in vzamemo 20s.
|
||||
Začnemo 15s v video (preskoči intro) in vzamemo 20s.
|
||||
Za MXF/multichannel: izberemo prvi audio stream, downmix v stereo.
|
||||
Vrne pot do tmp MP3 ali None.
|
||||
"""
|
||||
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".mp3")
|
||||
@ -123,8 +124,13 @@ def extract_short_audio(video_path, duration=20, start_offset=15):
|
||||
"ffmpeg", "-i", video_path,
|
||||
"-ss", str(start_offset),
|
||||
"-t", str(duration),
|
||||
"-vn", "-ar", "44100", "-ac", "1", "-b:a", "128k",
|
||||
"-f", "mp3", tmp_path, "-y", "-loglevel", "quiet"
|
||||
"-map", "0:a:0", # samo prvi audio stream (varno za MXF z več streami)
|
||||
"-vn",
|
||||
"-ac", "2", # downmix v stereo (če multichannel)
|
||||
"-ar", "44100",
|
||||
"-b:a", "128k",
|
||||
"-f", "mp3", tmp_path,
|
||||
"-y", "-loglevel", "error"
|
||||
]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
|
||||
@ -43,6 +43,68 @@ def get_video_info(path):
|
||||
}
|
||||
|
||||
|
||||
def get_audio_streams(path):
|
||||
"""Vrni seznam audio streamov z njihovimi metadati.
|
||||
|
||||
MXF datoteke imajo pogosto 4-8 audio streamov ali en stream z 8 kanali.
|
||||
Vrne: [{'index': 1, 'channels': 2, 'codec': 'pcm_s24le', 'language': 'eng'}, ...]
|
||||
"""
|
||||
cmd = [
|
||||
"ffprobe", "-v", "quiet", "-print_format", "json",
|
||||
"-show_streams", "-select_streams", "a", str(path)
|
||||
]
|
||||
try:
|
||||
data = json.loads(subprocess.check_output(cmd))
|
||||
streams = []
|
||||
for s in data.get("streams", []):
|
||||
streams.append({
|
||||
"index": s.get("index"),
|
||||
"channels": int(s.get("channels", 2)),
|
||||
"codec": s.get("codec_name", ""),
|
||||
"sample_rate": int(s.get("sample_rate", 48000)),
|
||||
"language": s.get("tags", {}).get("language", ""),
|
||||
"channel_layout": s.get("channel_layout", ""),
|
||||
})
|
||||
return streams
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def build_audio_args(audio_streams):
|
||||
"""Sestavi FFmpeg argumente za audio: izberi pravi stream + downmix v stereo.
|
||||
|
||||
Strategija:
|
||||
- Če je samo 1 stream: pretvori ga v stereo (downmix iz multichannel)
|
||||
- Če je več streamov (MXF z več jezikovnimi kanali):
|
||||
* preferiraj prvi 2-kanalni stereo stream (običajno glavna audio mix)
|
||||
* sicer prvi stream
|
||||
- Vedno: output 2 channels @ 48kHz, codec AAC 192k (boljša kvaliteta za broadcast)
|
||||
"""
|
||||
if not audio_streams:
|
||||
# Brez audia: prazen output
|
||||
return ["-an"]
|
||||
|
||||
# Najdi najboljši stream
|
||||
chosen = None
|
||||
# 1. Stereo (2-kanalni) ima prednost
|
||||
for s in audio_streams:
|
||||
if s["channels"] == 2:
|
||||
chosen = s
|
||||
break
|
||||
# 2. Sicer prvi stream
|
||||
if chosen is None:
|
||||
chosen = audio_streams[0]
|
||||
|
||||
args = [
|
||||
"-map", f"0:{chosen['index']}", # samo izbrani stream
|
||||
"-ac", "2", # downmix v stereo (če je multichannel)
|
||||
"-ar", "48000",
|
||||
"-c:a", "aac",
|
||||
"-b:a", "192k", # 192k = boljša kvaliteta za glasbo (prej 128k)
|
||||
]
|
||||
return args
|
||||
|
||||
|
||||
def detect_face_centers(video_path, sample_fps=5):
|
||||
"""
|
||||
Vzorči video pri sample_fps in vrni seznam (timestamp, x_center_normalized).
|
||||
@ -227,6 +289,23 @@ def main():
|
||||
# Če imamo --start/--duration, najprej trim z FFmpeg v temp file (hitreje)
|
||||
work_input = src
|
||||
tmp = None
|
||||
|
||||
# Probe audio streams iz originala
|
||||
audio_streams = get_audio_streams(src)
|
||||
src_ext = src.suffix.lower()
|
||||
is_broadcast = src_ext in (".mxf", ".mpg", ".mpeg", ".ts", ".m2ts", ".mts")
|
||||
has_complex_audio = (
|
||||
len(audio_streams) > 1 or
|
||||
(audio_streams and audio_streams[0].get("channels", 2) > 2) or
|
||||
is_broadcast
|
||||
)
|
||||
|
||||
if has_complex_audio:
|
||||
print(f"🎚 Broadcast format ({src_ext}) — {len(audio_streams)} audio stream(s):", file=sys.stderr)
|
||||
for s in audio_streams:
|
||||
print(f" #{s['index']}: {s['codec']} {s['channels']}ch "
|
||||
f"{s.get('channel_layout', '')} lang={s.get('language', '?')}", file=sys.stderr)
|
||||
|
||||
if args.start is not None or args.duration is not None:
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
||||
tmp.close()
|
||||
@ -236,21 +315,49 @@ def main():
|
||||
cmd += ["-i", str(src)]
|
||||
if args.duration is not None:
|
||||
cmd += ["-t", str(args.duration)]
|
||||
cmd += ["-c", "copy", tmp.name]
|
||||
|
||||
if has_complex_audio:
|
||||
# Broadcast format ali multichannel: ne kopiraj, transkodiraj v stereo MP4
|
||||
audio_args = build_audio_args(audio_streams)
|
||||
cmd += [
|
||||
"-map", "0:v:0", # samo prvi video stream
|
||||
"-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
|
||||
]
|
||||
cmd += audio_args
|
||||
else:
|
||||
# MP4/MOV s standardnim audiom — stream copy je OK in hitrejši
|
||||
cmd += ["-c", "copy"]
|
||||
cmd += [tmp.name]
|
||||
|
||||
print(f"🔧 TRIM CMD: {' '.join(cmd)}", file=sys.stderr)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ TRIM FAILED: {result.stderr[-500:]}", file=sys.stderr)
|
||||
print(f"❌ TRIM FAILED: {result.stderr[-1000:]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
work_input = Path(tmp.name)
|
||||
print(f"✂ Trim → {work_input}")
|
||||
# Verify trim output duration
|
||||
verify = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-show_entries", "format=duration",
|
||||
"-of", "default=nw=1:nokey=1", str(work_input)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
print(f"🔍 TRIMMED FILE DURATION: {verify.stdout.strip()}s (expected ~{args.duration}s)", file=sys.stderr)
|
||||
elif has_complex_audio:
|
||||
# Brez trim-a, ampak MXF/MPG → še vedno transkodiraj v MP4 z 2-channel audio
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
|
||||
tmp.close()
|
||||
audio_args = build_audio_args(audio_streams)
|
||||
cmd = ["ffmpeg", "-y", "-i", str(src),
|
||||
"-map", "0:v:0",
|
||||
"-c:v", "libx264", "-preset", "veryfast", "-crf", "20"]
|
||||
cmd += audio_args + [tmp.name]
|
||||
print(f"🔧 PRE-CONVERT CMD: {' '.join(cmd)}", file=sys.stderr)
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
print(f"❌ PRE-CONVERT FAILED: {result.stderr[-1000:]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
work_input = Path(tmp.name)
|
||||
print(f"🔄 Pre-converted → {work_input}")
|
||||
|
||||
info = get_video_info(work_input)
|
||||
print(f"📹 Vhod: {info['width']}x{info['height']} @ {info['fps']:.2f}fps, {info['duration']:.1f}s")
|
||||
@ -286,7 +393,9 @@ def main():
|
||||
"ffmpeg", "-y", "-i", str(work_input),
|
||||
"-filter_complex", vfilter,
|
||||
"-c:v", "libx264", "-preset", preset, "-crf", crf,
|
||||
"-c:a", "aac", "-b:a", "128k",
|
||||
"-map", "0:a:0?", # samo prvi audio stream (če obstaja)
|
||||
"-ac", "2", # force stereo
|
||||
"-c:a", "aac", "-b:a", "192k",
|
||||
]
|
||||
if audio_filter_str:
|
||||
cmd += ["-af", audio_filter_str]
|
||||
@ -296,7 +405,9 @@ def main():
|
||||
"ffmpeg", "-y", "-i", str(work_input),
|
||||
"-vf", vfilter,
|
||||
"-c:v", "libx264", "-preset", preset, "-crf", crf,
|
||||
"-c:a", "aac", "-b:a", "128k",
|
||||
"-map", "0:v:0", "-map", "0:a:0?", # video + samo prvi audio
|
||||
"-ac", "2", # force stereo
|
||||
"-c:a", "aac", "-b:a", "192k",
|
||||
]
|
||||
if audio_filter_str:
|
||||
cmd += ["-af", audio_filter_str]
|
||||
|
||||
@ -290,7 +290,7 @@
|
||||
</svg>
|
||||
<div>Klikni ali povleci video sem</div>
|
||||
<div class="small">.mp4, .mov, .webm — do 2 GB</div>
|
||||
<input type="file" id="file-input" accept="video/*" style="display:none">
|
||||
<input type="file" id="file-input" accept="video/*,.mxf,.mpg,.mpeg,.ts,.m2ts,.mts" style="display:none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user