Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inexact and inconsistent frame seek #2117

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions moviepy/Clip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Implements the central object of MoviePy, the Clip, and all the methods that
are common to the two subclasses of Clip, VideoClip and AudioClip.
"""

import copy as _copy
from functools import reduce
from numbers import Real
Expand Down
1 change: 1 addition & 0 deletions moviepy/audio/fx/all/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Use the fx method directly from the clip instance (e.g. ``clip.audio_normalize(...)``)
or import the function from moviepy.audio.fx instead.
"""

import warnings

from .. import * # noqa 401,F403
Expand Down
1 change: 1 addition & 0 deletions moviepy/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Decorators used by moviepy."""

import inspect
import os

Expand Down
1 change: 1 addition & 0 deletions moviepy/tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Misc. useful functions that can be used at many places in the program."""

import os
import subprocess as sp
import warnings
Expand Down
1 change: 1 addition & 0 deletions moviepy/video/fx/all/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
Use the fx method directly from the clip instance (e.g. ``clip.resize(...)``)
or import the function from moviepy.video.fx instead.
"""

import warnings

from moviepy.video.fx import * # noqa F401,F403
Expand Down
58 changes: 41 additions & 17 deletions moviepy/video/io/ffmpeg_reader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Implements all the functions to read a video or a picture using ffmpeg."""

import os
import re
import subprocess as sp
Expand Down Expand Up @@ -80,7 +81,28 @@ def initialize(self, start_time=0):
Sets self.pos to the appropriate value (1 if start_time == 0 because
it pre-reads the first frame).
"""
self.close(delete_lastread=False) # if any
self.close(delete_last_read=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)
Expand Down Expand Up @@ -123,12 +145,7 @@ 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.lastread = self.read_frame()
self.last_read = self.read_frame()

def skip_frames(self, n=1):
"""Reads and throws away n frames"""
Expand All @@ -143,7 +160,7 @@ def read_frame(self):
"""
Reads the next frame from the file.
Note that upon (re)initialization, the first frame will already have been read
and stored in ``self.lastread``.
and stored in ``self.last_read``.
"""
w, h = self.size
nbytes = self.depth * w * h
Expand Down Expand Up @@ -218,7 +235,7 @@ def get_frame(self, t):
elif (pos < self.pos) or (pos > self.pos + 100):
# We can't just skip forward to `pos` or it would take too long
self.initialize(t)
return self.lastread
return self.last_read
else:
# If pos == self.pos + 1, this line has no effect
self.skip_frames(pos - self.pos - 1)
Expand All @@ -233,7 +250,14 @@ def get_frame_number(self, t):
# are getting the nth frame by writing get_frame(n/fps).
return int(self.fps * t + 0.00001)

def close(self, delete_lastread=True):
@property
def lastread(self):
"""Support old name of the "last_read" attribute, which may be used
by existing libraries, such as scenedetect.
"""
return self.last_read

def close(self, delete_last_read=True):
"""Closes the reader terminating the process, if is still open."""
if self.proc:
if self.proc.poll() is None:
Expand All @@ -242,7 +266,7 @@ def close(self, delete_lastread=True):
self.proc.stderr.close()
self.proc.wait()
self.proc = None
if delete_lastread and hasattr(self, "last_read"):
if delete_last_read and hasattr(self, "last_read"):
del self.last_read

def __del__(self):
Expand Down Expand Up @@ -451,12 +475,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:
Expand Down
1 change: 1 addition & 0 deletions moviepy/video/tools/credits.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Contains different functions to make end and opening credits, even though it is
difficult to fill everyone needs in this matter.
"""

from moviepy.decorators import convert_path_to_string
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
from moviepy.video.fx.resize import resize
Expand Down