Repository: Zulko/moviepy
Pull request #2346 (2025-01-25 08:49:30)

Use audio codec for existing audiofile
<!--
Please tick when you have done these. They don't need to all be completed before the PR is submitted.
Delete them if they are not appropriate for this pull request.
-->

`moviepy.video.VideoClip.write_videofile` ignores `audio_codec` when `audio` is a file name. I have fixed it in this PR.

Although #1906 was closed when v2 was released, I am reopening this PR because I still believe it will improve functionality.

- [x] I have provided code that clearly demonstrates the bug and that only works correctly when applying this fix
  - `moviepy.video.VideoClip.write_videofile("output.mp4", audio="input.wav", fps=60, audio_codec="aac")` have run `ffmpeg` with `-acodec copy`. With this PR, it will run the command with `-acodec aac`.
- [x] I have added suitable tests demonstrating a fixed bug or new/changed feature to the test suite in `tests/`
- [x] I have properly documented new or changed features in the documentation or in the docstrings
- [x] I have properly explained unusual or unexpected code in the comments around it


Patch:
diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py
index 96a6657..d5378f9 100644
--- a/moviepy/video/VideoClip.py
+++ b/moviepy/video/VideoClip.py
@@ -386,6 +386,9 @@ class VideoClip(Clip):
                 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(
             self,
@@ -396,6 +399,7 @@ class VideoClip(Clip):
             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 8ffdb60..d2fc8f1 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. If None, ``"copy"`` codec is used.
+
     preset : str, optional
       Sets the time that FFMPEG will take to compress the video. The slower,
       the better the compression rate. Possibilities are: ``"ultrafast"``,
@@ -81,6 +84,7 @@ class FFMPEG_VideoWriter:
         fps,
         codec="libx264",
         audiofile=None,
+        audio_codec=None,
         preset="medium",
         bitrate=None,
         with_mask=False,
@@ -94,6 +98,7 @@ class FFMPEG_VideoWriter:
         self.logfile = logfile
         self.filename = filename
         self.codec = codec
+        self.audio_codec = audio_codec
         self.ext = self.filename.split(".")[-1]
 
         pixel_format = "rgba" if with_mask else "rgb24"
@@ -119,7 +124,9 @@ class FFMPEG_VideoWriter:
             "-",
         ]
         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])
 
         if codec == "h264_nvenc":
             cmd.extend(["-c:v", codec])
@@ -175,13 +182,14 @@ class FFMPEG_VideoWriter:
                 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}. "
+                    "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:
@@ -240,6 +248,7 @@ def ffmpeg_write_video(
     preset="medium",
     write_logfile=False,
     audiofile=None,
+    audio_codec=None,
     threads=None,
     ffmpeg_params=None,
     logger="bar",
@@ -269,6 +278,7 @@ def ffmpeg_write_video(
         with_mask=has_mask,
         logfile=logfile,
         audiofile=audiofile,
+        audio_codec=audio_codec,
         threads=threads,
         ffmpeg_params=ffmpeg_params,
         pixel_format=pixel_format,


Test patch:
diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py
index 6cd61bb..581a357 100644
--- a/tests/test_VideoClip.py
+++ b/tests/test_VideoClip.py
@@ -66,7 +66,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"
@@ -84,7 +84,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"
@@ -106,6 +106,24 @@ 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",