Repository: Zulko/moviepy

Issue:
Ability to set fps of VideoFileClip
FFMPEG supports a "video file name" of `image-%04d.png`, and moviepy accepts this in `VideoFileClip()` which is great. But unfortunately it assumes 25 fps, and I don't see any way to change that. I can call `clip=clip.set_fps(24)` but that doesn't change the duration - I think it just skips frames.

It would be great if `VideoFileClip` could accept an optional `fps` arg for this case.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/33912772/badge)](https://coveralls.io/builds/33912772)

Coverage decreased (-0.01%) to 67.466% when pulling **12cfa053145de661e333fece64ea30d1972ddd05 on mxbi:fps-changeduration** into **3a6f0148a2c064899451c5d883a89db80634435e on Zulko:master**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fa5c43..95cc727 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added <!-- for new features -->
 - New `pix_fmt` parameter in `VideoFileClip`, `VideoClip.write_videofile()`, `VideoClip.write_gif()` that allows passing a custom `pix_fmt` parameter such as `"bgr24"` to FFmpeg [#1237]
+- New `change_duration` parameter in `Clip.set_fps()` that allows changing the video speed to match the new fps [#1329]
 
 ### Changed <!-- for changes in existing functionality -->
 - `ffmpeg_parse_infos()` and `VideoFileClip` now have optional `decode_file` parameter that ensures that the detected duration is correct, but may take a long time to run [#1063, #1222]
diff --git a/moviepy/Clip.py b/moviepy/Clip.py
index c135dff..c1318ef 100644
--- a/moviepy/Clip.py
+++ b/moviepy/Clip.py
@@ -298,11 +298,22 @@ class Clip:
         """
         self.make_frame = make_frame
 
-    @outplace
-    def set_fps(self, fps):
+    def set_fps(self, fps, change_duration=False):
         """Returns a copy of the clip with a new default fps for functions like
-        write_videofile, iterframe, etc."""
-        self.fps = fps
+        write_videofile, iterframe, etc.
+        If ``change_duration=True``, then the video speed will change to match the
+        new fps (conserving all frames 1:1). For example, if the fps is
+        halved in this mode, the duration will be doubled."""
+
+        if change_duration:
+            from moviepy.video.fx.speedx import speedx
+
+            newclip = speedx(self, fps / self.fps)
+        else:
+            newclip = self.copy()
+
+        newclip.fps = fps
+        return newclip
 
     @outplace
     def set_ismask(self, ismask):


Test patch:
diff --git a/tests/test_VideoClip.py b/tests/test_VideoClip.py
index a1ba719..ae7a5da 100644
--- a/tests/test_VideoClip.py
+++ b/tests/test_VideoClip.py
@@ -261,5 +261,31 @@ def test_withoutaudio():
     close_all_clips(locals())
 
 
+def test_setfps_withoutchangeduration():
+    clip = VideoFileClip("media/big_buck_bunny_432_433.webm").subclip(0, 1)
+    # The sum is unique for each frame, so we can use it as a frame-ID
+    # to check which frames are being preserved
+    clip_sums = [f.sum() for f in clip.iter_frames()]
+
+    clip2 = clip.set_fps(48)
+    clip2_sums = [f.sum() for f in clip2.iter_frames()]
+    assert clip2_sums[::2] == clip_sums
+    assert clip2.duration == clip.duration
+    close_all_clips(locals())
+
+
+def test_setfps_withchangeduration():
+    clip = VideoFileClip("media/big_buck_bunny_432_433.webm").subclip(0, 1)
+    # The sum is unique for each frame, so we can use it as a frame-ID
+    # to check which frames are being preserved
+    clip_sums = [f.sum() for f in clip.iter_frames()]
+
+    clip2 = clip.set_fps(48, change_duration=True)
+    clip2_sums = [f.sum() for f in clip2.iter_frames()]
+    assert clip2_sums == clip_sums
+    assert clip2.duration == clip.duration / 2
+    close_all_clips(locals())
+
+
 if __name__ == "__main__":
     pytest.main()