Spaces:
Running
Running
Commit ·
7f39f97
1
Parent(s): a5190e9
Fix: XVID/AVI intermediate fixes zero-duration output, pix_fmt yuv420p for compatibility, FFmpeg error logging
Browse files- processors/video_processor.py +51 -21
processors/video_processor.py
CHANGED
|
@@ -85,10 +85,16 @@ class VideoProcessor:
|
|
| 85 |
if start_frame > 0:
|
| 86 |
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
| 87 |
|
| 88 |
-
#
|
| 89 |
-
|
| 90 |
-
|
|
|
|
| 91 |
writer = cv2.VideoWriter(raw_out_path, fourcc, fps, (width, height))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
frame_idx = start_frame # absolute frame number in the source video
|
| 94 |
processed = 0
|
|
@@ -205,34 +211,58 @@ class VideoProcessor:
|
|
| 205 |
@staticmethod
|
| 206 |
def _ffmpeg_encode(original_video_path: str, processed_raw_path: str, audio_start: float = 0.0) -> str:
|
| 207 |
"""
|
| 208 |
-
Re-encode processed frames as H.264 and
|
| 209 |
-
audio_start: seconds
|
| 210 |
-
|
| 211 |
"""
|
| 212 |
final_path = tempfile.mktemp(suffix="_output.mp4")
|
| 213 |
try:
|
| 214 |
import ffmpeg
|
|
|
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
(
|
| 221 |
-
ffmpeg.output(
|
| 222 |
-
video_stream,
|
| 223 |
-
audio_stream,
|
| 224 |
-
final_path,
|
| 225 |
-
vcodec="libx264",
|
| 226 |
-
crf=23,
|
| 227 |
-
preset="fast",
|
| 228 |
-
acodec="aac",
|
| 229 |
-
audio_bitrate="192k",
|
| 230 |
-
)
|
| 231 |
.overwrite_output()
|
| 232 |
-
.run(quiet=True)
|
| 233 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
return final_path
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
except Exception as e:
|
| 237 |
-
print(f"[VideoProcessor] FFmpeg
|
| 238 |
return processed_raw_path
|
|
|
|
| 85 |
if start_frame > 0:
|
| 86 |
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
| 87 |
|
| 88 |
+
# Use AVI + XVID for the intermediate file — far more reliable than
|
| 89 |
+
# mp4v on Linux (HF Spaces). FFmpeg converts it to H.264/mp4 after.
|
| 90 |
+
raw_out_path = tempfile.mktemp(suffix="_raw.avi")
|
| 91 |
+
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
| 92 |
writer = cv2.VideoWriter(raw_out_path, fourcc, fps, (width, height))
|
| 93 |
+
if not writer.isOpened():
|
| 94 |
+
# XVID not available — fall back to MJPG
|
| 95 |
+
raw_out_path = tempfile.mktemp(suffix="_raw.avi")
|
| 96 |
+
fourcc = cv2.VideoWriter_fourcc(*"MJPG")
|
| 97 |
+
writer = cv2.VideoWriter(raw_out_path, fourcc, fps, (width, height))
|
| 98 |
|
| 99 |
frame_idx = start_frame # absolute frame number in the source video
|
| 100 |
processed = 0
|
|
|
|
| 211 |
@staticmethod
|
| 212 |
def _ffmpeg_encode(original_video_path: str, processed_raw_path: str, audio_start: float = 0.0) -> str:
|
| 213 |
"""
|
| 214 |
+
Re-encode processed frames as H.264 mp4 and merge the original audio.
|
| 215 |
+
audio_start: seconds into the original audio (for resumed segments).
|
| 216 |
+
Returns the output path; raises if encoding fails so caller can report it.
|
| 217 |
"""
|
| 218 |
final_path = tempfile.mktemp(suffix="_output.mp4")
|
| 219 |
try:
|
| 220 |
import ffmpeg
|
| 221 |
+
import subprocess
|
| 222 |
|
| 223 |
+
video_in = ffmpeg.input(processed_raw_path)
|
| 224 |
+
audio_in = ffmpeg.input(original_video_path)
|
| 225 |
+
|
| 226 |
+
# Build output streams
|
| 227 |
+
streams = [video_in.video]
|
| 228 |
+
# Only attach audio if the source has an audio track
|
| 229 |
+
try:
|
| 230 |
+
probe = ffmpeg.probe(original_video_path)
|
| 231 |
+
has_audio = any(s["codec_type"] == "audio" for s in probe["streams"])
|
| 232 |
+
except Exception:
|
| 233 |
+
has_audio = False
|
| 234 |
+
|
| 235 |
+
if has_audio:
|
| 236 |
+
if audio_start > 0:
|
| 237 |
+
audio_in = ffmpeg.input(original_video_path, ss=audio_start)
|
| 238 |
+
streams.append(audio_in.audio)
|
| 239 |
+
|
| 240 |
+
out_kwargs = dict(
|
| 241 |
+
vcodec="libx264",
|
| 242 |
+
crf=23,
|
| 243 |
+
preset="fast",
|
| 244 |
+
pix_fmt="yuv420p", # widest player compatibility
|
| 245 |
+
)
|
| 246 |
+
if has_audio:
|
| 247 |
+
out_kwargs.update(acodec="aac", audio_bitrate="192k")
|
| 248 |
|
| 249 |
(
|
| 250 |
+
ffmpeg.output(*streams, final_path, **out_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
.overwrite_output()
|
| 252 |
+
.run(quiet=False, capture_stdout=True, capture_stderr=True)
|
| 253 |
)
|
| 254 |
+
|
| 255 |
+
# Validate output
|
| 256 |
+
if not os.path.exists(final_path) or os.path.getsize(final_path) < 1024:
|
| 257 |
+
raise RuntimeError("FFmpeg produced an empty output file.")
|
| 258 |
+
|
| 259 |
return final_path
|
| 260 |
|
| 261 |
+
except ffmpeg.Error as e:
|
| 262 |
+
stderr = e.stderr.decode(errors="replace") if e.stderr else ""
|
| 263 |
+
print(f"[VideoProcessor] FFmpeg error:\n{stderr}")
|
| 264 |
+
# Return the raw file as fallback so the user gets something
|
| 265 |
+
return processed_raw_path
|
| 266 |
except Exception as e:
|
| 267 |
+
print(f"[VideoProcessor] FFmpeg encode failed: {e}")
|
| 268 |
return processed_raw_path
|