Skip to content

Commit

Permalink
Merge pull request #2353 from OsaAjani/pr-2117
Browse files Browse the repository at this point in the history
Fix incoherent frame seek as describe in issue #2115 and pr #2117
  • Loading branch information
OsaAjani authored Jan 30, 2025
2 parents 06b5329 + f2d3174 commit 41ed88e
Show file tree
Hide file tree
Showing 4 changed files with 32 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix textclip being cut or of impredictable height (see issues #2325, #2260 and #2268)
- Fix TimeMirror and TimeSymmetrize cutting last second of clip
- ImageSequenceClip was wrong when calculating fps with duration and no fps (see issue #2351)
- More consistent frame seek (see issue #2115 and PR #2117)
- Fix audiopreview not working with ffplay >= 7.0.0

## [v2.1.2](https://github.com/zulko/moviepy/tree/master)
Expand Down
Binary file added media/smpte-2997.mp4
Binary file not shown.
26 changes: 21 additions & 5 deletions moviepy/video/io/ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ def initialize(self, start_time=0):
"""
self.close(delete_lastread=False) # if any

# self.pos represents the (0-indexed) index of the frame that is next in line
# to be read by self.read_frame().
# Eg when self.pos is 1, the 2nd frame will be read next.
self.pos = self.get_frame_number(start_time)

# Getting around a difference between ffmpeg and moviepy seeking:
# "moviepy seek" means "get the frame displayed at time t"
# Hence given a 29.97 FPS video, seeking to .01s means "get frame 0".
# "ffmpeg seek" means "skip all frames until you reach time t".
# This time, seeking to .01s means "get frame 1". Surprise!
#
# (In 30fps, timestamps like 2.0s, 3.5s will give the same frame output
# under both rules, for the timestamp can be represented exactly in
# decimal.)
#
# So we'll subtract an epsilon from the timestamp given to ffmpeg.
if self.pos != 0:
start_time = self.pos * (1 / self.fps) - 0.00001
else:
start_time = 0.0

if start_time != 0:
offset = min(1, start_time)
i_arg = [
Expand Down Expand Up @@ -143,11 +164,6 @@ def initialize(self, start_time=0):
}
)
self.proc = sp.Popen(cmd, **popen_params)

# self.pos represents the (0-indexed) index of the frame that is next in line
# to be read by self.read_frame().
# Eg when self.pos is 1, the 2nd frame will be read next.
self.pos = self.get_frame_number(start_time)
self.last_read = self.read_frame()

def skip_frames(self, n=1):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_ffmpeg_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,5 +790,15 @@ def test_read_transparent_video():
assert mask[100, 100] == 255


def test_frame_seek():
reader = FFMPEG_VideoReader("media/smpte-2997.mp4", pixel_format="rgba")

# Get first frame and second frame
frame = reader.get_frame(0)
frame2 = reader.get_frame(0.34)

assert not np.array_equal(frame, frame2)


if __name__ == "__main__":
pytest.main()

0 comments on commit 41ed88e

Please sign in to comment.