Skip to content
Merged
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
26 changes: 24 additions & 2 deletions src/Qt/VideoCacheThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,32 @@ namespace openshot
}

int dir = computeDirection();

// Near timeline boundaries, don't require more pre-roll than can exist.
int64_t max_frame = reader->info.video_length;
if (auto* timeline = dynamic_cast<Timeline*>(reader)) {
const int64_t timeline_max = timeline->GetMaxFrame();
if (timeline_max > 0) {
max_frame = timeline_max;
}
}
if (max_frame < 1) {
return false;
}

int64_t required_ahead = min_frames_ahead;
if (required_ahead < 0) {
required_ahead = 0;
}
int64_t available_ahead = (dir > 0)
? std::max<int64_t>(0, max_frame - requested_display_frame)
: std::max<int64_t>(0, requested_display_frame - 1);
required_ahead = std::min(required_ahead, available_ahead);

if (dir > 0) {
return (last_cached_index >= requested_display_frame + min_frames_ahead);
return (last_cached_index >= requested_display_frame + required_ahead);
}
return (last_cached_index <= requested_display_frame - min_frames_ahead);
return (last_cached_index <= requested_display_frame - required_ahead);
}

void VideoCacheThread::setSpeed(int new_speed)
Expand Down
4 changes: 2 additions & 2 deletions src/Timeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ namespace openshot {
/// the Clip with the highest end-frame number using std::max_element
struct CompareClipEndFrames {
bool operator()(const openshot::Clip* lhs, const openshot::Clip* rhs) {
return (lhs->Position() + lhs->Duration()) <= (rhs->Position() + rhs->Duration());
return (lhs->Position() + lhs->Duration()) < (rhs->Position() + rhs->Duration());
}};

/// Like CompareClipEndFrames, but for effects
struct CompareEffectEndFrames {
bool operator()(const openshot::EffectBase* lhs, const openshot::EffectBase* rhs) {
return (lhs->Position() + lhs->Duration()) <= (rhs->Position() + rhs->Duration());
return (lhs->Position() + lhs->Duration()) < (rhs->Position() + rhs->Duration());
}};

/**
Expand Down
34 changes: 34 additions & 0 deletions tests/Timeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,40 @@ TEST_CASE( "GetMinFrame and GetMinTime", "[libopenshot][timeline]" )
CHECK(t.GetMinFrame() == (5 * 30) + 1);
}

TEST_CASE( "GetMaxFrame with 24fps clip mapped to 30fps timeline", "[libopenshot][timeline]" )
{
Timeline t(640, 480, Fraction(30, 1), 44100, 2, LAYOUT_STEREO);
t.AutoMapClips(true);

std::stringstream path;
path << TEST_MEDIA_PATH << "sintel_trailer-720p.mp4";
Clip clip(path.str());

REQUIRE(clip.Reader()->info.fps.num == 24);
REQUIRE(clip.Reader()->info.fps.den == 1);

t.AddClip(&clip);

REQUIRE(clip.Reader()->Name() == "FrameMapper");
auto* mapper = static_cast<FrameMapper*>(clip.Reader());
REQUIRE(mapper->info.fps.num == 30);
REQUIRE(mapper->info.fps.den == 1);
REQUIRE(mapper->info.video_length > 0);

const int64_t timeline_max_frame = t.GetMaxFrame();
const int64_t mapped_video_length = mapper->info.video_length;

// Timeline max frame is computed from duration (seconds), while mapper length is
// rounded frame count. They should stay aligned within one frame at this boundary.
CHECK(timeline_max_frame >= mapped_video_length);
CHECK((timeline_max_frame - mapped_video_length) <= 1);

// Regression guard: fetching the mapped tail frame should not throw.
t.Open();
CHECK_NOTHROW(t.GetFrame(mapped_video_length));
t.Close();
}

TEST_CASE( "Multi-threaded Timeline GetFrame", "[libopenshot][timeline]" )
{
Timeline *t = new Timeline(1280, 720, Fraction(24, 1), 48000, 2, LAYOUT_STEREO);
Expand Down
29 changes: 29 additions & 0 deletions tests/VideoCacheThread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,35 @@ TEST_CASE("isReady: requires cached frames ahead of playhead", "[VideoCacheThrea
CHECK(thread.isReady());
}

TEST_CASE("isReady: clamps preroll requirement at timeline boundaries", "[VideoCacheThread]") {
TestableVideoCacheThread thread;

Timeline timeline(/*width=*/1280, /*height=*/720, /*fps=*/Fraction(30,1),
/*sample_rate=*/48000, /*channels=*/2, ChannelLayout::LAYOUT_STEREO);
thread.Reader(&timeline);

const int64_t end = timeline.info.video_length;
REQUIRE(end > 10);

// Forward near end: only a few frames remain, so don't require full preroll.
thread.setMinFramesAhead(30);
thread.setSpeed(1);
thread.setPlayhead(end - 5);
thread.setLastCachedIndex(end - 4);
CHECK(!thread.isReady());
thread.setLastCachedIndex(end);
CHECK(thread.isReady());

// Backward near start: only a few frames exist behind playhead.
thread.setMinFramesAhead(30);
thread.setSpeed(-1);
thread.setPlayhead(3);
thread.setLastCachedIndex(2);
CHECK(!thread.isReady());
thread.setLastCachedIndex(1);
CHECK(thread.isReady());
}

TEST_CASE("clearCacheIfPaused: clears only when paused and not in cache", "[VideoCacheThread]") {
TestableVideoCacheThread thread;
CacheMemory cache(/*max_bytes=*/100000000);
Expand Down