1# -*- coding: utf-8 -*-
2# Pitivi video editor
3# Copyright (c) 2009, Alessandro Decina <alessandro.d@gmail.com>
4# Copyright (c) 2013, Alex Băluț <alexandru.balut@gmail.com>
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this program; if not, write to the
18# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19# Boston, MA 02110-1301, USA.
20import collections
21import os
22import tempfile
23import time
24from unittest import mock
25
26from gi.repository import GES
27from gi.repository import Gst
28
29from pitivi import medialibrary
30from pitivi.project import Project
31from pitivi.project import ProjectManager
32from pitivi.utils.misc import path_from_uri
33from pitivi.utils.proxy import ProxyingStrategy
34from tests import common
35
36
37class ProjectManagerListener(object):
38
39    def __init__(self, manager):
40        self.manager = manager
41        self.connectToProjectManager(self.manager)
42        self._reset()
43
44    def _reset(self):
45        self.signals = []
46
47    def connectToProjectManager(self, manager):
48        for signal in ("new-project-loading", "new-project-loaded",
49                       "new-project-created", "new-project-failed", "missing-uri",
50                       "closing-project", "project-closed"):
51            self.manager.connect(signal, self._recordSignal, signal)
52
53    def _recordSignal(self, *args):
54        signal = args[-1]
55        args = args[1:-1]
56        self.signals.append((signal, args))
57
58        return True
59
60
61class TestProjectManager(common.TestCase):
62
63    def setUp(self):
64        super(TestProjectManager, self).setUp()
65        self.setupApp()
66
67    def setupApp(self, app=None):
68        if not app:
69            app = mock.MagicMock()
70        self.manager = ProjectManager(app)
71        self.listener = ProjectManagerListener(self.manager)
72        self.signals = self.listener.signals
73
74    def testLoadProjectFailedUnknownFormat(self):
75        """
76        Check that new-project-failed is emitted when we don't have a suitable
77        formatter.
78        """
79        uri = "file:///Untitled.meh"
80        self.manager.loadProject(uri)
81
82        # loading
83        name, args = self.signals[0]
84        self.assertEqual(uri, args[0].get_uri(), self.signals)
85
86        # failed
87        name, args = self.signals[1]
88        self.assertEqual("new-project-failed", name)
89        signalUri, unused_message = args
90        self.assertEqual(uri, signalUri, self.signals)
91
92    def testLoadProjectClosesCurrent(self):
93        """
94        Check that new-project-failed is emitted if we can't close the current
95        project instance.
96        """
97        state = {"tried-close": False}
98
99        def close():
100            state["tried-close"] = True
101            return False
102        self.manager.closeRunningProject = close
103
104        uri = "file:///Untitled.xptv"
105        self.manager.current_project = mock.Mock()
106        self.manager.loadProject(uri)
107
108        self.assertEqual(0, len(self.signals))
109        self.assertTrue(state["tried-close"], self.signals)
110
111    def testLoadProject(self):
112        self.manager.newBlankProject()
113
114        name, args = self.signals[0]
115        self.assertEqual("new-project-loading", name, self.signals)
116
117        name, args = self.signals[1]
118        self.assertEqual("new-project-created", name, self.signals)
119
120        name, args = self.signals[2]
121        self.assertEqual("new-project-loaded", name, self.signals)
122
123    def testMissingUriForwarded(self):
124        self.setupApp(app=common.create_pitivi_mock())
125        mainloop = common.create_main_loop()
126
127        def missingUriCb(self, project, error, clip_asset, result):
128            result[0] = True
129            mainloop.quit()
130
131        result = [False]
132        self.manager.connect("missing-uri", missingUriCb, result)
133
134        with common.cloned_sample():
135            asset_uri = common.get_sample_uri("missing.png")
136            with common.created_project_file(asset_uri) as uri:
137                self.assertTrue(self.manager.loadProject(uri))
138                mainloop.run()
139        self.assertTrue(result[0], "missing-uri has not been emitted")
140
141    def testLoaded(self):
142        mainloop = common.create_main_loop()
143
144        def new_project_loaded_cb(project_manager, project):
145            mainloop.quit()
146
147        self.manager.connect("new-project-loaded", new_project_loaded_cb)
148
149        with common.cloned_sample("flat_colour1_640x480.png"):
150            asset_uri = common.get_sample_uri("flat_colour1_640x480.png")
151            with common.created_project_file(asset_uri=asset_uri) as uri:
152                self.assertTrue(self.manager.loadProject(uri))
153                mainloop.run()
154
155        project = self.manager.current_project
156        self.assertFalse(project.at_least_one_asset_missing)
157        self.assertTrue(project.loaded)
158        self.assertFalse(project.hasUnsavedModifications())
159
160    def testCloseRunningProjectNoProject(self):
161        self.assertTrue(self.manager.closeRunningProject())
162        self.assertFalse(self.signals)
163
164    def testCloseRunningProjectRefuseFromSignal(self):
165        def closing(manager, project):
166            return False
167
168        self.manager.current_project = mock.Mock()
169        self.manager.current_project.uri = "file:///ciao"
170        self.manager.connect("closing-project", closing)
171
172        self.assertFalse(self.manager.closeRunningProject())
173        self.assertEqual(1, len(self.signals))
174        name, args = self.signals[0]
175        self.assertEqual("closing-project", name)
176        project = args[0]
177        self.assertTrue(project is self.manager.current_project)
178
179    def testCloseRunningProject(self):
180        current = mock.Mock()
181        current.uri = None
182        self.manager.current_project = current
183        self.assertTrue(self.manager.closeRunningProject())
184        self.assertEqual(2, len(self.signals))
185
186        name, args = self.signals[0]
187        self.assertEqual("closing-project", name)
188        project = args[0]
189        self.assertTrue(project is current)
190
191        name, args = self.signals[1]
192        self.assertEqual("project-closed", name)
193        project = args[0]
194        self.assertTrue(project is current)
195
196        self.assertTrue(self.manager.current_project is None)
197
198    def testNewBlankProjectCantCloseCurrent(self):
199        def closing(manager, project):
200            return False
201
202        self.manager.current_project = mock.Mock()
203        self.manager.current_project.uri = "file:///ciao"
204        self.manager.connect("closing-project", closing)
205        self.assertFalse(self.manager.newBlankProject())
206        self.assertEqual(1, len(self.signals))
207        signal, args = self.signals[0]
208        self.assertEqual("closing-project", signal)
209
210    def testNewBlankProject(self):
211        self.assertTrue(self.manager.newBlankProject())
212        self.assertEqual(3, len(self.signals))
213
214        name, args = self.signals[0]
215        self.assertEqual("new-project-loading", name)
216        project = args[0]
217        self.assertTrue(project.get_uri() is None)
218
219        name, args = self.signals[1]
220        self.assertEqual("new-project-created", name)
221        project = args[0]
222        self.assertEqual(project.get_uri(), project.uri)
223
224        name, args = self.signals[2]
225        self.assertEqual("new-project-loaded", name)
226        project = args[0]
227        self.assertTrue(project is self.manager.current_project)
228
229    def testSaveProject(self):
230        self.assertTrue(self.manager.newBlankProject())
231
232        unused, path = tempfile.mkstemp(suffix=".xges")
233        unused, path2 = tempfile.mkstemp(suffix=".xges")
234        try:
235            uri = "file://" + os.path.abspath(path)
236            uri2 = "file://" + os.path.abspath(path2)
237
238            # Save the project.
239            self.assertTrue(self.manager.saveProject(uri=uri, backup=False))
240            self.assertTrue(os.path.isfile(path))
241
242            # Wait a bit.
243            time.sleep(0.1)
244
245            # Save the project at a new location.
246            self.assertTrue(self.manager.saveProject(uri2, backup=False))
247            self.assertTrue(os.path.isfile(path2))
248
249            # Make sure the old path and the new path have different mtimes.
250            mtime = os.path.getmtime(path)
251            mtime2 = os.path.getmtime(path2)
252            self.assertLess(mtime, mtime2)
253
254            # Wait a bit more.
255            time.sleep(0.1)
256
257            # Save project again under the new path (by omitting uri arg)
258            self.assertTrue(self.manager.saveProject(backup=False))
259
260            # regression test for bug 594396
261            # make sure we didn't save to the old URI
262            self.assertEqual(mtime, os.path.getmtime(path))
263            # make sure we did save to the new URI
264            self.assertLess(mtime2, os.path.getmtime(path2))
265        finally:
266            os.remove(path)
267            os.remove(path2)
268
269    def testMakeBackupUri(self):
270        uri = "file:///tmp/x.xges"
271        self.assertEqual(uri + "~", self.manager._makeBackupURI(uri))
272
273    def testBackupProject(self):
274        self.manager.newBlankProject()
275
276        # Assign an uri to the project where it's saved by default.
277        unused, xges_path = tempfile.mkstemp(suffix=".xges")
278        uri = "file://" + os.path.abspath(xges_path)
279        self.manager.current_project.uri = uri
280        # This is where the automatic backup file is saved.
281        backup_uri = self.manager._makeBackupURI(uri)
282
283        # Save the backup
284        self.assertTrue(self.manager.saveProject(
285            self.manager.current_project, backup=True))
286        self.assertTrue(os.path.isfile(path_from_uri(backup_uri)))
287
288        self.manager.closeRunningProject()
289        self.assertFalse(os.path.isfile(path_from_uri(backup_uri)),
290                         "Backup file not deleted when project closed")
291
292
293class TestProjectLoading(common.TestCase):
294
295    def test_loaded_callback(self):
296        mainloop = common.create_main_loop()
297
298        def loaded(project, timeline):
299            # If not called, the timeout of the mainloop will fail the test.
300            mainloop.quit()
301
302        # Create a blank project and save it.
303        project = common.create_project()
304        project.connect("loaded", loaded)
305        mainloop.run()
306        # The blank project loading succeeded emitting signal "loaded".
307
308        self.assertIsNotNone(project.ges_timeline)
309        self.assertEqual(len(project.ges_timeline.get_layers()), 1)
310
311        # Load the blank project and make sure "loaded" is triggered.
312        unused, xges_path = tempfile.mkstemp()
313        uri = "file://%s" % xges_path
314        try:
315            # Save so we can close it without complaints.
316            project.save(project.ges_timeline, uri, None, overwrite=True)
317
318            project2 = common.create_project()
319            project2.connect("loaded", loaded)
320            mainloop.run()
321            # The blank project loading succeeded emitting signal "loaded".
322
323            self.assertIsNotNone(project2.ges_timeline)
324            self.assertEqual(len(project2.ges_timeline.get_layers()), 1)
325        finally:
326            os.remove(xges_path)
327
328    def test_asset_added_signal(self):
329        app = common.create_pitivi()
330        self.assertTrue(app.project_manager.newBlankProject())
331
332        project = app.project_manager.current_project
333        proxy_manager = app.proxy_manager
334
335        mainloop = common.create_main_loop()
336
337        def asset_added_cb(project, asset, assets):
338            assets.append(asset)
339
340        assets = []
341        project.connect("asset-added", asset_added_cb, assets)
342
343        def proxy_ready_cb(unused_proxy_manager, asset, proxy):
344            mainloop.quit()
345
346        proxy_manager.connect("proxy-ready", proxy_ready_cb)
347
348        uris = [common.get_sample_uri("tears_of_steel.webm")]
349        project.addUris(uris)
350
351        mainloop.run()
352
353        self.assertEqual(len(assets), 1, assets)
354
355    def create_project_file_from_xges(self, app, xges):
356        unused, xges_path = tempfile.mkstemp(suffix=".xges")
357        proj_uri = Gst.filename_to_uri(os.path.abspath(xges_path))
358        app.project_manager.saveProject(uri=proj_uri)
359
360        with open(xges_path, "w") as f:
361            f.write(xges)
362
363        return proj_uri
364
365    def load_project_with_missing_proxy(self):
366        """Loads a project with missing proxies."""
367        uris = [common.get_sample_uri("1sec_simpsons_trailer.mp4")]
368        proxy_uri = uris[0] + ".232417.proxy.mkv"
369        PROJECT_STR = """<ges version='0.3'>
370  <project properties='properties;' metadatas='metadatas, name=(string)&quot;New\ Project&quot;, author=(string)Unknown, render-scale=(double)100;'>
371    <encoding-profiles>
372    </encoding-profiles>
373    <ressources>
374      <asset id='%(uri)s' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)1228000000;' metadatas='metadatas, audio-codec=(string)&quot;MPEG-4\ AAC\ audio&quot;, maximum-bitrate=(uint)130625, bitrate=(uint)130625, datetime=(datetime)2007-02-19T05:03:04Z, encoder=(string)Lavf54.6.100, container-format=(string)&quot;ISO\ MP4/M4A&quot;, video-codec=(string)&quot;H.264\ /\ AVC&quot;, file-size=(guint64)232417;'  proxy-id='file:///home/thiblahute/devel/pitivi/flatpak/pitivi/tests/samples/1sec_simpsons_trailer.mp4.232417.proxy.mkv' />
375      <asset id='%(proxy_uri)s' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)1228020833;' metadatas='metadatas, container-format=(string)Matroska, audio-codec=(string)Opus, language-code=(string)en, encoder=(string)Lavf54.6.100, bitrate=(uint)64000, video-codec=(string)&quot;Motion\ JPEG&quot;, file-size=(guint64)4695434;' />
376    </ressources>
377    <timeline properties='properties, auto-transition=(boolean)true, snapping-distance=(guint64)0;' metadatas='metadatas, duration=(guint64)0;'>
378      <track caps='video/x-raw(ANY)' track-type='4' track-id='0' properties='properties, async-handling=(boolean)false, message-forward=(boolean)true, caps=(string)&quot;video/x-raw\(ANY\)&quot;, restriction-caps=(string)&quot;video/x-raw\,\ width\=\(int\)720\,\ height\=\(int\)576\,\ framerate\=\(fraction\)25/1&quot;, mixing=(boolean)true;' metadatas='metadatas;'/>
379      <track caps='audio/x-raw(ANY)' track-type='2' track-id='1' properties='properties, async-handling=(boolean)false, message-forward=(boolean)true, caps=(string)&quot;audio/x-raw\(ANY\)&quot;, restriction-caps=(string)&quot;audio/x-raw\,\ format\=\(string\)S32LE\,\ channels\=\(int\)2\,\ rate\=\(int\)44100\,\ layout\=\(string\)interleaved&quot;, mixing=(boolean)true;' metadatas='metadatas;'/>
380      <layer priority='0' properties='properties, auto-transition=(boolean)true;' metadatas='metadatas, volume=(float)1;'>
381        <clip id='0' asset-id='%(proxy_uri)s' type-name='GESUriClip' layer-priority='0' track-types='6' start='0' duration='1228000000' inpoint='0' rate='0' properties='properties, name=(string)uriclip0, mute=(boolean)false, is-image=(boolean)false;' >
382          <source track-id='1' children-properties='properties, GstVolume::mute=(boolean)false, GstVolume::volume=(double)1;'>
383            <binding type='direct' source_type='interpolation' property='volume' mode='1' track_id='1' values =' 0:0.10000000000000001  1228000000:0.10000000000000001 '/>
384          </source>
385          <source track-id='0' children-properties='properties, GstFramePositioner::alpha=(double)1, GstDeinterlace::fields=(int)0, GstFramePositioner::height=(int)720, GstDeinterlace::mode=(int)0, GstFramePositioner::posx=(int)0, GstFramePositioner::posy=(int)0, GstDeinterlace::tff=(int)0, GstFramePositioner::width=(int)1280;'>
386            <binding type='direct' source_type='interpolation' property='alpha' mode='1' track_id='0' values =' 0:1  1228000000:1 '/>
387          </source>
388        </clip>
389      </layer>
390      <groups>
391      </groups>
392    </timeline>
393</project>
394</ges>""" % {"uri": uris[0], "proxy_uri": proxy_uri}
395        app = common.create_pitivi(proxyingStrategy=ProxyingStrategy.ALL)
396        proxy_manager = app.proxy_manager
397        project_manager = app.project_manager
398        medialib = medialibrary.MediaLibraryWidget(app)
399
400        mainloop = common.create_main_loop()
401
402        proj_uri = self.create_project_file_from_xges(app, PROJECT_STR)
403
404        def closing_project_cb(*args, **kwargs):
405            # Do not ask whether to save project on closing.
406            return True
407
408        def proxy_ready_cb(proxy_manager, asset, proxy):
409            self.assertEqual(proxy.props.id, proxy_uri)
410            mainloop.quit()
411
412        project_manager.connect("closing-project", closing_project_cb)
413        proxy_manager.connect_after("proxy-ready", proxy_ready_cb)
414
415        app.project_manager.loadProject(proj_uri)
416        return mainloop, app, medialib, proxy_uri
417
418    def test_load_project_with_missing_proxy(self):
419        """Checks loading a project with missing proxies."""
420        with common.cloned_sample("1sec_simpsons_trailer.mp4"):
421            mainloop, app, medialib, proxy_uri = self.load_project_with_missing_proxy()
422            mainloop.run()
423
424        self.assertEqual(len(medialib.storemodel), 1)
425        self.assertEqual(medialib.storemodel[0][medialibrary.COL_ASSET].props.id,
426                         proxy_uri)
427        self.assertEqual(medialib.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
428                         medialibrary.AssetThumbnail.PROXIED)
429
430    def test_load_project_with_missing_proxy_progress_tracking(self):
431        """Checks progress tracking of loading project with missing proxies."""
432        from gi.repository import GstTranscoder
433
434        with common.cloned_sample("1sec_simpsons_trailer.mp4"):
435            # Disable proxy generation by not making it start ever.
436            # This way we are sure it will not finish before we test
437            # the state while it is being rebuilt.
438            with mock.patch.object(GstTranscoder.Transcoder, "run_async"):
439                mainloop, app, medialib, proxy_uri = self.load_project_with_missing_proxy()
440                uri = common.get_sample_uri("1sec_simpsons_trailer.mp4")
441
442                app.project_manager.connect("new-project-loaded", lambda x, y: mainloop.quit())
443                mainloop.run()
444
445        self.assertEqual(len(medialib.storemodel), 1)
446        self.assertEqual(medialib.storemodel[0][medialibrary.COL_ASSET].props.id,
447                         uri)
448        self.assertEqual(medialib.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
449                         medialibrary.AssetThumbnail.IN_PROGRESS)
450
451    def test_load_project_with_missing_proxy_stop_generating_and_proxy(self):
452        """Checks cancelling creation of a missing proxies and forcing it again."""
453        from gi.repository import GstTranscoder
454
455        with common.cloned_sample("1sec_simpsons_trailer.mp4"):
456            # Disable proxy generation by not making it start ever.
457            # This way we are sure it will not finish before we test
458            # stop generating the proxy and restart it.
459            with mock.patch.object(GstTranscoder.Transcoder, "run_async"):
460                mainloop, app, medialib, proxy_uri = self.load_project_with_missing_proxy()
461
462                app.project_manager.connect("new-project-loaded", lambda x, y: mainloop.quit())
463                mainloop.run()
464                asset = medialib.storemodel[0][medialibrary.COL_ASSET]
465                app.project_manager.current_project.disable_proxies_for_assets([asset])
466
467            row, = medialib.storemodel
468            asset = row[medialibrary.COL_ASSET]
469            self.assertEqual(medialib._progressbar.get_fraction(), 1.0)
470            uri = common.get_sample_uri("1sec_simpsons_trailer.mp4")
471            self.assertEqual(asset.props.id, uri)
472            self.assertEqual(asset.ready, True)
473            self.assertEqual(asset.creation_progress, 100)
474            self.assertEqual(row[medialibrary.COL_THUMB_DECORATOR].state,
475                             medialibrary.AssetThumbnail.NO_PROXY)
476
477            app.project_manager.current_project.use_proxies_for_assets([asset])
478            mainloop.run()
479
480        row, = medialib.storemodel
481        asset = row[medialibrary.COL_ASSET]
482        self.assertEqual(medialib._progressbar.is_visible(), False)
483        self.assertEqual(asset.props.id, proxy_uri)
484        self.assertEqual(asset.ready, True)
485        self.assertEqual(asset.creation_progress, 100)
486        self.assertEqual(row[medialibrary.COL_THUMB_DECORATOR].state,
487                         medialibrary.AssetThumbnail.PROXIED)
488
489    def test_loading_project_with_moved_asset(self):
490        """Loads a project with moved asset."""
491        app = common.create_pitivi(proxyingStrategy=ProxyingStrategy.NOTHING)
492
493        proj_uri = self.create_project_file_from_xges(app, """<ges version='0.3'>
494            <project properties='properties;' metadatas='metadatas;'>
495                <ressources>
496                    <asset id='file://this/is/a/moved/asset.mp4' extractable-type-name='GESUriClip'
497                        properties='properties, supported-formats=(int)6, duration=(guint64)1228000000;' metadatas='metadatas' />
498                </ressources>
499            </project>
500            </ges>""")
501        project_manager = app.project_manager
502        medialib = medialibrary.MediaLibraryWidget(app)
503
504        mainloop = common.create_main_loop()
505
506        def new_project_loaded_cb(*args, **kwargs):
507            mainloop.quit()
508
509        def missing_uri_cb(project_manager, project, unused_error, asset):
510            return common.get_sample_uri("1sec_simpsons_trailer.mp4")
511
512        project_manager.connect("missing-uri", missing_uri_cb)
513        project_manager.connect("new-project-loaded", new_project_loaded_cb)
514
515        project_manager.loadProject(proj_uri)
516        with common.cloned_sample("1sec_simpsons_trailer.mp4"):
517            mainloop.run()
518        self.assertEqual(medialib._progressbar.get_fraction(), 1.0)
519
520    def test_loading_project_with_moved_assets_and_deleted_proxy(self):
521        """Loads a project with moved asset as deleted proxy file."""
522
523        mainloop = common.create_main_loop()
524
525        def proxy_ready_cb(unused_proxy_manager, asset, proxy):
526            mainloop.quit()
527
528        app = common.create_pitivi(proxyingStrategy=ProxyingStrategy.ALL)
529        app.proxy_manager.connect("proxy-ready", proxy_ready_cb)
530
531        proj_uri = self.create_project_file_from_xges(app, """<ges version='0.3'>
532  <project properties='properties;' metadatas='metadatas, name=(string)&quot;New\ Project&quot;, author=(string)Unknown, render-scale=(double)100, format-version=(string)0.3;'>
533    <ressources>
534      <asset id='file:///nop/1sec_simpsons_trailer.mp4' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)1228000000;' metadatas='metadatas, audio-codec=(string)&quot;MPEG-4\ AAC\ audio&quot;, maximum-bitrate=(uint)130625, bitrate=(uint)130625, datetime=(datetime)2007-02-19T05:03:04Z, encoder=(string)Lavf54.6.100, container-format=(string)&quot;ISO\ MP4/M4A&quot;, video-codec=(string)&quot;H.264\ /\ AVC&quot;, file-size=(guint64)232417;'  proxy-id='file:///nop/1sec_simpsons_trailer.mp4.232417.proxy.mkv' />
535      <asset id='file:///nop/tears_of_steel.webm' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)2003000000;' metadatas='metadatas, container-format=(string)Matroska, language-code=(string)und, application-name=(string)Lavc56.60.100, encoder=(string)&quot;Xiph.Org\ libVorbis\ I\ 20150105\ \(\342\233\204\342\233\204\342\233\204\342\233\204\)&quot;, encoder-version=(uint)0, audio-codec=(string)Vorbis, nominal-bitrate=(uint)80000, bitrate=(uint)80000, video-codec=(string)&quot;VP8\ video&quot;, file-size=(guint64)223340;' />
536      <asset id='file:///nop/1sec_simpsons_trailer.mp4.232417.proxy.mkv' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)1228020833;' metadatas='metadatas, container-format=(string)Matroska, audio-codec=(string)Opus, language-code=(string)en, encoder=(string)Lavf54.6.100, bitrate=(uint)64000, video-codec=(string)&quot;Motion\ JPEG&quot;, file-size=(guint64)4694708;' />
537      <asset id='file:///nop/tears_of_steel.webm.223340.proxy.mkv' extractable-type-name='GESUriClip' properties='properties, supported-formats=(int)6, duration=(guint64)2003000000;' metadatas='metadatas, container-format=(string)Matroska, language-code=(string)und, application-name=(string)Lavc56.60.100, encoder=(string)&quot;Xiph.Org\ libVorbis\ I\ 20150105\ \(\342\233\204\342\233\204\342\233\204\342\233\204\)&quot;, encoder-version=(uint)0, audio-codec=(string)Vorbis, nominal-bitrate=(uint)80000, bitrate=(uint)80000, video-codec=(string)&quot;VP8\ video&quot;, file-size=(guint64)223340;' />
538    </ressources>
539</project>
540</ges>""")
541        project_manager = app.project_manager
542        medialib = medialibrary.MediaLibraryWidget(app)
543
544        # Remove proxy
545        with common.cloned_sample("1sec_simpsons_trailer.mp4", "tears_of_steel.webm"):
546            def new_project_loaded_cb(*args, **kwargs):
547                mainloop.quit()
548
549            missing_uris = []
550
551            def missing_uri_cb(project_manager, project, unused_error, asset):
552                missing_uris.append(asset.props.id)
553                return common.get_sample_uri(os.path.basename(asset.props.id))
554
555            project_manager.connect("missing-uri", missing_uri_cb)
556            project_manager.connect("new-project-loaded", new_project_loaded_cb)
557
558            project_manager.loadProject(proj_uri)
559            mainloop.run()
560            self.assertEqual(len(missing_uris), 1,
561                "missing_uri_cb should be called only once, got %s." % missing_uris)
562            self.assertEqual(medialib._progressbar.get_fraction(), 1.0)
563            mainloop.run()
564            self.assertEqual(len(medialib.storemodel), 2,
565                "We should have one asset displayed in the MediaLibrary.")
566            self.assertEqual(medialib.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
567                             medialibrary.AssetThumbnail.PROXIED)
568            self.assertEqual(medialib.storemodel[1][medialibrary.COL_THUMB_DECORATOR].state,
569                             medialibrary.AssetThumbnail.IN_PROGRESS)
570
571
572class TestProjectSettings(common.TestCase):
573
574    def testAudio(self):
575        project = common.create_project()
576        project.audiochannels = 2
577        self.assertEqual(2, project.audiochannels)
578        project.audiorate = 44100
579        self.assertEqual(44100, project.audiorate)
580
581    def testVideo(self):
582        project = common.create_project()
583        project.videowidth = 1920
584        self.assertEqual(1920, project.videowidth)
585        project.videoheight = 1080
586        self.assertEqual(1080, project.videoheight)
587        project.videorate = Gst.Fraction(50, 7)
588        self.assertEqual(Gst.Fraction(50, 7), project.videorate)
589
590    def testSetAudioProp(self):
591        timeline = common.create_timeline_container()
592        project = timeline.app.project_manager.current_project
593        project.addUris([common.get_sample_uri("mp3_sample.mp3")])
594
595        audio_track = [t for t in project.ges_timeline.tracks if isinstance(t, GES.AudioTrack)][0]
596        mainloop = common.create_main_loop()
597
598        def progress_cb(project, progress, estimated_time):
599            if progress == 100:
600                mainloop.quit()
601
602        project.connect_after("asset-loading-progress", progress_cb)
603        mainloop.run()
604
605        expected = Gst.Caps("audio/x-raw,channels=(int)2,rate=(int)44100")
606        ccaps = audio_track.props.restriction_caps
607        self.assertTrue(ccaps.is_equal_fixed(expected), "%s != %s" % (ccaps, expected))
608
609        project.audiochannels = 6
610
611        expected = Gst.Caps("audio/x-raw,channels=(int)6,rate=(int)44100")
612        ccaps = audio_track.props.restriction_caps
613        self.assertTrue(ccaps.is_equal_fixed(expected), "%s != %s" % (ccaps, expected))
614
615    def testInitialization(self):
616        mainloop = common.create_main_loop()
617        uris = collections.deque([
618            common.get_sample_uri("flat_colour1_640x480.png"),
619            common.get_sample_uri("tears_of_steel.webm"),
620            common.get_sample_uri("1sec_simpsons_trailer.mp4")])
621
622        def loaded_cb(project, timeline):
623            project.addUris([uris.popleft()])
624
625        def progress_cb(project, progress, estimated_time):
626            if progress == 100:
627                if uris:
628                    project.addUris([uris.popleft()])
629                else:
630                    mainloop.quit()
631
632        # Create a blank project and add some assets.
633        project = common.create_project()
634        self.assertTrue(project._has_default_video_settings)
635        self.assertTrue(project._has_default_audio_settings)
636
637        project.connect_after("loaded", loaded_cb)
638        project.connect_after("asset-loading-progress", progress_cb)
639
640        mainloop.run()
641
642        assets = project.list_assets(GES.UriClip)
643        self.assertEqual(3, len(assets), assets)
644
645        self.assertFalse(project._has_default_video_settings)
646        self.assertFalse(project._has_default_audio_settings)
647
648        # The audio settings should match tears_of_steel.webm
649        self.assertEqual(1, project.audiochannels)
650        self.assertEqual(44100, project.audiorate)
651
652        # The video settings should match tears_of_steel.webm
653        self.assertEqual(960, project.videowidth)
654        self.assertEqual(400, project.videoheight)
655        self.assertEqual(Gst.Fraction(24, 1), project.videorate)
656
657    def testLoad(self):
658        project = Project(uri="fake.xges", app=common.create_pitivi_mock())
659        self.assertFalse(project._has_default_video_settings)
660        self.assertFalse(project._has_default_audio_settings)
661
662
663class TestExportSettings(common.TestCase):
664
665    def test_master_attributes(self):
666        self._check_master_attribute("muxer", dependant_attr="containersettings")
667        self._check_master_attribute("vencoder", dependant_attr="vcodecsettings")
668        self._check_master_attribute("aencoder", dependant_attr="acodecsettings")
669
670    def _check_master_attribute(self, attr, dependant_attr):
671        """Test changing the specified attr has effect on its dependent attr."""
672        project = common.create_project()
673
674        attr_value1 = "%s_value1" % attr
675        attr_value2 = "%s_value2" % attr
676
677        setattr(project, attr, attr_value1)
678        setattr(project, dependant_attr, {})
679        getattr(project, dependant_attr)["key1"] = "v1"
680
681        setattr(project, attr, attr_value2)
682        setattr(project, dependant_attr, {})
683        getattr(project, dependant_attr)["key2"] = "v2"
684
685        setattr(project, attr, attr_value1)
686        self.assertTrue("key1" in getattr(project, dependant_attr))
687        self.assertFalse("key2" in getattr(project, dependant_attr))
688        self.assertEqual("v1", getattr(project, dependant_attr)["key1"])
689        setattr(project, dependant_attr, {})
690
691        setattr(project, attr, attr_value2)
692        self.assertFalse("key1" in getattr(project, dependant_attr))
693        self.assertTrue("key2" in getattr(project, dependant_attr))
694        self.assertEqual("v2", getattr(project, dependant_attr)["key2"])
695        setattr(project, dependant_attr, {})
696
697        setattr(project, attr, attr_value1)
698        self.assertFalse("key1" in getattr(project, dependant_attr))
699        self.assertFalse("key2" in getattr(project, dependant_attr))
700
701        setattr(project, attr, attr_value2)
702        self.assertFalse("key1" in getattr(project, dependant_attr))
703        self.assertFalse("key2" in getattr(project, dependant_attr))
704
705    def test_set_rendering(self):
706        """Checks the set_rendering method."""
707        mainloop = common.create_main_loop()
708
709        def loaded_cb(project, timeline):
710            project.addUris([common.get_sample_uri("tears_of_steel.webm")])
711
712        def progress_cb(project, progress, estimated_time):
713            if progress == 100:
714                mainloop.quit()
715
716        # Create a blank project and add some assets.
717        project = common.create_project()
718
719        project.connect_after("loaded", loaded_cb)
720        project.connect_after("asset-loading-progress", progress_cb)
721
722        mainloop.run()
723
724        # The video settings should match tears_of_steel.webm
725        self.assertEqual(project.videowidth, 960)
726        self.assertEqual(project.videoheight, 400)
727
728        project.render_scale = 3
729        # Pretend we're rendering.
730        project.set_rendering(True)
731        self.assertEqual(project.videowidth, 28)
732        self.assertEqual(project.videoheight, 12)
733
734        # Pretend we're not rendering anymore.
735        project.set_rendering(False)
736        self.assertEqual(project.videowidth, 960)
737        self.assertEqual(project.videoheight, 400)
738