[pitivi] timeline: Fix thumbnails position quantization
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] timeline: Fix thumbnails position quantization
- Date: Mon, 4 Oct 2021 21:23:37 +0000 (UTC)
commit 2fa1bbceb938c52843d756a03ded439e3fa7ca0b
Author: Alexandru Băluț <alexandru balut gmail com>
Date: Mon Oct 4 23:03:30 2021 +0200
timeline: Fix thumbnails position quantization
Fixes #2567
pitivi/timeline/previewers.py | 28 +++++---
tests/common.py | 2 +-
tests/test_previewers.py | 159 ++++++++++++++++++++++++++++++++----------
3 files changed, 144 insertions(+), 45 deletions(-)
---
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 82fbe213f..0433407cb 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -616,6 +616,7 @@ class AssetPreviewer(Previewer, Loggable):
# bringing the pipeline back to PAUSED.
self.pipeline.set_state(Gst.State.PAUSED)
return
+
pipeline = Gst.parse_launch(
"uridecodebin uri={uri} name=decode ! "
"videoconvert ! "
@@ -684,7 +685,10 @@ class AssetPreviewer(Previewer, Loggable):
if not self.thumb_width:
self.debug("Finding thumb width")
+ # The pipeline will call `_update_thumbnails` after it sets
+ # the missing `thumb_width`.
self._setup_pipeline()
+ # There is nothing else we can do now without `thummb_width`.
return False
# Update the thumbnails with what we already have, if anything.
@@ -790,7 +794,7 @@ class AssetPreviewer(Previewer, Loggable):
self.debug("Waiting for UI to become idle for: %s",
path_from_uri(self.uri))
self.__start_id = GLib.idle_add(self._start_thumbnailing_cb,
- priority=GLib.PRIORITY_LOW)
+ priority=GLib.PRIORITY_DEFAULT_IDLE + 50)
def stop_generation(self):
if self.__start_id:
@@ -839,7 +843,7 @@ class VideoPreviewer(Gtk.Layout, AssetPreviewer, Zoomable):
self.get_style_context().add_class("VideoPreviewer")
- self.ges_elem = ges_elem
+ self.ges_elem: GES.VideoUriSource = ges_elem
self.thumbs = {}
# Connect signals and fire things up
@@ -865,21 +869,22 @@ class VideoPreviewer(Gtk.Layout, AssetPreviewer, Zoomable):
def _update_thumbnails(self):
"""Updates the thumbnail widgets for the clip at the current zoom."""
+ # The thumb_width is available after the pipeline has been started.
if not self.thumb_width or not self.ges_elem.get_track() or not self.ges_elem.props.active:
- # The thumb_width will be available when pipeline has been started
return
thumbs = {}
queue = []
- interval = self.thumb_interval(self.thumb_width)
- y = (self.props.height_request - self.thumb_height) / 2
+ interval = self.thumb_interval(self.thumb_width)
+ y = (self.props.height_request - self.thumb_height) // 2
clip = self.ges_elem.get_parent()
- for position in range(clip.props.start, clip.props.start + clip.props.duration, interval):
- x = Zoomable.ns_to_pixel(position)
+ for element_position in range(0, self.ges_elem.props.duration, interval):
+ x = Zoomable.ns_to_pixel(element_position)
# Convert position in the timeline to the internal position in the source element
- position = clip.get_internal_time_from_timeline_time(self.ges_elem, position)
+ internal_position = clip.get_internal_time_from_timeline_time(self.ges_elem,
self.ges_elem.props.start + element_position)
+ position = quantize(internal_position, interval)
try:
thumb = self.thumbs.pop(position)
self.move(thumb, x, y)
@@ -895,9 +900,11 @@ class VideoPreviewer(Gtk.Layout, AssetPreviewer, Zoomable):
else:
if position not in self.failures and position != self.position:
queue.append(position)
+
for thumb in self.thumbs.values():
self.remove(thumb)
self.thumbs = thumbs
+
self.queue = queue
if queue:
self.become_controlled()
@@ -922,7 +929,10 @@ class VideoPreviewer(Gtk.Layout, AssetPreviewer, Zoomable):
def _inpoint_changed_cb(self, unused_ges_timeline_element, unused_param_spec):
"""Handles the changing of the in-point of the clip."""
- self._update_thumbnails()
+ # Whenever the inpoint changes, the duration also changes, as we never
+ # "roll". We rely on the handler for the duration change event to update
+ # the thumbnails.
+ self.debug("Inpoint change ignored, expecting following duration change")
def _duration_changed_cb(self, unused_ges_timeline_element, unused_param_spec):
"""Handles the changing of the duration of the clip."""
diff --git a/tests/common.py b/tests/common.py
index d074fc703..3af01ae5e 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -156,7 +156,7 @@ def create_main_loop():
source.set_callback(timeout_cb)
source.attach()
if until_empty:
- GLib.idle_add(mainloop.quit)
+ GLib.idle_add(mainloop.quit, priority=GLib.PRIORITY_LOW + 1)
GLib.MainLoop.run(mainloop)
source.destroy()
if timed_out:
diff --git a/tests/test_previewers.py b/tests/test_previewers.py
index bdf320ade..66d16c7a3 100644
--- a/tests/test_previewers.py
+++ b/tests/test_previewers.py
@@ -16,6 +16,7 @@
# License along with this program; if not, see <http://www.gnu.org/licenses/>.
"""Tests for the timeline.previewers module."""
# pylint: disable=protected-access,unused-argument
+import functools
import os
import tempfile
from unittest import mock
@@ -72,7 +73,127 @@ SIMPSON_WAVFORM_VALUES = [
3.6256085412966614, 0.0]
-class TestAudioPreviewer(BaseTestMediaLibrary):
+class TestPreviewers(BaseTestMediaLibrary):
+
+ def setup_trimmed_clips(self, frame_count):
+ # Set the project videorate to match tears_of_steel.webm.
+ self.project.videorate = Gst.Fraction(24, 1)
+ Zoomable.set_zoom_level(78)
+
+ timeline = self.timeline_container.timeline
+
+ start_frame = 43
+ start = self.timeline.get_frame_time(start_frame)
+ self.assertEqual(start, 1791666667)
+
+ clips = []
+ starts = []
+ asset = GES.UriClipAsset.request_sync(common.get_sample_uri("tears_of_steel.webm"))
+ for delta_frame in range(frame_count):
+ ges_layer = timeline.ges_timeline.append_layer()
+ ges_clip = timeline.add_clip_to_layer(ges_layer, asset, start)
+
+ editing_context = EditingContext(ges_clip, self.timeline, GES.EditMode.EDIT_TRIM,
GES.Edge.EDGE_START, self.app)
+ new_start = self.timeline.get_frame_time(start_frame + delta_frame)
+ editing_context.edit_to(new_start, ges_layer)
+ editing_context.finish()
+
+ clips.append(ges_clip)
+ starts.append(new_start)
+
+ expected_starts = [1791666667, 1833333334, 1875000000, 1916666667, 1958333334, 2000000000,
2041666667, 2083333334, 2125000000, 2166666667,
+ 2208333334, 2250000000, 2291666667, 2333333334, 2375000000, 2416666667,
2458333334, 2500000000, 2541666667, 2583333334,
+ 2625000000, 2666666667, 2708333334, 2750000000, 2791666667, 2833333334,
2875000000, 2916666667, 2958333334, 3000000000,
+ 3041666667, 3083333334, 3125000000, 3166666667, 3208333334, 3250000000,
3291666667, 3333333334, 3375000000, 3416666667,
+ 3458333334, 3500000000, 3541666667, 3583333334, 3625000000, 3666666667,
3708333334, 3750000000][:frame_count]
+
+ # Check we calculated correctly above where the clips should be placed.
+ self.assertListEqual(starts, expected_starts)
+
+ # Check the clips ended up exactly where we placed them.
+ self.assertListEqual([c.start for c in clips], expected_starts,
+ "the clips have been placed in unexpected positions")
+
+ expected_inpoints = [0, 41666667, 83333333, 125000000, 166666667, 208333333, 250000000, 291666667,
333333333, 375000000,
+ 416666667, 458333333, 500000000, 541666667, 583333333, 625000000, 666666667,
708333333, 750000000, 791666667,
+ 833333333, 875000000, 916666667, 958333333, 1000000000, 1041666667, 1083333333,
1125000000, 1166666667, 1208333333,
+ 1250000000, 1291666667, 1333333333, 1375000000, 1416666667, 1458333333,
1500000000, 1541666667, 1583333333, 1625000000,
+ 1666666667, 1708333333, 1750000000, 1791666667, 1833333333, 1875000000,
1916666667, 1958333333][:frame_count]
+ self.assertListEqual([c.inpoint for c in clips], expected_inpoints)
+
+ return clips
+
+
+class TestVideoPreviewer(TestPreviewers):
+ """Tests for the `VideoPreviewer` class."""
+
+ @common.setup_timeline
+ def test_thumbnails_position_quantization(self):
+ clips = self.setup_trimmed_clips(48)
+
+ # Check the video previewers.
+ video_previewers = list(self.get_clip_element(c, GES.VideoSource).ui.previewer
+ for c in clips)
+
+ mainloop = common.create_main_loop()
+
+ # Acrobatics to run the mainloop until the pipelines of the previewers
+ # are ready.
+ updated_previewers = set()
+
+ def update_thumbnails_func(previewer, original):
+ nonlocal updated_previewers
+ updated_previewers.add(previewer)
+
+ # Restore the original method.
+ previewer._update_thumbnails = original
+
+ if set(updated_previewers) == set(video_previewers):
+ mainloop.quit()
+
+ for previewer in video_previewers:
+ original = previewer._update_thumbnails
+ previewer._update_thumbnails = functools.partial(update_thumbnails_func, previewer, original)
+
+ mainloop.run()
+
+ expected_queues = (
+ [[0, 500000000, 1000000000, 1500000000]] * 12 +
+ [[500000000, 1000000000, 1500000000]] * 12 +
+ [[1000000000, 1500000000]] * 12 +
+ [[1500000000]] * 12
+ )
+
+ expected_x_positions = (
+ [[0, 237, 474, 712]] * 12 +
+ [[0, 237, 474]] * 12 +
+ [[0, 237]] * 12 +
+ [[0]] * 12
+ )
+
+ for previewer, expected_queue, expected_xs in zip(video_previewers, expected_queues,
expected_x_positions):
+ self.assertEqual(previewer.thumb_width, 151)
+
+ xs = []
+
+ def move_put_func(thumb, x, y):
+ # pylint: disable=cell-var-from-loop
+ xs.append(x)
+
+ previewer.move = move_put_func
+ previewer.put = move_put_func
+ try:
+ previewer.refresh()
+ finally:
+ # These should not be called anymore.
+ previewer.move = mock.Mock()
+ previewer.put = mock.Mock()
+
+ self.assertListEqual(previewer.queue, expected_queue)
+ self.assertListEqual(xs, expected_xs)
+
+
+class TestAudioPreviewer(TestPreviewers):
"""Tests for the `AudioPreviewer` class."""
def test_create_thumbnail_bin(self):
@@ -100,40 +221,8 @@ class TestAudioPreviewer(BaseTestMediaLibrary):
self.assertEqual(samples, SIMPSON_WAVFORM_VALUES)
@common.setup_timeline
- def test_offset(self):
- # Set the videorate of tears_of_steel.webm.
- self.project.videorate = Gst.Fraction(24, 1)
- Zoomable.set_zoom_level(78)
-
- timeline = self.timeline_container.timeline
-
- start_frame = 43
- start = self.timeline.get_frame_time(start_frame)
- self.assertEqual(start, 1791666667)
-
- clips = []
- starts = []
- asset = GES.UriClipAsset.request_sync(common.get_sample_uri("tears_of_steel.webm"))
- for delta_frame in range(10):
- ges_layer = timeline.ges_timeline.append_layer()
- ges_clip = timeline.add_clip_to_layer(ges_layer, asset, start)
-
- editing_context = EditingContext(ges_clip, self.timeline, GES.EditMode.EDIT_TRIM,
GES.Edge.EDGE_START, self.app)
- new_start = self.timeline.get_frame_time(start_frame + delta_frame)
- editing_context.edit_to(new_start, ges_layer)
- editing_context.finish()
-
- clips.append(ges_clip)
- starts.append(new_start)
-
- # Check the clips.
- expected_starts = [1791666667, 1833333334, 1875000000, 1916666667, 1958333334, 2000000000,
2041666667, 2083333334, 2125000000, 2166666667]
- self.assertListEqual(starts, expected_starts,
- "the start values calculated in the test are off")
- self.assertListEqual([c.start for c in clips], expected_starts,
- "the start of the clips are wrong")
- expected_inpoints = [0, 41666667, 83333333, 125000000, 166666667, 208333333, 250000000, 291666667,
333333333, 375000000]
- self.assertListEqual([c.inpoint for c in clips], expected_inpoints)
+ def test_waveform_offset(self):
+ clips = self.setup_trimmed_clips(10)
# Check the audio previewers.
audio_previewers = list(self.get_clip_element(c, GES.AudioSource).ui.previewer
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]