From 8fc03f6c49853662fc73c37c2995643ff9cbde41 Mon Sep 17 00:00:00 2001 From: zalgo Date: Mon, 30 Jan 2023 11:51:34 +0900 Subject: [PATCH 01/19] Use audio_codec option even if audio is given as filename --- moviepy/video/VideoClip.py | 2 ++ moviepy/video/io/ffmpeg_writer.py | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 76e19e47a..6584f4274 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -376,6 +376,7 @@ def write_videofile( write_logfile=write_logfile, logger=logger, ) + audio_codec = "copy" ffmpeg_write_video( self, @@ -386,6 +387,7 @@ def write_videofile( preset=preset, write_logfile=write_logfile, audiofile=audiofile, + audio_codec=audio_codec, threads=threads, ffmpeg_params=ffmpeg_params, logger=logger, diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 7032be0af..6544f61b9 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -44,6 +44,9 @@ class FFMPEG_VideoWriter: audiofile : str, optional The name of an audio file that will be incorporated to the video. + audio_codec : str, optional + FFMPEG audio codec. + preset : str, optional Sets the time that FFMPEG will take to compress the video. The slower, the better the compression rate. Possibilities are: ``"ultrafast"``, @@ -119,7 +122,9 @@ def __init__( "-", ] if audiofile is not None: - cmd.extend(["-i", audiofile, "-acodec", "copy"]) + if audio_codec is None: + audio_codec = "copy" + cmd.extend(["-i", audiofile, "-acodec", audio_codec]) cmd.extend(["-vcodec", codec, "-preset", preset]) if ffmpeg_params is not None: cmd.extend(ffmpeg_params) @@ -226,6 +231,7 @@ def ffmpeg_write_video( with_mask=False, write_logfile=False, audiofile=None, + audio_codec=None, threads=None, ffmpeg_params=None, logger="bar", @@ -252,6 +258,7 @@ def ffmpeg_write_video( bitrate=bitrate, logfile=logfile, audiofile=audiofile, + audio_codec=audio_codec, threads=threads, ffmpeg_params=ffmpeg_params, pixel_format=pixel_format, From 83b019f10b3b02d697ec8df488cb601846a92707 Mon Sep 17 00:00:00 2001 From: zalgo Date: Mon, 30 Jan 2023 12:04:18 +0900 Subject: [PATCH 02/19] Add an extra explanation to the docstring --- moviepy/video/io/ffmpeg_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 6544f61b9..7568d1d92 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -45,7 +45,7 @@ class FFMPEG_VideoWriter: The name of an audio file that will be incorporated to the video. audio_codec : str, optional - FFMPEG audio codec. + FFMPEG audio codec. If None, ``"copy"`` codec is used. preset : str, optional Sets the time that FFMPEG will take to compress the video. The slower, From c4f8fefefcb3442a95807606777e157d37eada34 Mon Sep 17 00:00:00 2001 From: zalgo Date: Mon, 30 Jan 2023 12:07:08 +0900 Subject: [PATCH 03/19] Add a comment --- moviepy/video/VideoClip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 6584f4274..d6506ce6a 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -376,6 +376,7 @@ def write_videofile( write_logfile=write_logfile, logger=logger, ) + # The audio is already encoded, so there is no need to encode it during video export audio_codec = "copy" ffmpeg_write_video( From 89c5fce5597195d7bb9a5b5bed5eb5c95567956b Mon Sep 17 00:00:00 2001 From: zalgo Date: Mon, 30 Jan 2023 13:54:22 +0900 Subject: [PATCH 04/19] Add an argument audio_codec to FFMPEG_VideoWriter.__init__ --- moviepy/video/io/ffmpeg_writer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 7568d1d92..b602364c5 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -84,6 +84,7 @@ def __init__( fps, codec="libx264", audiofile=None, + audio_codec=None, preset="medium", bitrate=None, with_mask=False, From 8fd8d87fd3af3817fc78cc2600ff4810d7dda02c Mon Sep 17 00:00:00 2001 From: zalgo Date: Mon, 30 Jan 2023 22:18:47 +0900 Subject: [PATCH 05/19] Fix the error message and modify/add tests --- moviepy/video/io/ffmpeg_writer.py | 7 ++++--- tests/test_VideoClip.py | 20 ++++++++++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index b602364c5..694d580cf 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -98,6 +98,7 @@ def __init__( self.logfile = logfile self.filename = filename self.codec = codec + self.audio_codec = audio_codec self.ext = self.filename.split(".")[-1] if not pixel_format: # pragma: no cover pixel_format = "rgba" if with_mask else "rgb24" @@ -164,13 +165,13 @@ def write_frame(self, img_array): f"writing file {self.filename}:\n\n {ffmpeg_error}" ) - if "Unknown encoder" in ffmpeg_error: + if "Unknown encoder" in ffmpeg_error or "Unknown decoder" in ffmpeg_error: error += ( "\n\nThe video export failed because FFMPEG didn't find the " - f"specified codec for video encoding {self.codec}. " + f"specified codec for video or audio. " "Please install this codec or change the codec when calling " "write_videofile.\nFor instance:\n" - " >>> clip.write_videofile('myvid.webm', codec='libvpx')" + " >>> clip.write_videofile('myvid.webm', audio='myaudio.mp3', codec='libvpx', audio_codec='aac')" ) elif "incorrect codec parameters ?" in ffmpeg_error: diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index 4e2d1abbc..ddc2f8380 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -72,7 +72,7 @@ def test_write_frame_errors(util, video): clip.write_videofile(location, codec="nonexistent-codec") assert ( "The video export failed because FFMPEG didn't find the specified" - " codec for video encoding nonexistent-codec" in str(e.value) + " codec for video or audio" in str(e.value) ), e.value autogenerated_location = "unlogged-writeTEMP_MPY_wvf_snd.mp3" @@ -90,7 +90,7 @@ def test_write_frame_errors_with_redirected_logs(util, video): clip.write_videofile(location, codec="nonexistent-codec", write_logfile=True) assert ( "The video export failed because FFMPEG didn't find the specified" - " codec for video encoding nonexistent-codec" in str(e.value) + " codec for video or audio" in str(e.value) ) autogenerated_location_mp3 = "logged-writeTEMP_MPY_wvf_snd.mp3" @@ -112,6 +112,22 @@ def test_write_videofiles_with_temp_audiofile_path(util): assert any(file.startswith("temp_audiofile_path") for file in contents_of_temp_dir) +def test_write_videofiles_audio_codec_error(util, video): + """Checks error cases return helpful messages.""" + clip = video() + location = os.path.join(util.TMP_DIR, "unlogged-write.mp4") + with pytest.raises(IOError) as e: + clip.write_videofile(location, audio="media/crunching.mp3", audio_codec="nonexistent-codec") + assert ( + "The video export failed because FFMPEG didn't find the specified" + " codec for video or audio" in str(e.value) + ), e.value + + autogenerated_location = "unlogged-writeTEMP_MPY_wvf_snd.mp3" + if os.path.exists(autogenerated_location): + os.remove(autogenerated_location) + + @pytest.mark.parametrize("mask_color", (0, 0.5, 0.8, 1)) @pytest.mark.parametrize( "with_mask", From c629ebe2fd6288c6d7f68e21f5fd4642d8c5d4cc Mon Sep 17 00:00:00 2001 From: zalgo Date: Tue, 7 Feb 2023 09:13:34 +0900 Subject: [PATCH 06/19] Apply black formatter --- tests/test_VideoClip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py index ddc2f8380..1807279a6 100644 --- a/tests/test_VideoClip.py +++ b/tests/test_VideoClip.py @@ -117,7 +117,9 @@ def test_write_videofiles_audio_codec_error(util, video): clip = video() location = os.path.join(util.TMP_DIR, "unlogged-write.mp4") with pytest.raises(IOError) as e: - clip.write_videofile(location, audio="media/crunching.mp3", audio_codec="nonexistent-codec") + clip.write_videofile( + location, audio="media/crunching.mp3", audio_codec="nonexistent-codec" + ) assert ( "The video export failed because FFMPEG didn't find the specified" " codec for video or audio" in str(e.value) From 419548a9a6f5833b1d94869b18d6bb5e6a47b0e9 Mon Sep 17 00:00:00 2001 From: zalgo Date: Tue, 7 Feb 2023 09:37:48 +0900 Subject: [PATCH 07/19] fix comment break lines --- moviepy/video/VideoClip.py | 3 ++- moviepy/video/io/ffmpeg_writer.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index d6506ce6a..8b3a68e29 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -376,7 +376,8 @@ def write_videofile( write_logfile=write_logfile, logger=logger, ) - # The audio is already encoded, so there is no need to encode it during video export + # The audio is already encoded, + # so there is no need to encode it during video export audio_codec = "copy" ffmpeg_write_video( diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 694d580cf..05fbcf153 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -171,7 +171,8 @@ def write_frame(self, img_array): f"specified codec for video or audio. " "Please install this codec or change the codec when calling " "write_videofile.\nFor instance:\n" - " >>> clip.write_videofile('myvid.webm', audio='myaudio.mp3', codec='libvpx', audio_codec='aac')" + " >>> clip.write_videofile('myvid.webm', audio='myaudio.mp3', " + "codec='libvpx', audio_codec='aac')" ) elif "incorrect codec parameters ?" in ffmpeg_error: From 5635b97f3e57e9b4c0403924a500a822637eefd5 Mon Sep 17 00:00:00 2001 From: zalgo Date: Fri, 10 Feb 2023 14:08:15 +0900 Subject: [PATCH 08/19] fix flake8 error --- moviepy/video/io/ffmpeg_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 05fbcf153..cff303ad7 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -168,7 +168,7 @@ def write_frame(self, img_array): if "Unknown encoder" in ffmpeg_error or "Unknown decoder" in ffmpeg_error: error += ( "\n\nThe video export failed because FFMPEG didn't find the " - f"specified codec for video or audio. " + "specified codec for video or audio. " "Please install this codec or change the codec when calling " "write_videofile.\nFor instance:\n" " >>> clip.write_videofile('myvid.webm', audio='myaudio.mp3', " From 34516a6dccf8becf0e0d2b4e934ea0a77a4c2990 Mon Sep 17 00:00:00 2001 From: zalgo3 Date: Wed, 22 Feb 2023 10:36:16 +0900 Subject: [PATCH 09/19] Apply black --- moviepy/Clip.py | 6 ------ moviepy/audio/AudioClip.py | 1 - moviepy/audio/io/AudioFileClip.py | 1 - moviepy/audio/io/readers.py | 1 - moviepy/config.py | 1 - moviepy/video/VideoClip.py | 3 --- moviepy/video/compositing/CompositeVideoClip.py | 1 - moviepy/video/fx/freeze_region.py | 2 -- moviepy/video/fx/resize.py | 2 -- moviepy/video/io/ImageSequenceClip.py | 7 ------- moviepy/video/io/VideoFileClip.py | 4 ---- moviepy/video/io/ffmpeg_reader.py | 2 -- moviepy/video/io/ffmpeg_writer.py | 2 -- moviepy/video/io/gif_writers.py | 7 ------- moviepy/video/io/preview.py | 1 - moviepy/video/tools/cuts.py | 8 +++----- moviepy/video/tools/interpolators.py | 2 -- moviepy/video/tools/subtitles.py | 1 - moviepy/video/tools/tracking.py | 4 ---- tests/test_VideoFileClip.py | 2 +- tests/test_ffmpeg_reader.py | 1 - tests/test_ffmpeg_writer.py | 1 - tests/test_fx.py | 1 - tests/test_issues.py | 1 - tests/test_videotools.py | 1 - 25 files changed, 4 insertions(+), 59 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index 69eb0d817..73ebdfadc 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -43,7 +43,6 @@ class Clip: _TEMP_FILES_PREFIX = "TEMP_MPY_" def __init__(self): - self.start = 0 self.end = None self.duration = None @@ -375,7 +374,6 @@ def is_playing(self, t): return result else: - return (t >= self.start) and ((self.end is None) or (t < self.end)) @convert_parameter_to_seconds(["start_time", "end_time"]) @@ -424,11 +422,9 @@ def subclip(self, start_time=0, end_time=None): new_clip = self.time_transform(lambda t: t + start_time, apply_to=[]) if (end_time is None) and (self.duration is not None): - end_time = self.duration elif (end_time is not None) and (end_time < 0): - if self.duration is None: raise ValueError( ( @@ -439,11 +435,9 @@ def subclip(self, start_time=0, end_time=None): ) else: - end_time = self.duration + end_time if end_time is not None: - new_clip.duration = end_time - start_time new_clip.end = new_clip.start + new_clip.duration diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index b3110984f..e36cebde5 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -279,7 +279,6 @@ class AudioArrayClip(AudioClip): """ def __init__(self, array, fps): - Clip.__init__(self) self.array = array self.fps = fps diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 4ab79b407..dd241f84a 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -57,7 +57,6 @@ class AudioFileClip(AudioClip): def __init__( self, filename, decode_file=False, buffersize=200000, nbytes=2, fps=44100 ): - AudioClip.__init__(self) self.filename = filename diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py index c714c69d6..4f60efb44 100644 --- a/moviepy/audio/io/readers.py +++ b/moviepy/audio/io/readers.py @@ -216,7 +216,6 @@ def get_frame(self, tt): return result else: - ind = int(self.fps * tt) if ind < 0 or ind > self.n_frames: # out of time: return 0 return np.zeros(self.nchannels) diff --git a/moviepy/config.py b/moviepy/config.py index 90d9282de..1ba214f16 100644 --- a/moviepy/config.py +++ b/moviepy/config.py @@ -44,7 +44,6 @@ def try_cmd(cmd): FFMPEG_BINARY = get_exe() elif FFMPEG_BINARY == "auto-detect": - if try_cmd(["ffmpeg"])[0]: FFMPEG_BINARY = "ffmpeg" elif not IS_POSIX_OS and try_cmd(["ffmpeg.exe"])[0]: diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 8b3a68e29..89af5c559 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -327,7 +327,6 @@ def write_videofile( logger = proglog.default_bar_logger(logger) if codec is None: - try: codec = extensions_dict[ext]["codec"][0] except KeyError: @@ -1048,7 +1047,6 @@ def __init__( img = imread(img) if len(img.shape) == 3: # img is (now) a RGB(a) numpy array - if img.shape[2] == 4: if fromalpha: img = 1.0 * img[:, :, 3] / 255 @@ -1248,7 +1246,6 @@ def __init__( remove_temp=True, print_cmd=False, ): - if text is not None: if temptxt is None: temptxt_fd, temptxt = tempfile.mkstemp(suffix=".txt") diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py index cb3c6b711..fec0b0d29 100644 --- a/moviepy/video/compositing/CompositeVideoClip.py +++ b/moviepy/video/compositing/CompositeVideoClip.py @@ -54,7 +54,6 @@ class CompositeVideoClip(VideoClip): def __init__( self, clips, size=None, bg_color=None, use_bgclip=False, is_mask=False ): - if size is None: size = clips[0].size diff --git a/moviepy/video/fx/freeze_region.py b/moviepy/video/fx/freeze_region.py index 6726c4c05..a067ccfed 100644 --- a/moviepy/video/fx/freeze_region.py +++ b/moviepy/video/fx/freeze_region.py @@ -29,7 +29,6 @@ def freeze_region(clip, t=0, region=None, outside_region=None, mask=None): """ if region is not None: - x1, y1, x2, y2 = region freeze = ( clip.fx(crop, *region) @@ -40,7 +39,6 @@ def freeze_region(clip, t=0, region=None, outside_region=None, mask=None): return CompositeVideoClip([clip, freeze]) elif outside_region is not None: - x1, y1, x2, y2 = outside_region animated_region = clip.fx(crop, *outside_region).with_position((x1, y1)) freeze = clip.to_ImageClip(t=t).with_duration(clip.duration) diff --git a/moviepy/video/fx/resize.py b/moviepy/video/fx/resize.py index 843472cd4..1476fbb7d 100644 --- a/moviepy/video/fx/resize.py +++ b/moviepy/video/fx/resize.py @@ -191,7 +191,6 @@ def filter(get_frame, t): new_size = translate_new_size(new_size) elif height is not None: - if hasattr(height, "__call__"): def func(t): @@ -203,7 +202,6 @@ def func(t): new_size = [w * height / h, height] elif width is not None: - if hasattr(width, "__call__"): def func(t): diff --git a/moviepy/video/io/ImageSequenceClip.py b/moviepy/video/io/ImageSequenceClip.py index fd939c49a..930a6feff 100644 --- a/moviepy/video/io/ImageSequenceClip.py +++ b/moviepy/video/io/ImageSequenceClip.py @@ -49,7 +49,6 @@ def __init__( is_mask=False, load_images=False, ): - # CODE WRITTEN AS IT CAME, MAY BE IMPROVED IN THE FUTURE if (fps is None) and (durations is None): @@ -111,12 +110,10 @@ def find_image_index(t): ) if fromfiles: - self.last_index = None self.last_image = None def make_frame(t): - index = find_image_index(t) if index != self.last_index: @@ -126,13 +123,11 @@ def make_frame(t): return self.last_image if with_mask and (imread(self.sequence[0]).shape[2] == 4): - self.mask = VideoClip(is_mask=True) self.mask.last_index = None self.mask.last_image = None def mask_make_frame(t): - index = find_image_index(t) if index != self.mask.last_index: frame = imread(self.sequence[index])[:, :, 3] @@ -147,12 +142,10 @@ def mask_make_frame(t): else: def make_frame(t): - index = find_image_index(t) return self.sequence[index][:, :, :3] if with_mask and (self.sequence[0].shape[2] == 4): - self.mask = VideoClip(is_mask=True) def mask_make_frame(t): diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index 1be47cb45..f18cff612 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -96,7 +96,6 @@ def __init__( fps_source="fps", pixel_format=None, ): - VideoClip.__init__(self) # Make a reader @@ -122,7 +121,6 @@ def __init__( self.filename = filename if has_mask: - self.make_frame = lambda t: self.reader.get_frame(t)[:, :, :3] def mask_make_frame(t): @@ -134,12 +132,10 @@ def mask_make_frame(t): self.mask.fps = self.fps else: - self.make_frame = lambda t: self.reader.get_frame(t) # Make a reader for the audio, if any. if audio and self.reader.infos["audio_found"]: - self.audio = AudioFileClip( filename, buffersize=audio_buffersize, diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 616deff6b..b7be97177 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -25,7 +25,6 @@ def __init__( resize_algo="bicubic", fps_source="fps", ): - self.filename = filename self.proc = None infos = ffmpeg_parse_infos( @@ -577,7 +576,6 @@ def parse(self): # not default audio found, assume first audio stream is the default if self.result["audio_found"] and not self.result.get("audio_bitrate"): - self.result["audio_bitrate"] = None for streams_input in self.result["inputs"]: for stream in streams_input["streams"]: diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index cff303ad7..827724cdc 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -190,14 +190,12 @@ def write_frame(self, img_array): ) elif "bitrate not specified" in ffmpeg_error: - error += ( "\n\nThe video export failed, possibly because the bitrate " "specified was too high or too low for the video codec." ) elif "Invalid encoder type" in ffmpeg_error: - error += ( "\n\nThe video export failed because the codec " "or file extension you provided is not suitable for video" diff --git a/moviepy/video/io/gif_writers.py b/moviepy/video/io/gif_writers.py index b052bcecb..f4240139b 100644 --- a/moviepy/video/io/gif_writers.py +++ b/moviepy/video/io/gif_writers.py @@ -101,7 +101,6 @@ def write_gif_with_tempfiles( logger(message="MoviePy - - Generating GIF frames") for i, t in logger.iter_bar(t=list(enumerate(tt))): - name = "%s_GIFTEMP%04d.png" % (file_root, i + 1) tempfiles.append(name) clip.save_frame(name, t, with_mask=True) @@ -167,7 +166,6 @@ def write_gif_with_tempfiles( logger(message="MoviePy - GIF ready: %s." % filename) except (IOError, OSError) as err: - error = ( "MoviePy Error: creation of %s failed because " "of the following error:\n\n%s.\n\n." % (filename, str(err)) @@ -335,7 +333,6 @@ def write_gif( **popen_params, ) else: - popen_params["stdin"] = sp.PIPE popen_params["stdout"] = sp.PIPE @@ -344,7 +341,6 @@ def write_gif( ) if program == "ImageMagick": - cmd2 = [ IMAGEMAGICK_BINARY, "-delay", @@ -368,7 +364,6 @@ def write_gif( proc2 = sp.Popen(cmd2 + ["gif:-"], **popen_params) if opt: - cmd3 = ( [ IMAGEMAGICK_BINARY, @@ -399,7 +394,6 @@ def write_gif( proc1.stdin.write(frame.tobytes()) except IOError as err: - error = ( "[MoviePy] Error: creation of %s failed because " "of the following error:\n\n%s.\n\n." % (filename, str(err)) @@ -450,5 +444,4 @@ def write_gif_with_image_io( logger(message="MoviePy - Building file %s with imageio." % filename) for frame in clip.iter_frames(fps=fps, logger=logger, dtype="uint8"): - writer.append_data(frame) diff --git a/moviepy/video/io/preview.py b/moviepy/video/io/preview.py index 30ac4c428..c936726fe 100644 --- a/moviepy/video/io/preview.py +++ b/moviepy/video/io/preview.py @@ -159,7 +159,6 @@ def preview( t0 = time.time() for t in np.arange(1.0 / fps, clip.duration - 0.001, 1.0 / fps): - img = clip.get_frame(t) for event in pg.event.get(): diff --git a/moviepy/video/tools/cuts.py b/moviepy/video/tools/cuts.py index 40ff5fca3..1bf4e6cc6 100644 --- a/moviepy/video/tools/cuts.py +++ b/moviepy/video/tools/cuts.py @@ -253,8 +253,7 @@ def distance(t1, t2): matching_frames = [] # the final result. - for (t, frame) in clip.iter_frames(with_times=True, logger=logger): - + for t, frame in clip.iter_frames(with_times=True, logger=logger): flat_frame = 1.0 * frame.flatten() F_norm_sq = dot_product(flat_frame, flat_frame) F_norm = np.sqrt(F_norm_sq) @@ -359,7 +358,7 @@ def select_scenes( nomatch_threshold = match_threshold dict_starts = defaultdict(lambda: []) - for (start, end, min_distance, max_distance) in self: + for start, end, min_distance, max_distance in self: dict_starts[start].append([end, min_distance, max_distance]) starts_ends = sorted(dict_starts.items(), key=lambda k: k[0]) @@ -367,7 +366,6 @@ def select_scenes( result = [] min_start = 0 for start, ends_distances in starts_ends: - if start < min_start: continue @@ -445,7 +443,7 @@ def write_gifs(self, clip, gifs_dir, **kwargs): MoviePy - Building file foo/00000128_00000372.gif with imageio. MoviePy - Building file foo/00000140_00000360.gif with imageio. """ - for (start, end, _, _) in self: + for start, end, _, _ in self: name = "%s/%08d_%08d.gif" % (gifs_dir, 100 * start, 100 * end) clip.subclip(start, end).write_gif(name, **kwargs) diff --git a/moviepy/video/tools/interpolators.py b/moviepy/video/tools/interpolators.py index 83db3be3d..268ad3a64 100644 --- a/moviepy/video/tools/interpolators.py +++ b/moviepy/video/tools/interpolators.py @@ -38,7 +38,6 @@ class Interpolator: """ def __init__(self, tt=None, ss=None, ttss=None, left=None, right=None): - if ttss is not None: tt, ss = zip(*ttss) @@ -88,7 +87,6 @@ class Trajectory: """ def __init__(self, tt, xx, yy): - self.tt = 1.0 * np.array(tt) self.xx = np.array(xx) self.yy = np.array(yy) diff --git a/moviepy/video/tools/subtitles.py b/moviepy/video/tools/subtitles.py index b2a9b841f..0b676d6ef 100644 --- a/moviepy/video/tools/subtitles.py +++ b/moviepy/video/tools/subtitles.py @@ -43,7 +43,6 @@ class SubtitlesClip(VideoClip): """ def __init__(self, subtitles, make_textclip=None, encoding=None): - VideoClip.__init__(self, has_constant_size=False) if not isinstance(subtitles, list): diff --git a/moviepy/video/tools/tracking.py b/moviepy/video/tools/tracking.py index 8546074bc..ab0df233f 100644 --- a/moviepy/video/tools/tracking.py +++ b/moviepy/video/tools/tracking.py @@ -100,14 +100,11 @@ def manual_tracking(clip, t1=None, t2=None, fps=None, n_objects=1, savefile=None txy_list = [] def gatherClicks(t): - imdisplay(clip.get_frame(t), screen) objects_to_click = n_objects clicks = [] while objects_to_click: - for event in pg.event.get(): - if event.type == pg.KEYDOWN: if event.key == pg.K_BACKSLASH: return "return" @@ -122,7 +119,6 @@ def gatherClicks(t): return clicks while t < t2: - clicks = gatherClicks(t) if clicks == "return": txy_list.pop() diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index 3fca0ec65..e92f78e04 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -37,7 +37,7 @@ def test_ffmpeg_resizing(): for target_resolution in target_resolutions: video = VideoFileClip(video_file, target_resolution=target_resolution) frame = video.get_frame(0) - for (target, observed) in zip(target_resolution[::-1], frame.shape): + for target, observed in zip(target_resolution[::-1], frame.shape): if target is not None: assert target == observed video.close() diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py index e566bc558..f01127b73 100644 --- a/tests/test_ffmpeg_reader.py +++ b/tests/test_ffmpeg_reader.py @@ -727,7 +727,6 @@ def test_failure_to_release_file(util): # Repeat this so we can see that the problems escalate: for i in range(5): - # Create a random video file. red = ColorClip((256, 200), color=(255, 0, 0)) green = ColorClip((256, 200), color=(0, 255, 0)) diff --git a/tests/test_ffmpeg_writer.py b/tests/test_ffmpeg_writer.py index 23025a2c6..89ee48e99 100644 --- a/tests/test_ffmpeg_writer.py +++ b/tests/test_ffmpeg_writer.py @@ -238,7 +238,6 @@ def test_write_gif(util, clip_class, opt, loop, with_mask, pixel_format): ) if pixel_format != "invalid": - final_clip = VideoFileClip(filename) r, g, b = final_clip.get_frame(0)[0][0] diff --git a/tests/test_fx.py b/tests/test_fx.py index 23a646d94..0f4990302 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -1486,7 +1486,6 @@ def test_audio_delay(stereo_wave, duration, offset, n_repeats, decay): decayments = np.linspace(1, max(0, decay), n_repeats) for i in range(n_repeats + 1): # first clip, is not part of the repeated ones - if i == n_repeats: # the delay ends in sound, so last muted chunk does not exists break diff --git a/tests/test_issues.py b/tests/test_issues.py index 1abcedc6e..449dc418e 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -245,7 +245,6 @@ def size(t): def test_issue_354(): with ImageClip("media/python_logo.png") as clip: - clip.duration = 10 crosstime = 1 diff --git a/tests/test_videotools.py b/tests/test_videotools.py index 8d126dbb6..94ba0a7f2 100644 --- a/tests/test_videotools.py +++ b/tests/test_videotools.py @@ -654,7 +654,6 @@ def test_color_gradient( assert np.array_equal(result, expected_result) if shape == "radial": - circle_result = circle( size, p1, From da7d9f7a5d25a5d5bbad00d62cfe03e6bf4d822c Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Fri, 17 Jan 2025 15:37:20 +0100 Subject: [PATCH 10/19] Add support for new argument ch_layout on ffmpeg >= 7 --- CHANGELOG.md | 1 + moviepy/audio/io/ffplay_audiopreviewer.py | 28 +++++--- moviepy/version.py | 2 +- moviepy/video/io/ffmpeg_tools.py | 87 ++++++++++++++++++++++- 4 files changed, 106 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3d1c79e..25cc0398b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve perfs of decorator by pre-computing arguments - Fix textclip being cut or of impredictable height (see issues #2325, #2260 and #2268) - Fix TimeMirror and TimeSymmetrize cutting last second of clip +- Fix audiopreview not working with ffplay >= 7.0.0 ## [v2.1.2](https://github.com/zulko/moviepy/tree/master) diff --git a/moviepy/audio/io/ffplay_audiopreviewer.py b/moviepy/audio/io/ffplay_audiopreviewer.py index e1217ab56..fbfcfcacc 100644 --- a/moviepy/audio/io/ffplay_audiopreviewer.py +++ b/moviepy/audio/io/ffplay_audiopreviewer.py @@ -5,7 +5,7 @@ from moviepy.config import FFPLAY_BINARY from moviepy.decorators import requires_duration from moviepy.tools import cross_platform_popen_params - +from moviepy.video.io import ffmpeg_tools class FFPLAY_AudioPreviewer: """ @@ -24,7 +24,6 @@ class FFPLAY_AudioPreviewer: nchannels: Number of audio channels in the clip. Default to 2 channels. - """ def __init__( @@ -42,8 +41,22 @@ def __init__( "s%dle" % (8 * nbytes), "-ar", "%d" % fps_input, - "-ac", - "%d" % nchannels, + ] + + # Adapt number of channels argument to ffplay version + ffplay_version = ffmpeg_tools.ffplay_version()[1] + if int(ffplay_version.split('.')[0]) >= 7: + cmd += [ + "-ch_layout", + "stereo" if nchannels == 2 else "mono", + ] + else : + cmd += [ + "-ac", + "%d" % nchannels, + ] + + cmd += [ "-i", "-", ] @@ -62,12 +75,7 @@ def write_frames(self, frames_array): _, ffplay_error = self.proc.communicate() if ffplay_error is not None: ffplay_error = ffplay_error.decode() - else: - # The error was redirected to a logfile with `write_logfile=True`, - # so read the error from that file instead - self.logfile.seek(0) - ffplay_error = self.logfile.read() - + error = ( f"{err}\n\nMoviePy error: FFPLAY encountered the following error while " f":\n\n {ffplay_error}" diff --git a/moviepy/version.py b/moviepy/version.py index 58039f505..4eabd0b3f 100644 --- a/moviepy/version.py +++ b/moviepy/version.py @@ -1 +1 @@ -__version__ = "2.1.1" +__version__ = "2.1.2" diff --git a/moviepy/video/io/ffmpeg_tools.py b/moviepy/video/io/ffmpeg_tools.py index e9716ffbb..0da6921a0 100644 --- a/moviepy/video/io/ffmpeg_tools.py +++ b/moviepy/video/io/ffmpeg_tools.py @@ -2,7 +2,11 @@ import os -from moviepy.config import FFMPEG_BINARY +import subprocess + +import re + +from moviepy.config import FFMPEG_BINARY, FFPLAY_BINARY from moviepy.decorators import convert_parameter_to_seconds, convert_path_to_string from moviepy.tools import ffmpeg_escape_filename, subprocess_call @@ -207,3 +211,84 @@ def ffmpeg_stabilize_video( cmd.append("-y") subprocess_call(cmd, logger=logger) + + +def ffmpeg_version(): + """ + Retrieve the FFmpeg version. + + This function retrieves both the full and numeric version of FFmpeg + by executing the `ffmpeg -version` command. The full version includes + additional details like build information, while the numeric version + contains only the version numbers (e.g., '7.0.2'). + + Return + ------ + tuple + A tuple containing: + - `full_version` (str): The complete version string (e.g., '7.0.2-static'). + - `numeric_version` (str): The numeric version string (e.g., '7.0.2'). + + Example + ------- + >>> ffmpeg_version() + ('7.0.2-static', '7.0.2') + + Raises + ------ + subprocess.CalledProcessError + If the FFmpeg command fails to execute properly. + """ + cmd = [ + FFMPEG_BINARY, + "-version", + "-v" + "quiet", + ] + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + # Extract the version number from the first line of output + full_version = result.stdout.splitlines()[0].split()[2] + numeric_version = re.match(r'^[0-9.]+', full_version).group(0) + return (full_version, numeric_version) + + +def ffplay_version(): + """ + Retrieve the FFplay version. + + This function retrieves both the full and numeric version of FFplay + by executing the `ffplay -version` command. The full version includes + additional details like build information, while the numeric version + contains only the version numbers (e.g., '6.0.1'). + + Return + ------ + tuple + A tuple containing: + - `full_version` (str): The complete version string (e.g., '6.0.1-static'). + - `numeric_version` (str): The numeric version string (e.g., '6.0.1'). + + Example + ------- + >>> ffplay_version() + ('6.0.1-static', '6.0.1') + + Raises + ------ + subprocess.CalledProcessError + If the FFplay command fails to execute properly. + """ + cmd = [ + FFPLAY_BINARY, + "-version", + ] + + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + # Extract the version number from the first line of output + full_version = result.stdout.splitlines()[0].split()[2] + numeric_version = re.match(r'^[0-9.]+', full_version).group(0) + return (full_version, numeric_version) + + From 70ef9f770c4c0a6521031953ae6e88fe632a52b5 Mon Sep 17 00:00:00 2001 From: Implosiv3 Date: Mon, 20 Jan 2025 08:00:12 +0100 Subject: [PATCH 11/19] Fix issue #2330 audio write wrong fps Fixes a bug that is not setting the instance 'fps' value when it has the attribute, but doing it when it doesn't. --- moviepy/audio/AudioClip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index 5aa10e912..3a04b40e1 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -240,9 +240,9 @@ def write_audiofile( """ if not fps: if hasattr(self, "fps"): - fps = 44100 - else: fps = self.fps + else: + fps = 44100 if codec is None: name, ext = os.path.splitext(os.path.basename(filename)) From f12ee5af262d89701d48806676a9868052e2354e Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Wed, 22 Jan 2025 22:35:22 +0100 Subject: [PATCH 12/19] Fix to support changes in ffmpeg >= 7 --- moviepy/video/io/ffmpeg_reader.py | 17 ++++++++++++++--- tests/test_PR.py | 2 ++ tests/test_ffmpeg_reader.py | 10 +++++++++- tests/test_ffmpeg_tools.py | 27 +++++++++++++++------------ 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 8f6835aa6..64cfc7975 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -418,7 +418,7 @@ def parse(self): self.result["duration"] = self.parse_duration(line) # parse global bitrate (in kb/s) - bitrate_match = re.search(r"bitrate: (\d+) kb/s", line) + bitrate_match = re.search(r"bitrate: (\d+) k(i?)b/s", line) self.result["bitrate"] = ( int(bitrate_match.group(1)) if bitrate_match else None ) @@ -528,8 +528,11 @@ def parse(self): if self._current_stream["stream_type"] == "video": field, value = self.video_metadata_type_casting(field, value) + # ffmpeg 7 now use displaymatrix instead of rotate if field == "rotate": self.result["video_rotation"] = value + elif field == "displaymatrix": + self.result["video_rotation"] = value # multiline metadata value parsing if field == "": @@ -644,7 +647,7 @@ def parse_audio_stream_data(self, line): # AttributeError: 'NoneType' object has no attribute 'group' # ValueError: invalid literal for int() with base 10: '' stream_data["fps"] = "unknown" - match_audio_bitrate = re.search(r"(\d+) kb/s", line) + match_audio_bitrate = re.search(r"(\d+) k(i?)b/s", line) stream_data["bitrate"] = ( int(match_audio_bitrate.group(1)) if match_audio_bitrate else None ) @@ -672,7 +675,7 @@ def parse_video_stream_data(self, line): % (self.filename, self.infos) ) - match_bitrate = re.search(r"(\d+) kb/s", line) + match_bitrate = re.search(r"(\d+) k(i?)b/s", line) stream_data["bitrate"] = int(match_bitrate.group(1)) if match_bitrate else None # Get the frame rate. Sometimes it's 'tbr', sometimes 'fps', sometimes @@ -785,6 +788,14 @@ def video_metadata_type_casting(self, field, value): """Cast needed video metadata fields to other types than the default str.""" if field == "rotate": return (field, float(value)) + + elif field == "displaymatrix": + match = re.search(r"[-+]?\d+(\.\d+)?", value) + if match: + # We must multiply by -1 because displaymatrix return info + # about how to rotate to show video, not about video rotation + return (field, float(match.group()) * -1) + return (field, value) diff --git a/tests/test_PR.py b/tests/test_PR.py index 85bb2f3d3..33ae58464 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -69,6 +69,8 @@ def test_PR_528(util): def test_PR_529(): + #print(ffmpeg_tools.ffplay_version()) + print(ffmpeg_tools.ffmpeg_version()) with VideoFileClip("media/fire2.mp4") as video_clip: assert video_clip.rotation == 180 diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py index 40a520c12..e28fd80b3 100644 --- a/tests/test_ffmpeg_reader.py +++ b/tests/test_ffmpeg_reader.py @@ -16,6 +16,9 @@ FFmpegInfosParser, ffmpeg_parse_infos, ) +from moviepy.video.io.ffmpeg_tools import ( + ffmpeg_version, +) from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.VideoClip import BitmapClip, ColorClip @@ -59,7 +62,7 @@ def test_ffmpeg_parse_infos_video_nframes(): ("decode_file", "expected_duration"), ( (False, 30), - (True, 30.02), + (True, 30), ), ids=( "decode_file=False", @@ -69,6 +72,11 @@ def test_ffmpeg_parse_infos_video_nframes(): def test_ffmpeg_parse_infos_decode_file(decode_file, expected_duration): """Test `decode_file` argument of `ffmpeg_parse_infos` function.""" d = ffmpeg_parse_infos("media/big_buck_bunny_0_30.webm", decode_file=decode_file) + + # On old version of ffmpeg, duration and video duration was different + if int(ffmpeg_version()[1].split('.')[0]) < 7: + expected_duration += 0.02 + assert d["duration"] == expected_duration # check metadata is fine diff --git a/tests/test_ffmpeg_tools.py b/tests/test_ffmpeg_tools.py index bc9a8323f..366d4bfe0 100644 --- a/tests/test_ffmpeg_tools.py +++ b/tests/test_ffmpeg_tools.py @@ -9,6 +9,7 @@ ffmpeg_extract_subclip, ffmpeg_resize, ffmpeg_stabilize_video, + ffmpeg_version ) from moviepy.video.io.VideoFileClip import VideoFileClip @@ -57,9 +58,10 @@ def test_ffmpeg_resize(util): ffmpeg_resize("media/bitmap.mp4", outputfile, expected_size, logger=None) assert os.path.isfile(outputfile) - # overwrite file - with pytest.raises(OSError): - ffmpeg_resize("media/bitmap.mp4", outputfile, expected_size, logger=None) + # overwrite file on old version of ffmpeg + if int(ffmpeg_version()[1].split('.')[0]) < 7: + with pytest.raises(OSError): + ffmpeg_resize("media/bitmap.mp4", outputfile, expected_size, logger=None) clip = VideoFileClip(outputfile) assert clip.size[0] == expected_size[0] @@ -98,15 +100,16 @@ def test_ffmpeg_stabilize_video(util): expected_filepath = os.path.join(stabilize_video_tempdir, "foo.mp4") assert os.path.isfile(expected_filepath) - # don't overwrite file - with pytest.raises(OSError): - ffmpeg_stabilize_video( - "media/bitmap.mp4", - output_dir=stabilize_video_tempdir, - outputfile="foo.mp4", - overwrite_file=False, - logger=None, - ) + # don't overwrite file on old version of ffmpeg + if int(ffmpeg_version()[1].split('.')[0]) < 7: + with pytest.raises(OSError): + ffmpeg_stabilize_video( + "media/bitmap.mp4", + output_dir=stabilize_video_tempdir, + outputfile="foo.mp4", + overwrite_file=False, + logger=None, + ) if os.path.isdir(stabilize_video_tempdir): try: From 25b61eb9ea62db5f5f0515e6dc41db7939712778 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Wed, 22 Jan 2025 22:38:21 +0100 Subject: [PATCH 13/19] linting --- moviepy/audio/io/ffplay_audiopreviewer.py | 7 +++--- moviepy/video/io/ffmpeg_reader.py | 16 +++++++------- moviepy/video/io/ffmpeg_tools.py | 27 +++++++++-------------- tests/test_PR.py | 2 +- tests/test_ffmpeg_reader.py | 6 ++--- tests/test_ffmpeg_tools.py | 6 ++--- 6 files changed, 29 insertions(+), 35 deletions(-) diff --git a/moviepy/audio/io/ffplay_audiopreviewer.py b/moviepy/audio/io/ffplay_audiopreviewer.py index fbfcfcacc..c7bdb251b 100644 --- a/moviepy/audio/io/ffplay_audiopreviewer.py +++ b/moviepy/audio/io/ffplay_audiopreviewer.py @@ -7,6 +7,7 @@ from moviepy.tools import cross_platform_popen_params from moviepy.video.io import ffmpeg_tools + class FFPLAY_AudioPreviewer: """ A class to preview an AudioClip. @@ -45,12 +46,12 @@ def __init__( # Adapt number of channels argument to ffplay version ffplay_version = ffmpeg_tools.ffplay_version()[1] - if int(ffplay_version.split('.')[0]) >= 7: + if int(ffplay_version.split(".")[0]) >= 7: cmd += [ "-ch_layout", "stereo" if nchannels == 2 else "mono", ] - else : + else: cmd += [ "-ac", "%d" % nchannels, @@ -75,7 +76,7 @@ def write_frames(self, frames_array): _, ffplay_error = self.proc.communicate() if ffplay_error is not None: ffplay_error = ffplay_error.decode() - + error = ( f"{err}\n\nMoviePy error: FFPLAY encountered the following error while " f":\n\n {ffplay_error}" diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 64cfc7975..c9d073b6c 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -476,12 +476,12 @@ def parse(self): # for default streams, set their numbers globally, so it's # easy to get without iterating all if self._current_stream["default"]: - self.result[ - f"default_{stream_type_lower}_input_number" - ] = input_number - self.result[ - f"default_{stream_type_lower}_stream_number" - ] = stream_number + self.result[f"default_{stream_type_lower}_input_number"] = ( + input_number + ) + self.result[f"default_{stream_type_lower}_stream_number"] = ( + stream_number + ) # exit chapter if self._current_chapter: @@ -788,14 +788,14 @@ def video_metadata_type_casting(self, field, value): """Cast needed video metadata fields to other types than the default str.""" if field == "rotate": return (field, float(value)) - + elif field == "displaymatrix": match = re.search(r"[-+]?\d+(\.\d+)?", value) if match: # We must multiply by -1 because displaymatrix return info # about how to rotate to show video, not about video rotation return (field, float(match.group()) * -1) - + return (field, value) diff --git a/moviepy/video/io/ffmpeg_tools.py b/moviepy/video/io/ffmpeg_tools.py index 0da6921a0..c154d9b59 100644 --- a/moviepy/video/io/ffmpeg_tools.py +++ b/moviepy/video/io/ffmpeg_tools.py @@ -1,10 +1,8 @@ """Miscellaneous bindings to ffmpeg.""" import os - -import subprocess - import re +import subprocess from moviepy.config import FFMPEG_BINARY, FFPLAY_BINARY from moviepy.decorators import convert_parameter_to_seconds, convert_path_to_string @@ -217,9 +215,9 @@ def ffmpeg_version(): """ Retrieve the FFmpeg version. - This function retrieves both the full and numeric version of FFmpeg - by executing the `ffmpeg -version` command. The full version includes - additional details like build information, while the numeric version + This function retrieves both the full and numeric version of FFmpeg + by executing the `ffmpeg -version` command. The full version includes + additional details like build information, while the numeric version contains only the version numbers (e.g., '7.0.2'). Return @@ -242,15 +240,14 @@ def ffmpeg_version(): cmd = [ FFMPEG_BINARY, "-version", - "-v" - "quiet", + "-v", "quiet", ] result = subprocess.run(cmd, capture_output=True, text=True, check=True) - + # Extract the version number from the first line of output full_version = result.stdout.splitlines()[0].split()[2] - numeric_version = re.match(r'^[0-9.]+', full_version).group(0) + numeric_version = re.match(r"^[0-9.]+", full_version).group(0) return (full_version, numeric_version) @@ -258,9 +255,9 @@ def ffplay_version(): """ Retrieve the FFplay version. - This function retrieves both the full and numeric version of FFplay - by executing the `ffplay -version` command. The full version includes - additional details like build information, while the numeric version + This function retrieves both the full and numeric version of FFplay + by executing the `ffplay -version` command. The full version includes + additional details like build information, while the numeric version contains only the version numbers (e.g., '6.0.1'). Return @@ -288,7 +285,5 @@ def ffplay_version(): result = subprocess.run(cmd, capture_output=True, text=True, check=True) # Extract the version number from the first line of output full_version = result.stdout.splitlines()[0].split()[2] - numeric_version = re.match(r'^[0-9.]+', full_version).group(0) + numeric_version = re.match(r"^[0-9.]+", full_version).group(0) return (full_version, numeric_version) - - diff --git a/tests/test_PR.py b/tests/test_PR.py index 33ae58464..0acd3f687 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -69,7 +69,7 @@ def test_PR_528(util): def test_PR_529(): - #print(ffmpeg_tools.ffplay_version()) + # print(ffmpeg_tools.ffplay_version()) print(ffmpeg_tools.ffmpeg_version()) with VideoFileClip("media/fire2.mp4") as video_clip: assert video_clip.rotation == 180 diff --git a/tests/test_ffmpeg_reader.py b/tests/test_ffmpeg_reader.py index e28fd80b3..4269616b4 100644 --- a/tests/test_ffmpeg_reader.py +++ b/tests/test_ffmpeg_reader.py @@ -16,9 +16,7 @@ FFmpegInfosParser, ffmpeg_parse_infos, ) -from moviepy.video.io.ffmpeg_tools import ( - ffmpeg_version, -) +from moviepy.video.io.ffmpeg_tools import ffmpeg_version from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.VideoClip import BitmapClip, ColorClip @@ -74,7 +72,7 @@ def test_ffmpeg_parse_infos_decode_file(decode_file, expected_duration): d = ffmpeg_parse_infos("media/big_buck_bunny_0_30.webm", decode_file=decode_file) # On old version of ffmpeg, duration and video duration was different - if int(ffmpeg_version()[1].split('.')[0]) < 7: + if int(ffmpeg_version()[1].split(".")[0]) < 7: expected_duration += 0.02 assert d["duration"] == expected_duration diff --git a/tests/test_ffmpeg_tools.py b/tests/test_ffmpeg_tools.py index 366d4bfe0..af66de2ea 100644 --- a/tests/test_ffmpeg_tools.py +++ b/tests/test_ffmpeg_tools.py @@ -9,7 +9,7 @@ ffmpeg_extract_subclip, ffmpeg_resize, ffmpeg_stabilize_video, - ffmpeg_version + ffmpeg_version, ) from moviepy.video.io.VideoFileClip import VideoFileClip @@ -59,7 +59,7 @@ def test_ffmpeg_resize(util): assert os.path.isfile(outputfile) # overwrite file on old version of ffmpeg - if int(ffmpeg_version()[1].split('.')[0]) < 7: + if int(ffmpeg_version()[1].split(".")[0]) < 7: with pytest.raises(OSError): ffmpeg_resize("media/bitmap.mp4", outputfile, expected_size, logger=None) @@ -101,7 +101,7 @@ def test_ffmpeg_stabilize_video(util): assert os.path.isfile(expected_filepath) # don't overwrite file on old version of ffmpeg - if int(ffmpeg_version()[1].split('.')[0]) < 7: + if int(ffmpeg_version()[1].split(".")[0]) < 7: with pytest.raises(OSError): ffmpeg_stabilize_video( "media/bitmap.mp4", From 3245040eca93da6d522d00d3496c87db1ca26e93 Mon Sep 17 00:00:00 2001 From: Pierre-Lin Bonnemaison Date: Wed, 22 Jan 2025 22:44:43 +0100 Subject: [PATCH 14/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c15fd6c86..8eb9b4b27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add codecs to .mov files - Add background radius to text clips - Support pillow 11 +- Add support for ffmpeg v7 ### Changed - Subclipping outside of clip boundaries now raise an exception From 0b74192dcdac87b2e58263caa1815734171e014b Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Wed, 22 Jan 2025 23:22:32 +0100 Subject: [PATCH 15/19] Fix Freeze effect removing original start and end parameters --- moviepy/Clip.py | 18 ++++++++++++++++++ moviepy/video/fx/Freeze.py | 14 +++++++++++++- moviepy/video/io/ffmpeg_tools.py | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index e931f5551..92fbfddf0 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -218,6 +218,15 @@ def with_start(self, t, change_end=True): These changes are also applied to the ``audio`` and ``mask`` clips of the current clip, if they exist. + note:: + The start and end attribute of a clip define when a clip will start + playing when used in a composite video clip, not the start time of + the clip itself. + + i.e: with_start(10) mean the clip will still start at his first frame, + but if used in a composite video clip it will only start to show at + 10 seconds. + Parameters ---------- @@ -248,6 +257,15 @@ def with_end(self, t): (hour, min, sec), or as a string: '01:03:05.35'. Also sets the duration of the mask and audio, if any, of the returned clip. + note:: + The start and end attribute of a clip define when a clip will start + playing when used in a composite video clip, not the start time of + the clip itself. + + i.e: with_start(10) mean the clip will still start at his first frame, + but if used in a composite video clip it will only start to show at + 10 seconds. + Parameters ---------- diff --git a/moviepy/video/fx/Freeze.py b/moviepy/video/fx/Freeze.py index 40dc3c9f9..532f2558d 100644 --- a/moviepy/video/fx/Freeze.py +++ b/moviepy/video/fx/Freeze.py @@ -15,12 +15,16 @@ class Freeze(Effect): With ``total_duration`` you can specify the total duration of the clip and the freeze (i.e. the duration of the freeze is automatically computed). One of them must be provided. + + With ``update_start_end`` you can define if the effect must preserve + and/or update start and end properties of the original clip """ t: float = 0 freeze_duration: float = None total_duration: float = None padding_end: float = 0 + update_start_end: bool = True def apply(self, clip: Clip) -> Clip: """Apply the effect to the clip.""" @@ -40,4 +44,12 @@ def apply(self, clip: Clip) -> Clip: before = [clip[: self.t]] if (self.t != 0) else [] freeze = [clip.to_ImageClip(self.t).with_duration(self.freeze_duration)] after = [clip[self.t :]] if (self.t != clip.duration) else [] - return concatenate_videoclips(before + freeze + after) + + new_clip = concatenate_videoclips(before + freeze + after) + if self.update_start_end: + if clip.start is not None: + new_clip = new_clip.with_start(clip.start) + if clip.end is not None: + new_clip = new_clip.with_end(clip.end + self.freeze_duration) + + return new_clip diff --git a/moviepy/video/io/ffmpeg_tools.py b/moviepy/video/io/ffmpeg_tools.py index c154d9b59..8cb5351c7 100644 --- a/moviepy/video/io/ffmpeg_tools.py +++ b/moviepy/video/io/ffmpeg_tools.py @@ -240,7 +240,8 @@ def ffmpeg_version(): cmd = [ FFMPEG_BINARY, "-version", - "-v", "quiet", + "-v", + "quiet", ] result = subprocess.run(cmd, capture_output=True, text=True, check=True) From 030c147722c201abfdfb204abb326438c1bea795 Mon Sep 17 00:00:00 2001 From: Pierre-Lin Bonnemaison Date: Wed, 22 Jan 2025 23:31:56 +0100 Subject: [PATCH 16/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb9b4b27..76afb4e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Subclipping outside of clip boundaries now raise an exception +- Freeze effect no longer remove start and end ### Deprecated From 0ca2fea675230030f496de1d4d49556074897089 Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Thu, 23 Jan 2025 17:39:08 +0100 Subject: [PATCH 17/19] Add support for default pillow font --- CHANGELOG.md | 1 + moviepy/video/VideoClip.py | 55 +++++++++++++++++++++++--------------- tests/test_TextClip.py | 7 +++++ 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25cc0398b..6baefdc0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add codecs to .mov files - Add background radius to text clips - Support pillow 11 +- Add support for Pillow default font on textclip ### Changed diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index e64d8a37f..96a66578a 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -1450,7 +1450,8 @@ class TextClip(ImageClip): ---------- font - Path to the font to use. Must be an OpenType font. + Path to the font to use. Must be an OpenType font. If set to None + (default) will use Pillow default font text A string of the text to write. Can be replaced by argument @@ -1549,7 +1550,7 @@ class TextClip(ImageClip): @convert_path_to_string("filename") def __init__( self, - font, + font=None, text=None, filename=None, font_size=None, @@ -1567,12 +1568,13 @@ def __init__( transparent=True, duration=None, ): - try: - _ = ImageFont.truetype(font) - except Exception as e: - raise ValueError( - "Invalid font {}, pillow failed to use it with error {}".format(font, e) - ) + if font is not None: + try: + _ = ImageFont.truetype(font) + except Exception as e: + raise ValueError( + "Invalid font {}, pillow failed to use it with error {}".format(font, e) + ) if filename: with open(filename, "r") as file: @@ -1620,30 +1622,30 @@ def __init__( allow_break=True, ) - if img_height is None: - img_height = self.__find_text_size( + # Add line breaks whenever needed + text = "\n".join( + self.__break_text( + width=img_width, text=text, font=font, font_size=font_size, stroke_width=stroke_width, align=text_align, spacing=interline, - max_width=img_width, - allow_break=True, - )[1] + ) + ) - # Add line breaks whenever needed - text = "\n".join( - self.__break_text( - width=img_width, + if img_height is None: + img_height = self.__find_text_size( text=text, font=font, font_size=font_size, stroke_width=stroke_width, align=text_align, spacing=interline, - ) - ) + max_width=img_width, + allow_break=True, + )[1] elif method == "label": if font_size is None and img_width is None: @@ -1693,7 +1695,10 @@ def __init__( bg_color = (0, 0, 0, 0) img = Image.new(img_mode, (img_width, img_height), color=bg_color) - pil_font = ImageFont.truetype(font, font_size) + if font: + pil_font = ImageFont.truetype(font, font_size) + else: + pil_font = ImageFont.load_default(font_size) draw = ImageDraw.Draw(img) # Dont need allow break here, because we already breaked in caption @@ -1760,7 +1765,10 @@ def __break_text( ) -> List[str]: """Break text to never overflow a width""" img = Image.new("RGB", (1, 1)) - font_pil = ImageFont.truetype(font, font_size) + if font: + font_pil = ImageFont.truetype(font, font_size) + else: + font_pil = ImageFont.load_default(font_size) draw = ImageDraw.Draw(img) lines = [] @@ -1843,7 +1851,10 @@ def __find_text_size( ``real_font_size + (stroke_width * 2) + (lines - 1) * height`` """ img = Image.new("RGB", (1, 1)) - font_pil = ImageFont.truetype(font, font_size) + if font: + font_pil = ImageFont.truetype(font, font_size) + else: + font_pil = ImageFont.load_default(font_size) ascent, descent = font_pil.getmetrics() real_font_size = ascent + descent draw = ImageDraw.Draw(img) diff --git a/tests/test_TextClip.py b/tests/test_TextClip.py index 1d75c1b81..7f87d09ee 100644 --- a/tests/test_TextClip.py +++ b/tests/test_TextClip.py @@ -133,5 +133,12 @@ def test_label_autosizing(util): assert not np.allclose(last_three_columns, [0, 0, 0], rtol=0.01) +def test_no_font(util): + # Try make a clip with default font + clip = TextClip(text="Hello world !", font_size=20, color="white") + clip.show(1) + assert clip.size[0] > 10 + + if __name__ == "__main__": pytest.main() From 9009cbc3a9196d7ba51e79f0dafb7326db33136f Mon Sep 17 00:00:00 2001 From: George Tsertsvadze Date: Sat, 25 Jan 2025 00:41:34 +0000 Subject: [PATCH 18/19] Update config.py check() to log correct binary name When finding FFMPEG_BINARY, it correctly displays that it found ffmpeg. But when finding FFPLAY_BINARY it again displays ffmpeg. This changes it to display ffplay --- moviepy/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/moviepy/config.py b/moviepy/config.py index 4a32acaff..965d92771 100644 --- a/moviepy/config.py +++ b/moviepy/config.py @@ -78,9 +78,9 @@ def check(): print(f"MoviePy: can't find or access ffmpeg in '{FFMPEG_BINARY}'.") if try_cmd([FFPLAY_BINARY])[0]: - print(f"MoviePy: ffmpeg successfully found in '{FFPLAY_BINARY}'.") + print(f"MoviePy: ffplay successfully found in '{FFPLAY_BINARY}'.") else: # pragma: no cover - print(f"MoviePy: can't find or access ffmpeg in '{FFPLAY_BINARY}'.") + print(f"MoviePy: can't find or access ffplay in '{FFPLAY_BINARY}'.") if DOTENV: print(f"\n.env file content at {DOTENV}:\n") From 0f876ffecaf359b0a99b8d6c08e4843210002d31 Mon Sep 17 00:00:00 2001 From: Pierre-Lin Bonnemaison Date: Sun, 26 Jan 2025 22:06:05 +0100 Subject: [PATCH 19/19] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acd6e450..1a2a26a11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Subclipping outside of clip boundaries now raise an exception - Freeze effect no longer remove start and end +- Add a parameter to define audio codec of a clip ### Deprecated