Zulko / moviepy

Video editing with Python
https://zulko.github.io/moviepy/
MIT License
12.07k stars 1.51k forks source link

Unexpecte/confusing output when `subclip()` tries to clip beyond end of file #2116

Open abingham opened 4 months ago

abingham commented 4 months ago

This little program uses subclip() to take more than the available data in an input file, and then write the result to file:

from moviepy.video.io.VideoFileClip import VideoFileClip

clip = VideoFileClip("movie.mov")
clip = clip.subclip(0, clip.duration + 1)
clip.write_videofile("movie2.mp4")

In the file writing phase I end up with an exception:

<elided>
File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/io/readers.py", line 170, in get_frame
    raise IOError("Error in file %s, "%(self.filename)+
OSError: Error in file output/good-name-movie-0/good-name.mov, Accessing time t=3.43-3.48 seconds, with clip duration=3 seconds,

Expected Behavior

The ultimate problem with the program is that I'm trying to subclip more data than is available. So I would expect for subclip() to complain when I ask for the impossible.

If it's actually the case that out-of-bounds subclip() calls are desirable, then I'd expect a more informative exception when the file writing eventually fails. That is, I'd like an error reporting the actual mistake I made (subclipping too much data) rather than some implementation detail (the OSError above).

Actual Behavior

As listed above, I get a relatively obscure and not terribly helpful exception when I do this. Here's the stack trace (trimmed to my call into moviepy code):

  File "/Users/austin/repos/sixty-north/demonstrable/deca/source/deca/ext/build_steps/timeline/step.py", line 164, in _build
    composite.write_videofile(
  File "<decorator-gen-73>", line 2, in write_videofile
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 54, in requires_duration
    return f(clip, *a, **k)
           ^^^^^^^^^^^^^^^^
  File "<decorator-gen-72>", line 2, in write_videofile
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 135, in use_clip_fps_by_default
    return f(clip, *new_a, **new_kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-71>", line 2, in write_videofile
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 22, in convert_masks_to_RGB
    return f(clip, *a, **k)
           ^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/video/VideoClip.py", line 293, in write_videofile
    self.audio.write_audiofile(audiofile, audio_fps,
  File "<decorator-gen-63>", line 2, in write_audiofile
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 54, in requires_duration
    return f(clip, *a, **k)
           ^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/AudioClip.py", line 206, in write_audiofile
    return ffmpeg_audiowrite(self, filename, fps, nbytes, buffersize,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-27>", line 2, in ffmpeg_audiowrite
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 54, in requires_duration
    return f(clip, *a, **k)
           ^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/io/ffmpeg_audiowriter.py", line 166, in ffmpeg_audiowrite
    for chunk in clip.iter_chunks(chunksize=buffersize,
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/AudioClip.py", line 85, in iter_chunks
    yield self.to_soundarray(tt, nbytes=nbytes, quantize=quantize,
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-62>", line 2, in to_soundarray
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 54, in requires_duration
    return f(clip, *a, **k)
           ^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/AudioClip.py", line 127, in to_soundarray
    snd_array = self.get_frame(tt)
                ^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-29>", line 2, in get_frame
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 89, in wrapper
    return f(*new_a, **new_kw)
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/Clip.py", line 93, in get_frame
    return self.make_frame(t)
           ^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/AudioClip.py", line 296, in make_frame
    sounds = [c.get_frame(t - c.start)*np.array([part]).T
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/AudioClip.py", line 296, in <listcomp>
    sounds = [c.get_frame(t - c.start)*np.array([part]).T
              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "<decorator-gen-29>", line 2, in get_frame
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 89, in wrapper
    return f(*new_a, **new_kw)
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/Clip.py", line 93, in get_frame
    return self.make_frame(t)
           ^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/Clip.py", line 136, in <lambda>
    newclip = self.set_make_frame(lambda t: fun(self.get_frame, t))
                                            ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/Clip.py", line 187, in <lambda>
    return self.fl(lambda gf, t: gf(t_func(t)), apply_to,
                                 ^^^^^^^^^^^^^
  File "<decorator-gen-29>", line 2, in get_frame
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/decorators.py", line 89, in wrapper
    return f(*new_a, **new_kw)
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/Clip.py", line 93, in get_frame
    return self.make_frame(t)
           ^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/io/AudioFileClip.py", line 77, in <lambda>
    self.make_frame = lambda t: self.reader.get_frame(t)
                                ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/austin/.virtualenvs/deca-dev/lib/python3.11/site-packages/moviepy/audio/io/readers.py", line 170, in get_frame
    raise IOError("Error in file %s, "%(self.filename)+
OSError: Error in file output/good-name-movie-0/good-name.mov, Accessing time t=3.43-3.48 seconds, with clip duration=3 seconds, 

Steps to Reproduce the Problem

Run the little program at the top, mutatis mutandis the filenames.

Specifications

bzczb commented 4 months ago

Idea to fix problems like this:

ffmpeg_reader should be very forgiving for out of bounds reads.

The enforcement of duration should be in subclip(). subclip() could take a parameter elasticity with the following values:

The default should be truncatewarn

abingham commented 4 months ago

Something along these lines would work well for my purposes.

Just one minor issue: in the truncate case, the new_clip.duration == min(original_clip.duration, end_time - start_time) comparison doesn't seem correct. If my [start_time, end_time] range overlaps the end of the data, then I'd expect new_clip.duration to be original_clip.duration - start_time which may be shorter than either of the arguments to min().

bzczb commented 4 months ago

Something along these lines would work well for my purposes.

Just one minor issue: in the truncate case, the new_clip.duration == min(original_clip.duration, end_time - start_time) comparison doesn't seem correct. If my [start_time, end_time] range overlaps the end of the data, then I'd expect new_clip.duration to be original_clip.duration - start_time which may be shorter than either of the arguments to min().

Thanks, my formulation was wrong. corrected