[pitivi] timeline: Fix thumbnails position quantization



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]