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
Timeline: Add clip/effect lookup api, GetMaxFrame/GetMaxTime method (w/ unit tests) #563
Conversation
- Replacement method names are SetParentClip/GetParentClip - Old names are retained as deprecated alternates, for now - libopenshot internal calls (very few) are updated ReaderBase.cpp: Remove (Set,Get)Clip
Add GetMaxTime tests
Codecov Report
@@ Coverage Diff @@
## develop #563 +/- ##
===========================================
+ Coverage 48.69% 49.36% +0.66%
===========================================
Files 129 129
Lines 10036 10192 +156
===========================================
+ Hits 4887 5031 +144
- Misses 5149 5161 +12
Continue to review full report at Codecov.
|
ResultsThe savings from The impact of t = openshot.Timeline(
1280, 720, openshot.Fraction(30, 1), 48000, 2, openshot.LAYOUT_STEREO)
num_clips = 20
def lookup_locally(t, id):
clips = t.Clips()
for c in clips:
if c.Id() == id:
return c
def request_by_id(t, id):
c = t.GetClip(id)
return c
def maxframe_locally(t):
clips = t.Clips()
timeline_length = 0.0
for clip in clips:
clip_last_frame = clip.Position() + clip.Duration()
if clip_last_frame > timeline_length:
# Set max length of timeline
timeline_length = clip_last_frame
# Convert to int and round
timeline_length_int = round(timeline_length * 30) + 1
return timeline_length_int
def request_maxframe(t):
n = t.GetMaxFrame()
return n
def updatePositions(t):
upd_clips = t.Clips()
[t.RemoveClip(c) for c in upd_clips]
for i, c in enumerate(upd_clips):
p = c.Position()
c.Position((p * 13333 * i) % 1599)
shuffle(upd_clips)
[t.AddClip(c) for c in upd_clips]
def main():
test_clips = [openshot.Clip(TEST_MEDIA_FILE) for i in range(num_clips)]
layers = [l for l in range(50, 50+num_clips)]
starts = [s for s in range(num_clips)]
positions = [p for p in range(20, 20+num_clips*2, 2)]
shuffle(layers)
shuffle(starts)
shuffle(positions)
for i, c in enumerate(test_clips):
id = f"CLIP_NUMBER_{i}"
c.Id(id)
c.Layer(layers[i])
c.Position(positions[i])
c.Start(starts[i])
shuffle(test_clips)
[t.AddClip(c) for c in test_clips]
t.Open() So, it creates a somewhat-randomized timeline populated with 20 clips that all have shuffled Layer, Position, and Start values. I ran best-of-5 trials with the
Like I said, the GetMaxFrame() results are almost too tiny to measure. The test also isn't entirely valid, as both versions run # Locally (the Clips()-and-search version)
x = [lookup_locally(t, f"CLIP_NUMBER_{i}") for i in range(20)];
[x_c.Id() for x_c in x]
# Remotely (using t.GetClip(id))
y = [request_by_id(t, f"CLIP_NUMBER_{i}") for i in range(20)];
[y_c.Id() for y_c in y] Now, obviously that's a bit contrived since it's unfair to the local version. If it actually needed all 20 But in all the places we are grabbing just a single Benchmark code: Bench_GetClip.py.txt |
Heh. You can see the exact point in the unit tests where I just got tired of writing code comments, apparently. |
@ferdnyc Nice work! I love it. I feel like these methods should have been written a long time ago, lol. This might create some merge issues for me (I'm refactoring the Timeline and Clip classes), but no big deal. Let's get this merged! Thanks! |
I was a bit surprised that there were no API calls to do these things. Especially since it causes OpenShot itself to jump through a lot of hoops doing them for itself.
Great, thanks! I just applied all of my self-review suggestions, as soon as Travis finishes I'll send it through. (Then I have to start on the Python-side changes to take advantage of these new calls.) |
Oh, fun, seems Travis is a little backed up right now. Took almost 10 minutes for the first job to even start. (Hmm... although, maybe it was still finishing up our previous run, since I put through two quick edits in separate commits. Gotta remember to use that batch button.) Eh. Regardless, it'll get there eventually I'm sure. Just a reminder that I shouldn't be hanging around waiting on Travis anyway. |
Also, I temp disabled Traivs to work on some of the older PRs... (i.e. made it not required) |
This PR adds two things to Timeline, primarily intended for use by OpenShot:
Id
string lookupGetMaxTime()
that computes the time the last clip/effect ends and returns it as adouble
whole-and-partial-seconds value, and aGetMaxFrame()
that returns the same thing accounting for frame rate as anint64_t
frame number.Full unit tests are included for
Timeline::GetMaxFrame()/Timeline::GetMaxTime()
,Timeline::GetClip()
,Timeline::GetTimelineEffect()
, andTimeline::GetClipEffect()
. TheClip::GetEffect()
implementation isn't unit-tested directly, but it is used byTimeline::GetClipEffect()
so it's tested indirectly.Along the way, I also deprecated
ReaderBase::GetClip
andReaderBase::SetClip
, because I wanted to reuse one of the names and anyway those names are awful and confusing. They're now calledGetParentClip
andSetParentClip
. The few calls in the libopenshot source are updated. The old names are still available as deprecated aliases, for now.Motivation
I noticed that there are a lot of places OpenShot does stuff like this, from
video_widget.py
:Which just seemed inefficient. Sometimes OpenShot needs all of the clips, but when it only needs one, why should it search through them all when the library can do it much faster and just return what's needed? So, now, that can be:
(
GetClip(id)
returnsnullptr
if it can't find a match, which in Python automatically translates toNone
. So there's no need to pre-set it toNone
or treat the result as fragile.)And then, of course, there are the max-frame computations that get performed every time the preview is going to start playing, or needs to seek to the end of the timeline, etc. From
actionJumpEnd_trigger
inmain_window.py
:Now, all of that's just:
If the max time is needed (I'm sure somewhere we do that, too), it's just