diff --git a/media/no_duration.webm b/media/no_duration.webm new file mode 100644 index 000000000..f5f1e415d Binary files /dev/null and b/media/no_duration.webm differ diff --git a/moviepy/video/io/errors.py b/moviepy/video/io/errors.py new file mode 100644 index 000000000..b3c44cea8 --- /dev/null +++ b/moviepy/video/io/errors.py @@ -0,0 +1,3 @@ +class VideoCorruptedError(RuntimeError): + """Error raised when a video file is corrupted.""" + pass \ No newline at end of file diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index c9d073b6c..8ea12db05 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -13,6 +13,7 @@ cross_platform_popen_params, ffmpeg_escape_filename, ) +from moviepy.video.io.errors import VideoCorruptedError class FFMPEG_VideoReader: @@ -760,7 +761,11 @@ def parse_duration(self, line): r"([0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9])", time_raw_string, ) + if match_duration is None: + raise VideoCorruptedError("Video is corrupted and missing duration") return convert_to_seconds(match_duration.group(1)) + except VideoCorruptedError: + raise except Exception: raise IOError( ( @@ -883,6 +888,8 @@ def ffmpeg_parse_infos( check_duration=check_duration, decode_file=decode_file, ).parse() + except VideoCorruptedError as exc: + raise exc except Exception as exc: if os.path.isdir(filename): raise IsADirectoryError(f"'{filename}' is a directory") diff --git a/moviepy/video/re_encode.py b/moviepy/video/re_encode.py new file mode 100644 index 000000000..cff9241ed --- /dev/null +++ b/moviepy/video/re_encode.py @@ -0,0 +1,40 @@ +import subprocess +from pathlib import Path + +def reencode_video(input_file:str|Path, output_file:str|Path): + """ + Re-encode a video file using ffmpeg. (this may fix some issues with corrupted video file) + Parameters + ---------- + input_file : str or Path file that will be re-encoded + output_file: str or Path file where the re-encoded video will be saved + + Returns + ------- + + """ + # Convert input and output files to Path objects + input_path = Path(input_file).resolve() + output_path = Path(output_file).resolve() + + # Check if the input file exists + if not input_path.exists(): + raise FileNotFoundError(f"Input file '{input_file}' not found.") + + if output_path.exists(): + raise FileExistsError(f"Output file '{output_file}' already exists.") + + try: + # Construct the ffmpeg command + command = [ + "ffmpeg", + "-i", str(input_path), + "-c", "copy", # Copy audio without re-encoding + str(output_path) + ] + + # Run the command + proc=subprocess.run(command, check=True) + proc.check_returncode() + except subprocess.CalledProcessError as e: + print(f"Error: ffmpeg command failed with error {e}") diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index 4ee33cffa..e115c378a 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -2,12 +2,16 @@ import copy import os +from pathlib import Path +from tempfile import TemporaryDirectory import pytest from moviepy.video.compositing.CompositeVideoClip import clips_array from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.errors import VideoCorruptedError +from moviepy.video.re_encode import reencode_video def test_setup(util): @@ -98,5 +102,17 @@ def test_ffmpeg_transparency_mask(util): video.close() +def test_no_duration_raise_io_error(): + with pytest.raises(VideoCorruptedError,match="Video is corrupted and missing duration"): + VideoFileClip("media/no_duration.webm") + + +def test_no_duration_re_encode_can_be_opened(): + with TemporaryDirectory() as temp_dir: + target=Path(temp_dir).joinpath("re_encoded.webm") + reencode_video("media/no_duration.webm",target) + VideoFileClip(target) + + if __name__ == "__main__": pytest.main()