[pitivi] proxy: Add Scaled Proxies
- From: Alexandru Băluț <alexbalut src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] proxy: Add Scaled Proxies
- Date: Thu, 26 Sep 2019 19:45:20 +0000 (UTC)
commit 3a2725ba7e3f8c3eb3b535aeca75ed9b59e46a71
Author: yatinmaan <yatinmaan1 gmail com>
Date: Wed May 23 10:38:16 2018 +0530
proxy: Add Scaled Proxies
Fixes #743
Fixes #2344
data/pixmaps/asset-scaled.svg | 107 +++++++++++
data/pixmaps/question-round-symbolic.svg | 89 +++++++++
data/pixmaps/warning-symbolic.svg | 89 +++++++++
data/ui/projectsettings.ui | 181 +++++++++++++++++-
pitivi/dialogs/prefs.py | 189 +++++++++++++------
pitivi/medialibrary.py | 254 +++++++++++++++++++------
pitivi/project.py | 152 +++++++++++++--
pitivi/render.py | 93 ++++++----
pitivi/timeline/previewers.py | 2 +-
pitivi/timeline/timeline.py | 39 ++--
pitivi/utils/proxy.py | 310 ++++++++++++++++++++++++++-----
pitivi/utils/timeline.py | 4 +-
pitivi/utils/ui.py | 3 +-
pitivi/utils/widgets.py | 4 +-
tests/test_media_library.py | 207 ++++++++++++++++++++-
tests/test_prefs.py | 14 ++
tests/test_project.py | 34 ++++
tests/test_proxy.py | 98 ++++++++++
tests/test_render.py | 86 +++++++++
19 files changed, 1697 insertions(+), 258 deletions(-)
---
diff --git a/data/pixmaps/asset-scaled.svg b/data/pixmaps/asset-scaled.svg
new file mode 100644
index 00000000..547bda05
--- /dev/null
+++ b/data/pixmaps/asset-scaled.svg
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="26.955959mm"
+ height="25.062054mm"
+ viewBox="0 0 95.513242 88.802552"
+ id="svg4817"
+ version="1.1"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ sodipodi:docname="bolt-gradient-2.svg">
+ <defs
+ id="defs4819">
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient4173"
+ id="radialGradient4181"
+ cx="33.283539"
+ cy="1002.8445"
+ fx="33.283539"
+ fy="1002.8445"
+ r="80.256622"
+ gradientTransform="matrix(1.1432904,-1.9668506e-8,1.8574145e-8,1.0796774,70.127178,-163.84935)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient4173">
+ <stop
+ style="stop-color:#000000;stop-opacity:0.502"
+ offset="0"
+ id="stop4175" />
+ <stop
+ style="stop-color:#000000;stop-opacity:0"
+ offset="1"
+ id="stop4177" />
+ </linearGradient>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="50.999465"
+ inkscape:cy="63.919525"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1920"
+ inkscape:window-height="1018"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata4822">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-107.92332,-830.2478)">
+ <rect
+
style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:url(#radialGradient4181);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.54330707;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="rect4171"
+ width="95.513245"
+ height="88.802551"
+ x="107.92332"
+ y="830.2478" />
+ <path
+ id="path32"
+ d="m 127.49428,898.70065 h -3.37127 l 1.01131,-3.79238 c 0.11861,-0.44487 -0.21717,-0.88178
-0.67746,-0.88178 h -3.97303 c -0.35141,0 -0.64855,0.26013 -0.69496,0.60845 l -0.93471,7.01125 c
-0.056,0.42034 0.27135,0.79378 0.69495,0.79378 h 3.46767 l -1.34554,5.68104 c -0.10506,0.44355
0.23409,0.86279 0.68202,0.86279 0.24392,0 0.47839,-0.12778 0.60699,-0.34992 l 5.14079,-8.88082 c
0.27005,-0.46645 -0.0669,-1.05241 -0.60676,-1.05241 z"
+ inkscape:connector-curvature="0"
+ style="fill:currentColor;stroke-width:0.02921349" />
+ <path
+ id="path32-0"
+ d="m 126.99443,898.19899 h -3.37127 l 1.01131,-3.79238 c 0.11861,-0.44487 -0.21717,-0.88178
-0.67746,-0.88178 h -3.97303 c -0.35141,0 -0.64855,0.26013 -0.69496,0.60845 l -0.93471,7.01125 c
-0.056,0.42034 0.27135,0.79378 0.69495,0.79378 h 3.46767 l -1.34554,5.68104 c -0.10506,0.44355
0.23409,0.86279 0.68202,0.86279 0.24392,0 0.47839,-0.12778 0.60699,-0.34992 l 5.14079,-8.88082 c
0.27005,-0.46645 -0.0669,-1.05241 -0.60676,-1.05241 z"
+ inkscape:connector-curvature="0"
+ style="fill:#ffcc00;stroke-width:0.02921349" />
+ </g>
+</svg>
+<!--Derived work based on
+Font Awesome Free 5.1.0 by @fontawesome - https://fontawesome.com
+License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
+-->
diff --git a/data/pixmaps/question-round-symbolic.svg b/data/pixmaps/question-round-symbolic.svg
new file mode 100644
index 00000000..b64de43d
--- /dev/null
+++ b/data/pixmaps/question-round-symbolic.svg
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ viewBox="0 0 16 16"
+ version="1.1"
+ id="svg7384"
+ height="16">
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Gnome Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title9167">Gnome Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ osb:paint="solid"
+ id="linearGradient7212">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop7214" />
+ </linearGradient>
+ </defs>
+ <g
+ transform="translate(-19.9999,139.98872)"
+ style="display:inline"
+ id="layer1" />
+ <g
+ transform="translate(-261.0001,506.98872)"
+ style="display:inline"
+ id="layer9" />
+ <g
+ transform="translate(-261.0001,506.98872)"
+ style="display:inline"
+ id="g7628" />
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer13" />
+ <g
+ transform="translate(-19.9999,139.98872)"
+ style="display:inline"
+ id="layer11" />
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer14" />
+ <g
+ transform="translate(-19.9999,139.98872)"
+ style="display:inline"
+ id="g6387">
+ <path
+ d="m 28,-139 a 7,7 0 0 0 -7,7 7,7 0 0 0 7,7 7,7 0 0 0 7,-7 7,7 0 0 0 -7,-7 z m -0.1875,2.01172 c
1.64243,-0.092 3.0955,1.17008 3.1875,2.8125 -10e-5,1.40136 -0.37771,1.92177 -1.59375,2.84375 -0.19093,0.14364
-0.3256,0.2506 -0.375,0.3125 -0.0494,0.0621 -0.03125,0.0333 -0.03125,0.0312 0.007,0.52831 -0.47163,1 -1,1
-0.52837,0 -1.007,-0.47169 -1,-1 0,-0.50239 0.22424,-0.94342 0.46875,-1.25 0.24451,-0.30663 0.4913,-0.51638
0.71875,-0.6875 0.20405,-0.16056 0.46083,-0.38454 0.6875,-0.65625 0.0935,-0.1121 0.129,-0.30766 0.125,-0.4375
v -0.0312 c -0.0316,-0.56324 -0.49926,-0.9691 -1.0625,-0.9375 -0.56324,0.0316 -0.9691,0.43676 -0.9375,1 h -2
c -0.092,-1.64243 1.17007,-2.9079 2.8125,-3 z m 0.1875,8 c 0.55228,0 1,0.44772 1,1 0,0.55228 -0.44772,1 -1,1
-0.55228,0 -1,-0.44772 -1,-1 0,-0.55228 0.44772,-1 1,-1 z"
+ id="path15586"
+
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal"
/>
+ </g>
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer18" />
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer17" />
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer16" />
+ <g
+ transform="translate(-19.9999,139.98872)"
+ style="display:inline"
+ id="layer10" />
+ <g
+ transform="translate(-19.9999,-60.01128)"
+ id="layer15" />
+ <g
+ transform="translate(-19.9999,139.98872)"
+ id="layer12" />
+</svg>
diff --git a/data/pixmaps/warning-symbolic.svg b/data/pixmaps/warning-symbolic.svg
new file mode 100644
index 00000000..8caf2a36
--- /dev/null
+++ b/data/pixmaps/warning-symbolic.svg
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="16"
+ viewBox="0 0 16 16"
+ version="1.1"
+ id="svg7384"
+ height="16">
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Gnome Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title9167">Gnome Symbolic Icon Theme</title>
+ <defs
+ id="defs7386">
+ <linearGradient
+ osb:paint="solid"
+ id="linearGradient7212">
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="0"
+ id="stop7214" />
+ </linearGradient>
+ </defs>
+ <g
+ transform="translate(-39.9997,159.98872)"
+ style="display:inline"
+ id="layer1" />
+ <g
+ transform="translate(-280.9999,526.98872)"
+ style="display:inline"
+ id="layer9" />
+ <g
+ transform="translate(-280.9999,526.98872)"
+ style="display:inline"
+ id="g7628" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer13" />
+ <g
+ transform="translate(-39.9997,159.98872)"
+ style="display:inline"
+ id="layer11" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer14" />
+ <g
+ transform="translate(-39.9997,159.98872)"
+ style="display:inline"
+ id="g6387" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer18" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer17" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer16" />
+ <g
+ transform="translate(-39.9997,159.98872)"
+ style="display:inline"
+ id="layer10" />
+ <g
+ transform="translate(-39.9997,-40.01128)"
+ id="layer15" />
+ <g
+ transform="translate(-39.9997,159.98872)"
+ id="layer12">
+ <path
+ d="m 47.90615,-159.89497 c -0.5255,-0.0286 -1.03823,0.28305 -1.4375,0.96875 l -6.25,11.59375 c
-0.53347,0.96339 0.04822,2.34375 1.09375,2.34375 h 13.15625 c 0.98172,0 1.90311,-1.15939 1.21875,-2.34375 l
-6.3125,-11.53125 c -0.39872,-0.64617 -0.94325,-1.00262 -1.46875,-1.03125 z m 0.0625,3.9375 c 0.54448,-0.0172
1.04849,0.48677 1.03125,1.03125 v 3.9375 c 0.007,0.52831 -0.47163,1 -1,1 -0.52836,0 -1.00747,-0.47169 -1,-1 v
-3.9375 c -0.008,-0.4666 0.3541,-0.91253 0.8125,-1 0.0511,-0.0145 0.10345,-0.0249 0.15625,-0.0312 z m
0.03125,6.96875 c 0.552285,0 1,0.44772 1,1 0,0.55228 -0.447715,1 -1,1 -0.552285,0 -1,-0.44772 -1,-1
0,-0.55228 0.447715,-1 1,-1 z"
+ id="path18112"
+
style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
/>
+ </g>
+</svg>
diff --git a/data/ui/projectsettings.ui b/data/ui/projectsettings.ui
index 5aa8a52a..5d36665b 100644
--- a/data/ui/projectsettings.ui
+++ b/data/ui/projectsettings.ui
@@ -23,6 +23,18 @@
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
+ <object class="GtkAdjustment" id="adjustment4">
+ <property name="lower">1</property>
+ <property name="upper">9999</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="adjustment5">
+ <property name="lower">1</property>
+ <property name="upper">9999</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">10</property>
+ </object>
<object class="GtkDialog" id="project-settings-dialog">
<property name="can_focus">False</property>
<property name="border_width">5</property>
@@ -218,7 +230,7 @@
<object class="GtkLabel" id="label9">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="label">x</property>
+ <property name="label" translatable="no">×</property>
</object>
<packing>
<property name="expand">False</property>
@@ -296,7 +308,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
- <property name="label" translatable="yes">Frame Rate:</property>
+ <property name="label" translatable="yes">Frame rate:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -601,7 +613,7 @@
<property name="halign">start</property>
<property name="max_length">4</property>
<property name="invisible_char">●</property>
- <property name="text">1900</property>
+ <property name="text" translatable="no">1900</property>
<property name="adjustment">adjustment3</property>
<property name="numeric">True</property>
<property name="value">1900</property>
@@ -629,6 +641,169 @@
<property name="width">2</property>
</packing>
</child>
+ <child>
+ <object class="GtkFrame" id="frame4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label_xalign">0</property>
+ <child>
+ <object class="GtkAlignment" id="alignment4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="left_padding">12</property>
+ <child>
+ <object class="GtkBox" id="video_tab1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="border_width">12</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkBox" id="video_details1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkBox" id="size_box1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Scaled proxies
resolution:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkSpinButton" id="scaled_proxy_width">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="text" translatable="yes">1</property>
+ <property name="adjustment">adjustment4</property>
+ <property name="numeric">True</property>
+ <property name="value">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label13">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="no">×</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="scaled_proxy_height">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">●</property>
+ <property name="text" translatable="yes">1</property>
+ <property name="adjustment">adjustment5</property>
+ <property name="numeric">True</property>
+ <property name="value">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label14">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">pixels</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="proxy_res_linked">
+ <property name="label" translatable="yes" comments="When checked,
changing the width or height affects also the other so that the aspect ratio value (width / height) does not
change.">Constrain proportions</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="halign">start</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="_proxy_res_linked_toggle_cb"
swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="label">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Proxy</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
</object>
<packing>
<property name="expand">False</property>
diff --git a/pitivi/dialogs/prefs.py b/pitivi/dialogs/prefs.py
index 220f2ec7..8b31d5a5 100644
--- a/pitivi/dialogs/prefs.py
+++ b/pitivi/dialogs/prefs.py
@@ -59,8 +59,9 @@ class PreferencesDialog(Loggable):
prefs = {}
section_names = {
"timeline": _("Timeline"),
- "_plugins": _("Plugins"),
- "_shortcuts": _("Shortcuts")
+ "__plugins": _("Plugins"),
+ "__shortcuts": _("Shortcuts"),
+ "_proxies": _("Proxies"),
}
def __init__(self, app):
@@ -91,6 +92,7 @@ class PreferencesDialog(Loggable):
self.add_settings_page(section_id)
self.factory_settings.set_sensitive(self._canReset())
+ self.__add_proxies_section()
self.__add_shortcuts_section()
self.__add_plugin_manager_section()
self.__setup_css()
@@ -152,7 +154,7 @@ class PreferencesDialog(Loggable):
"""
if section not in cls.section_names:
raise Exception("%s is not a valid section id" % section)
- if section.startswith("_"):
+ if section.startswith("__"):
raise Exception("Cannot add preferences to reserved sections")
if section not in cls.prefs:
cls.prefs[section] = {}
@@ -225,8 +227,11 @@ class PreferencesDialog(Loggable):
def add_settings_page(self, section_id):
"""Adds a page for the preferences in the specified section."""
- options = self.prefs[section_id]
+ prefs = self._prepare_prefs_widgets(self.prefs[section_id])
+ container = self._create_props_container(prefs)
+ self._add_page(section_id, container)
+ def _create_props_container(self, prefs):
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
container.set_border_width(SPACING)
@@ -235,84 +240,151 @@ class PreferencesDialog(Loggable):
listbox.props.margin = PADDING * 2
listbox.get_style_context().add_class('prefs_list')
- container.add(listbox)
+ container.pack_start(listbox, False, False, 0)
label_size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
prop_size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
- prefs = []
- for attrname in options:
- label, description, widget_class, args = options[attrname]
- widget = widget_class(**args)
- widget.setWidgetValue(getattr(self.settings, attrname))
- widget.connectValueChanged(self._valueChangedCb, widget, attrname)
- widget.set_tooltip_text(description)
- self.widgets[attrname] = widget
-
- widget.props.margin_left = PADDING * 3
- widget.props.margin_right = PADDING * 3
- widget.props.margin_top = PADDING * 2
- widget.props.margin_bottom = PADDING * 2
-
- prop_size_group.add_widget(widget)
+ for y, (label, widget, revert_widget, extra_widget) in enumerate(prefs):
+ box = Gtk.Box()
label_widget = Gtk.Label(label=label)
- label_widget.set_tooltip_text(description)
label_widget.set_alignment(0.0, 0.5)
-
- label_widget.props.margin_left = PADDING * 3
label_widget.props.margin_right = PADDING * 3
label_widget.props.margin_top = PADDING * 2
label_widget.props.margin_bottom = PADDING * 2
-
- label_widget.show()
label_size_group.add_widget(label_widget)
- icon = Gtk.Image()
- icon.set_from_icon_name(
- "edit-clear-all-symbolic", Gtk.IconSize.MENU)
- revert = Gtk.Button()
- revert.add(icon)
- revert.set_tooltip_text(_("Reset to default value"))
- revert.set_relief(Gtk.ReliefStyle.NONE)
- revert.set_sensitive(not self.settings.isDefault(attrname))
- revert.connect("clicked", self.__reset_option_cb, attrname)
- revert.show_all()
+ widget.props.margin_right = PADDING * 3
+ widget.props.margin_top = PADDING * 2
+ widget.props.margin_bottom = PADDING * 2
+ prop_size_group.add_widget(widget)
- self.resets[attrname] = revert
- row_widgets = (label_widget, widget, revert)
- # Construct the prefs list so that it can be sorted.
- # Make sure the L{ToggleWidget}s appear at the end.
- prefs.append((label_widget is None, label, row_widgets))
-
- # Sort widgets: I think we only want to sort by the non-localized
- # names, so options appear in the same place across locales ...
- # but then I may be wrong
- for y, (_1, _2, row_widgets) in enumerate(sorted(prefs)):
- label, widget, revert = row_widgets
- box = Gtk.Box()
+ box.pack_start(label_widget, True, True, 0)
+ box.pack_start(widget, False, False, 0)
+ if revert_widget:
+ box.pack_start(revert_widget, False, False, 0)
- if label:
- box.pack_start(label, True, True, 0)
+ if extra_widget:
+ box1 = box
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ box.pack_start(box1, False, False, 0)
+ box.pack_start(extra_widget, False, False, SPACING)
- box.pack_start(widget, False, False, 0)
- box.pack_start(revert, False, False, 0)
+ box.props.margin_left = PADDING * 3
row = Gtk.ListBoxRow()
row.set_activatable(False)
row.add(box)
- listbox.add(row)
-
if y == 0:
row.get_style_context().add_class('first')
+ listbox.add(row)
container.show_all()
- self._add_page(section_id, container)
+ return container
+
+ def _create_revert_button(self):
+ revert = Gtk.Button.new_from_icon_name("edit-clear-all-symbolic", Gtk.IconSize.MENU)
+ revert.set_tooltip_text(_("Reset to default value"))
+ revert.set_relief(Gtk.ReliefStyle.NONE)
+ revert.props.valign = Gtk.Align.CENTER
+ return revert
+
+ def _prepare_prefs_widgets(self, options):
+ prefs = []
+ for attrname in options:
+ label, description, widget_class, args = options[attrname]
+ widget = widget_class(**args)
+ widget.setWidgetValue(getattr(self.settings, attrname))
+ widget.connectValueChanged(self._valueChangedCb, widget, attrname)
+ widget.set_tooltip_text(description)
+ self.widgets[attrname] = widget
+
+ revert = self._create_revert_button()
+ revert.set_sensitive(not self.settings.isDefault(attrname))
+ revert.connect("clicked", self.__reset_option_cb, attrname)
+ self.resets[attrname] = revert
+
+ # Construct the prefs list so that it can be sorted.
+ prefs.append((label, widget, revert, None))
+
+ # TODO: We need to know exactly where each preference appears,
+ # currently they are sorted by the translated label.
+ return sorted(prefs)
def __add_plugin_manager_section(self):
page = PluginPreferencesPage(self.app, self)
page.show_all()
- self._add_page("_plugins", page)
+ self._add_page("__plugins", page)
+
+ def __add_proxies_section(self):
+ """Adds a section for proxy settings."""
+ prefs = self._prepare_prefs_widgets(self.prefs["_proxies"])
+
+ self.proxy_width_widget = widgets.NumericWidget(lower=1, width_chars=4)
+ self.proxy_width_widget.setWidgetValue(self.app.settings.default_scaled_proxy_width)
+ self.widgets["default_scaled_proxy_width"] = self.proxy_width_widget
+ self.proxy_height_widget = widgets.NumericWidget(lower=1, width_chars=4)
+ self.proxy_height_widget.setWidgetValue(self.app.settings.default_scaled_proxy_height)
+ self.widgets["default_scaled_proxy_height"] = self.proxy_height_widget
+ size_box = Gtk.Box(spacing=SPACING)
+ size_box.pack_start(self.proxy_width_widget, False, False, 0)
+ size_box.pack_start(Gtk.Label("×"), False, False, 0)
+ size_box.pack_start(self.proxy_height_widget, False, False, 0)
+ size_box.set_tooltip_text(_("This resolution will be used as the"
+ " default target resolution for new projects and projects missing"
+ " scaled proxy meta-data."))
+ self.scaled_proxy_size_revert_button = self._create_revert_button()
+
+ self.proxy_infobar = Gtk.InfoBar.new()
+ fix_infobar(self.proxy_infobar)
+ self.proxy_infobar.set_message_type(Gtk.MessageType.WARNING)
+ self.proxy_infobar.add_button(_("Project Settings"), Gtk.ResponseType.OK)
+ self.scaled_proxies_infobar_label = Gtk.Label.new()
+ self.proxy_infobar.get_content_area().add(self.scaled_proxies_infobar_label)
+ self.proxy_infobar.show_all()
+
+ prefs.append((_("Initial proxy size for new projects"), size_box,
self.scaled_proxy_size_revert_button, None))
+
+ container = self._create_props_container(prefs)
+
+ container.pack_start(self.proxy_infobar, False, False, 0)
+
+ self._add_page("_proxies", container)
+
+ self.__update_scaled_proxies_infobar()
+ self.__update_proxy_size_revert_button()
+
+ self.proxy_width_widget.connectValueChanged(self.__scaled_proxy_size_change_cb)
+ self.proxy_height_widget.connectValueChanged(self.__scaled_proxy_size_change_cb)
+ self.scaled_proxy_size_revert_button.connect("clicked", self.__reset_option_cb,
"default_scaled_proxy_width", "default_scaled_proxy_height")
+ self.proxy_infobar.connect("response", self.__proxy_infobar_cb)
+
+ def __scaled_proxy_size_change_cb(self, unused_widget):
+ self.app.settings.default_scaled_proxy_width = self.proxy_width_widget.getWidgetValue()
+ self.app.settings.default_scaled_proxy_height = self.proxy_height_widget.getWidgetValue()
+ self.__update_scaled_proxies_infobar()
+ self.__update_proxy_size_revert_button()
+
+ def __update_proxy_size_revert_button(self):
+ default = all([self.settings.isDefault(setting)
+ for setting in ("default_scaled_proxy_width", "default_scaled_proxy_height")])
+ self.scaled_proxy_size_revert_button.set_sensitive(not default)
+
+ def __update_scaled_proxies_infobar(self):
+ project = self.app.project_manager.current_project
+ different = project and \
+ (project.scaled_proxy_width != self.app.settings.default_scaled_proxy_width or
+ project.scaled_proxy_height != self.app.settings.default_scaled_proxy_height)
+ self.proxy_infobar.set_visible(different)
+ if different:
+ self.scaled_proxies_infobar_label.set_text(
+ _("Proxy resolution for the current project is %d×%d px") %
+ (project.scaled_proxy_width, project.scaled_proxy_height))
+
+ def __proxy_infobar_cb(self, unused_infobar, unused_response_id):
+ self.app.gui.editor.showProjectSettingsDialog()
+ self.__update_scaled_proxies_infobar()
def __add_shortcuts_section(self):
"""Adds a section with keyboard shortcuts."""
@@ -349,7 +421,7 @@ class PreferencesDialog(Loggable):
outside_box.add(scrolled_window)
outside_box.show_all()
- self._add_page("_shortcuts", outside_box)
+ self._add_page("__shortcuts", outside_box)
def __row_activated_cb(self, list_box, row):
index = row.get_index()
@@ -463,8 +535,9 @@ class PreferencesDialog(Loggable):
self.revert_button.set_sensitive(False)
self.factory_settings.set_sensitive(self._canReset())
- def __reset_option_cb(self, button, attrname):
- self.__reset_option(button, attrname)
+ def __reset_option_cb(self, button, *attrnames):
+ for attrname in attrnames:
+ self.__reset_option(button, attrname)
def __reset_option(self, button, attrname):
"""Resets a particular setting to the factory default."""
diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py
index 5a1082d7..2024e6c8 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -50,6 +50,7 @@ from pitivi.utils.misc import disconnectAllByFunc
from pitivi.utils.misc import path_from_uri
from pitivi.utils.misc import PathWalker
from pitivi.utils.misc import quote_uri
+from pitivi.utils.misc import show_user_manual
from pitivi.utils.proxy import get_proxy_target
from pitivi.utils.proxy import ProxyingStrategy
from pitivi.utils.proxy import ProxyManager
@@ -115,59 +116,121 @@ for category, mime_types in SUPPORTED_FILE_FORMATS.items():
SUPPORTED_MIMETYPES.append(category + "/" + mime)
-class FileChooserExtraWidget(Gtk.Grid, Loggable):
+class FileChooserExtraWidget(Gtk.Box, Loggable):
+
def __init__(self, app):
Loggable.__init__(self)
- Gtk.Grid.__init__(self)
+ Gtk.Box.__init__(self, orientation=Gtk.Orientation.HORIZONTAL)
self.app = app
- self.set_row_spacing(SPACING)
- self.set_column_spacing(SPACING)
-
- self.__close_after = Gtk.CheckButton(label=_("Close after importing files"))
- self.__close_after.set_active(self.app.settings.closeImportDialog)
- self.attach(self.__close_after, 0, 0, 1, 2)
-
- self.__automatic_proxies = Gtk.RadioButton.new_with_label(
- None, _("Create proxies when the media format is not supported officially"))
- self.__automatic_proxies.set_tooltip_markup(
- _("Let Pitivi decide when to"
- " create proxy files and when not. The decision will be made"
- " depending on the file format, and how well it is supported."
- " For example H.264, FLAC files contained in QuickTime will"
- " not be proxied, but AAC, H.264 contained in MPEG-TS will.\n\n"
- "<i>This is the only option officially supported by the"
- " Pitivi developers and thus is the safest."
- "</i>"))
-
- self.__force_proxies = Gtk.RadioButton.new_with_label_from_widget(
- self.__automatic_proxies, _("Create proxies for all files"))
- self.__force_proxies.set_tooltip_markup(
- _("Use proxies for every imported file"
- " whatever its current media format is."))
- self.__no_proxies = Gtk.RadioButton.new_with_label_from_widget(
- self.__automatic_proxies, _("Do not use proxy files"))
-
- if self.app.settings.proxyingStrategy == ProxyingStrategy.ALL:
- self.__force_proxies.set_active(True)
- elif self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING:
- self.__no_proxies.set_active(True)
- else:
- self.__automatic_proxies.set_active(True)
+ self.__keep_open_check = Gtk.CheckButton(label=_("Keep dialog open"))
+ self.__keep_open_check.props.valign = Gtk.Align.START
+ self.__keep_open_check.set_tooltip_text(_("When importing files keep the dialog open"))
+ self.__keep_open_check.set_active(not self.app.settings.closeImportDialog)
+ self.pack_start(self.__keep_open_check, expand=False, fill=False, padding=0)
+
+ self.hq_proxy_check = Gtk.CheckButton.new()
+ # Translators: Create optimized media for unsupported files.
+ self.hq_proxy_check.set_label(_("Optimize:"))
+ self.hq_proxy_check.connect("toggled", self._hq_proxy_check_cb)
+
+ self.hq_combo = Gtk.ComboBoxText.new()
+ self.hq_combo.insert_text(0, _("Unsupported assets"))
+ self.hq_combo.insert_text(1, _("All"))
+ self.hq_combo.props.active = 0
+ self.hq_combo.set_sensitive(False)
+
+ self.help_button = Gtk.Button()
+ self.__update_help_button()
+ self.help_button.props.relief = Gtk.ReliefStyle.NONE
+ self.help_button.connect("clicked", self._help_button_clicked_cb)
+
+ self.scaled_proxy_check = Gtk.CheckButton.new()
+ self.__update_scaled_proxy_check()
+ self.scaled_proxy_check.connect("toggled", self._scaled_proxy_check_cb)
+
+ self.project_settings_label = Gtk.Label()
+ self.project_settings_label.set_markup("<a href='#'>%s</a>" % _("Project Settings"))
+ self.project_settings_label.connect("activate-link", self._target_res_cb)
+
+ proxy_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+
+ hq_proxy_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ hq_proxy_row.pack_start(self.hq_proxy_check, expand=False, fill=False, padding=0)
+ hq_proxy_row.pack_start(self.hq_combo, expand=False, fill=False, padding=PADDING)
+ hq_proxy_row.pack_start(self.help_button, expand=False, fill=False, padding=SPACING)
+ proxy_box.pack_start(hq_proxy_row, expand=False, fill=False, padding=0)
+
+ row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+ row.pack_start(self.scaled_proxy_check, expand=False, fill=False, padding=0)
+ row.pack_start(self.project_settings_label, expand=False, fill=False, padding=SPACING)
+ proxy_box.pack_start(row, expand=False, fill=False, padding=0)
+
+ self.pack_start(proxy_box, expand=False, fill=False, padding=SPACING * 2)
- self.attach(self.__automatic_proxies, 1, 0, 1, 1)
- self.attach(self.__force_proxies, 1, 1, 1, 1)
- self.attach(self.__no_proxies, 1, 2, 1, 1)
self.show_all()
+ size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.VERTICAL)
+ size_group.add_widget(self.__keep_open_check)
+ size_group.add_widget(hq_proxy_row)
+
+ if self.app.settings.proxyingStrategy == ProxyingStrategy.AUTOMATIC:
+ self.hq_proxy_check.set_active(True)
+ self.hq_combo.set_sensitive(True)
+ self.hq_combo.props.active = 0
+ elif self.app.settings.proxyingStrategy == ProxyingStrategy.ALL:
+ self.hq_proxy_check.set_active(True)
+ self.hq_combo.set_sensitive(True)
+ self.hq_combo.props.active = 1
+
+ if self.app.settings.auto_scaling_enabled:
+ self.scaled_proxy_check.set_active(True)
+
+ def _hq_proxy_check_cb(self, check_button):
+ active = check_button.get_active()
+ self.hq_combo.set_sensitive(active)
+ self.__update_help_button()
+
+ if not active:
+ self.scaled_proxy_check.set_active(False)
+
+ def __update_help_button(self):
+ if self.hq_proxy_check.get_active():
+ icon = "question-round-symbolic"
+ else:
+ icon = "warning-symbolic"
+ image = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON)
+ self.help_button.set_image(image)
+
+ def _help_button_clicked_cb(self, unused_button):
+ show_user_manual("importing")
+
+ def _scaled_proxy_check_cb(self, unused_button):
+ if self.scaled_proxy_check.get_active():
+ self.hq_proxy_check.set_active(True)
+
+ def _target_res_cb(self, label_widget, unused_uri):
+ self.app.gui.editor.showProjectSettingsDialog()
+ self.__update_scaled_proxy_check()
+
+ def __update_scaled_proxy_check(self):
+ target_width = self.app.project_manager.current_project.scaled_proxy_width
+ target_height = self.app.project_manager.current_project.scaled_proxy_height
+ self.scaled_proxy_check.set_label(_("Scale assets larger than %s×%s px.") % (target_width,
target_height))
+
def saveValues(self):
- self.app.settings.closeImportDialog = self.__close_after.get_active()
- if self.__force_proxies.get_active():
- self.app.settings.proxyingStrategy = ProxyingStrategy.ALL
- elif self.__no_proxies.get_active():
- self.app.settings.proxyingStrategy = ProxyingStrategy.NOTHING
+ self.app.settings.closeImportDialog = not self.__keep_open_check.get_active()
+
+ if self.hq_proxy_check.get_active():
+ if self.hq_combo.props.active == 0:
+ self.app.settings.proxyingStrategy = ProxyingStrategy.AUTOMATIC
+ else:
+ self.app.settings.proxyingStrategy = ProxyingStrategy.ALL
else:
- self.app.settings.proxyingStrategy = ProxyingStrategy.AUTOMATIC
+ assert not self.scaled_proxy_check.get_active()
+ self.app.settings.proxyingStrategy = ProxyingStrategy.NOTHING
+
+ self.app.settings.auto_scaling_enabled = self.scaled_proxy_check.get_active()
class AssetThumbnail(GObject.Object, Loggable):
@@ -184,6 +247,7 @@ class AssetThumbnail(GObject.Object, Loggable):
EMBLEMS = {}
PROXIED = "asset-proxied"
+ SCALED = "asset-scaled"
NO_PROXY = "no-proxy"
IN_PROGRESS = "asset-proxy-in-progress"
ASSET_PROXYING_ERROR = "asset-proxying-error"
@@ -193,7 +257,7 @@ class AssetThumbnail(GObject.Object, Loggable):
icons_by_name = {}
- for status in [PROXIED, IN_PROGRESS, ASSET_PROXYING_ERROR, UNSUPPORTED]:
+ for status in [PROXIED, SCALED, IN_PROGRESS, ASSET_PROXYING_ERROR, UNSUPPORTED]:
EMBLEMS[status] = GdkPixbuf.Pixbuf.new_from_file_at_size(
os.path.join(get_pixmap_dir(), "%s.svg" % status), 64, 64)
@@ -347,14 +411,17 @@ class AssetThumbnail(GObject.Object, Loggable):
icon = icon_theme.load_icon("dialog-question", size, 0)
return icon
- def __setState(self):
+ def _set_state(self):
asset = self.__asset
target = asset.get_proxy_target()
- asset_is_proxy = self.proxy_manager.is_proxy_asset(asset)
- if asset_is_proxy and target and not target.get_error():
- # The asset is a proxy.
+ target_is_valid = target and not target.get_error()
+ if self.proxy_manager.is_scaled_proxy(asset) and target_is_valid:
+ # The asset is a scaled proxy.
+ self.state = self.SCALED
+ elif self.proxy_manager.is_hq_proxy(asset) and target_is_valid:
+ # The asset is a HQ proxy.
self.state = self.PROXIED
- elif not asset_is_proxy and asset.proxying_error:
+ elif not self.proxy_manager.is_proxy_asset(asset) and asset.proxying_error:
self.state = self.ASSET_PROXYING_ERROR
elif self.proxy_manager.is_asset_queued(asset):
self.state = self.IN_PROGRESS
@@ -364,7 +431,7 @@ class AssetThumbnail(GObject.Object, Loggable):
self.state = self.NO_PROXY
def decorate(self):
- self.__setState()
+ self._set_state()
if self.state == self.NO_PROXY:
self.small_thumb = self.src_small
self.large_thumb = self.src_large
@@ -776,7 +843,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self.treeview_scrollwin.hide()
self.iconview_scrollwin.show_all()
- def __filter_unsupported(self, filter_info):
+ def _filter_unsupported(self, filter_info):
"""Returns whether the specified item should be displayed."""
if filter_info.mime_type not in SUPPORTED_MIMETYPES:
return False
@@ -811,7 +878,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
filter.set_name(_("Supported file formats"))
filter.add_custom(Gtk.FileFilterFlags.URI |
Gtk.FileFilterFlags.MIME_TYPE,
- self.__filter_unsupported)
+ self._filter_unsupported)
for formatter in GES.list_assets(GES.Formatter):
for extension in formatter.get_meta("extension").split(","):
if not extension:
@@ -956,7 +1023,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self._addAsset(asset)
if self._project.loaded:
- self.app.gui.editor.timeline_ui.switchProxies(asset)
+ self.app.gui.editor.timeline_ui.update_clips_asset(asset, proxy)
def _assetAddedCb(self, unused_project, asset):
"""Checks whether the asset added to the project should be shown."""
@@ -1236,14 +1303,22 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self.iconview.unselect_all()
def __stopUsingProxyCb(self, unused_action, unused_parameter):
- self._project.disable_proxies_for_assets(self.getSelectedAssets())
+ prefer_original = self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING
+ self._project.disable_proxies_for_assets(self.getSelectedAssets(),
+ hq_proxy=not prefer_original)
def __useProxiesCb(self, unused_action, unused_parameter):
self._project.use_proxies_for_assets(self.getSelectedAssets())
+ def __use_scaled_proxies_cb(self, unused_action, unused_parameter):
+ self._project.use_proxies_for_assets(self.getSelectedAssets(),
+ scaled=True)
+
def __deleteProxiesCb(self, unused_action, unused_parameter):
+ prefer_original = self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING
self._project.disable_proxies_for_assets(self.getSelectedAssets(),
- delete_proxy_file=True)
+ delete_proxy_file=True,
+ hq_proxy=not prefer_original)
def __open_containing_folder_cb(self, unused_action, unused_parameter):
assets = self.getSelectedAssets()
@@ -1293,15 +1368,63 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
proxies = [asset.get_proxy_target() for asset in assets
if self.app.proxy_manager.is_proxy_asset(asset)]
+ hq_proxies = [asset.get_proxy_target() for asset in assets
+ if self.app.proxy_manager.is_hq_proxy(asset)]
+ scaled_proxies = [asset.get_proxy_target() for asset in assets
+ if self.app.proxy_manager.is_scaled_proxy(asset)]
in_progress = [asset.creation_progress for asset in assets
if asset.creation_progress < 100]
- if proxies or in_progress:
+ if hq_proxies:
+ action = Gio.SimpleAction.new("unproxy-asset", None)
+ action.connect("activate", self.__stopUsingProxyCb)
+ action_group.insert(action)
+ text = ngettext("Do not use Optimised Proxy for selected asset",
+ "Do not use Optimised Proxies for selected assets",
+ len(proxies) + len(in_progress))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ action = Gio.SimpleAction.new("delete-proxies", None)
+ action.connect("activate", self.__deleteProxiesCb)
+ action_group.insert(action)
+
+ text = ngettext("Delete corresponding proxy file",
+ "Delete corresponding proxy files",
+ len(proxies) + len(in_progress))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ if in_progress:
action = Gio.SimpleAction.new("unproxy-asset", None)
action.connect("activate", self.__stopUsingProxyCb)
action_group.insert(action)
- text = ngettext("Do not use proxy for selected asset",
- "Do not use proxies for selected assets",
+ text = ngettext("Do not use Proxy for selected asset",
+ "Do not use Proxies for selected assets",
+ len(proxies) + len(in_progress))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ action = Gio.SimpleAction.new("delete-proxies", None)
+ action.connect("activate", self.__deleteProxiesCb)
+ action_group.insert(action)
+
+ text = ngettext("Delete corresponding proxy file",
+ "Delete corresponding proxy files",
+ len(proxies) + len(in_progress))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ if scaled_proxies:
+ action = Gio.SimpleAction.new("unproxy-asset", None)
+ action.connect("activate", self.__stopUsingProxyCb)
+ action_group.insert(action)
+ text = ngettext("Do not use Scaled Proxy for selected asset",
+ "Do not use Scaled Proxies for selected assets",
len(proxies) + len(in_progress))
menu_model.append(text, "assets.%s" %
@@ -1322,8 +1445,17 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
action = Gio.SimpleAction.new("use-proxies", None)
action.connect("activate", self.__useProxiesCb)
action_group.insert(action)
- text = ngettext("Use proxy for selected asset",
- "Use proxies for selected assets", len(assets))
+ text = ngettext("Use Optimised Proxy for selected asset",
+ "Use Optimised Proxies for selected assets", len(assets))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ action = Gio.SimpleAction.new("use-scaled-proxies", None)
+ action.connect("activate", self.__use_scaled_proxies_cb)
+ action_group.insert(action)
+ text = ngettext("Use Scaled Proxy for selected asset",
+ "Use Scaled Proxies for selected assets", len(assets))
menu_model.append(text, "assets.%s" %
action.get_name().replace(" ", "."))
diff --git a/pitivi/project.py b/pitivi/project.py
index b7da243a..2b63f24f 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -708,6 +708,9 @@ class Project(Loggable, GES.Project):
# Project property default values
self.register_meta(GES.MetaFlag.READWRITE, "author", "")
+ self.register_meta(GES.MetaFlag.READWRITE, "scaled_proxy_width", 0)
+ self.register_meta(GES.MetaFlag.READWRITE, "scaled_proxy_height", 0)
+
# The rendering settings.
self.set_meta("render-scale", 100.0)
@@ -805,6 +808,32 @@ class Project(Loggable, GES.Project):
return
self.set_meta("author", author)
+ @property
+ def scaled_proxy_height(self):
+ return self.get_meta("scaled_proxy_height") or self.app.settings.default_scaled_proxy_height
+
+ @scaled_proxy_height.setter
+ def scaled_proxy_height(self, scaled_proxy_height):
+ if scaled_proxy_height == self.get_meta("scaled_proxy_height"):
+ return
+ self.set_meta("scaled_proxy_height", scaled_proxy_height)
+ self.setModificationState(True)
+
+ @property
+ def scaled_proxy_width(self):
+ return self.get_meta("scaled_proxy_width") or self.app.settings.default_scaled_proxy_width
+
+ @scaled_proxy_width.setter
+ def scaled_proxy_width(self, scaled_proxy_width):
+ if scaled_proxy_width == self.get_meta("scaled_proxy_width"):
+ return
+ self.set_meta("scaled_proxy_width", scaled_proxy_width)
+ self.setModificationState(True)
+
+ def has_scaled_proxy_size(self):
+ """Returns whether the proxy size has been set."""
+ return bool(self.get_meta("scaled_proxy_width") and self.get_meta("scaled_proxy_height"))
+
@staticmethod
def get_thumb_path(uri, resolution):
"""Returns path of thumbnail of specified resolution in the cache."""
@@ -1123,9 +1152,22 @@ class Project(Loggable, GES.Project):
else:
# Check that we are not recreating deleted proxy
proxy_uri = self.app.proxy_manager.getProxyUri(asset)
+ scaled_proxy_uri = self.app.proxy_manager.getProxyUri(asset, scaled=True)
+
+ no_hq_proxy = False
+ no_scaled_proxy = False
+
if proxy_uri and proxy_uri not in self.__deleted_proxy_files and \
asset.props.id not in self.__awaited_deleted_proxy_targets:
+ no_hq_proxy = True
+
+ if scaled_proxy_uri and scaled_proxy_uri not in self.__deleted_proxy_files and \
+ asset.props.id not in self.__awaited_deleted_proxy_targets:
+ no_scaled_proxy = True
+
+ if no_hq_proxy and no_scaled_proxy:
asset.ready = True
+
num_loaded += 1
if all_ready:
@@ -1224,9 +1266,7 @@ class Project(Loggable, GES.Project):
asset.creation_progress = 100
asset.ready = True
if proxy:
- proxy.ready = False
- proxy.error = None
- proxy.creation_progress = 100
+ self.finalize_proxy(proxy)
asset.set_proxy(proxy)
try:
@@ -1240,6 +1280,11 @@ class Project(Loggable, GES.Project):
self.__updateAssetLoadingProgress()
+ def finalize_proxy(self, proxy):
+ proxy.ready = False
+ proxy.error = None
+ proxy.creation_progress = 100
+
# ------------------------------------------ #
# GES.Project virtual methods implementation #
# ------------------------------------------ #
@@ -1250,12 +1295,12 @@ class Project(Loggable, GES.Project):
self._prepare_asset_processing(asset)
- def __regenerate_missing_proxy(self, asset):
+ def __regenerate_missing_proxy(self, asset, scaled=False):
self.info("Re generating deleted proxy file %s.", asset.props.id)
GES.Asset.needs_reload(GES.UriClip, asset.props.id)
self._prepare_asset_processing(asset)
asset.force_proxying = True
- self.app.proxy_manager.add_job(asset)
+ self.app.proxy_manager.add_job(asset, scaled=scaled)
self.__updateAssetLoadingProgress()
def do_missing_uri(self, error, asset):
@@ -1271,7 +1316,8 @@ class Project(Loggable, GES.Project):
target = [asset for asset in self.list_assets(GES.UriClip) if
asset.props.id == target_uri]
if target:
- self.__regenerate_missing_proxy(target[0])
+ scaled = self.app.proxy_manager.is_scaled_proxy(asset)
+ self.__regenerate_missing_proxy(target[0], scaled=scaled)
else:
self.__awaited_deleted_proxy_targets.add(target_uri)
@@ -1515,12 +1561,19 @@ class Project(Loggable, GES.Project):
return GES.Project.save(self, ges_timeline, uri, formatter_asset, overwrite)
- def use_proxies_for_assets(self, assets):
+ def use_proxies_for_assets(self, assets, scaled=False):
+ proxy_manager = self.app.proxy_manager
originals = []
for asset in assets:
- if not self.app.proxy_manager.is_proxy_asset(asset):
+ if scaled:
+ is_proxied = proxy_manager.is_scaled_proxy(asset) and \
+ not proxy_manager.asset_matches_target_res(asset)
+ else:
+ is_proxied = proxy_manager.is_hq_proxy(asset)
+ if not is_proxied:
target = asset.get_proxy_target()
- if target and target.props.id == self.app.proxy_manager.getProxyUri(asset):
+ uri = proxy_manager.getProxyUri(asset, scaled=scaled)
+ if target and target.props.id == uri:
self.info("Missing proxy needs to be recreated after cancelling"
" its recreation")
target.unproxy(asset)
@@ -1536,22 +1589,43 @@ class Project(Loggable, GES.Project):
self.app.action_log.push(action)
self._prepare_asset_processing(asset)
asset.force_proxying = True
- self.app.proxy_manager.add_job(asset)
+ proxy_manager.add_job(asset, scaled)
- def disable_proxies_for_assets(self, assets, delete_proxy_file=False):
+ def disable_proxies_for_assets(self, assets, delete_proxy_file=False, hq_proxy=True):
+ proxy_manager = self.app.proxy_manager
for asset in assets:
- if self.app.proxy_manager.is_proxy_asset(asset):
+ if proxy_manager.is_proxy_asset(asset):
proxy_target = asset.get_proxy_target()
# The asset is a proxy for the proxy_target original asset.
self.debug("Stop proxying %s", proxy_target.props.id)
proxy_target.set_proxy(None)
+ if proxy_manager.is_scaled_proxy(asset) \
+ and not proxy_manager.isAssetFormatWellSupported(proxy_target) \
+ and hq_proxy:
+ # The original asset is unsupported, and the user prefers
+ # to edit with HQ proxies instead of scaled proxies.
+ self.use_proxies_for_assets([proxy_target])
self.remove_asset(asset)
proxy_target.force_proxying = False
if delete_proxy_file:
os.remove(Gst.uri_get_location(asset.props.id))
else:
# The asset is an original which is not being proxied.
- self.app.proxy_manager.cancel_job(asset)
+ proxy_manager.cancel_job(asset)
+
+ def regenerate_scaled_proxies(self):
+ assets = self.list_assets(GES.Extractable)
+ scaled_proxies = []
+ scaled_proxy_targets = []
+
+ for asset in assets:
+ if self.app.proxy_manager.is_scaled_proxy(asset):
+ scaled_proxies.append(asset)
+ scaled_proxy_targets.append(asset.get_proxy_target())
+
+ self.disable_proxies_for_assets(scaled_proxies, delete_proxy_file=True,
+ hq_proxy=False)
+ self.use_proxies_for_assets(scaled_proxy_targets, scaled=True)
def _commit(self):
"""Logs the operation and commits.
@@ -1980,7 +2054,6 @@ class ProjectSettingsDialog(object):
self.video_presets_combo = getObj("video_presets_combo")
self.constrain_sar_button = getObj("constrain_sar_button")
self.select_dar_radiobutton = getObj("select_dar_radiobutton")
- self.author_entry = getObj("author_entry")
self.year_spinbutton = getObj("year_spinbutton")
self.video_preset_menubutton = getObj("video_preset_menubutton")
@@ -1991,6 +2064,10 @@ class ProjectSettingsDialog(object):
self.audio_presets.setupUi(self.audio_presets_combo,
self.audio_preset_menubutton)
+ self.scaled_proxy_width_spin = getObj("scaled_proxy_width")
+ self.scaled_proxy_height_spin = getObj("scaled_proxy_height")
+ self.proxy_res_linked_check = getObj("proxy_res_linked")
+
def _setupUiConstraints(self):
"""Creates the dynamic widgets and connects other widgets."""
@@ -2002,7 +2079,6 @@ class ProjectSettingsDialog(object):
# Populate comboboxes.
self.frame_rate_combo.set_model(frame_rates)
-
self.channels_combo.set_model(audio_channels)
self.sample_rate_combo.set_model(audio_rates)
@@ -2026,14 +2102,26 @@ class ProjectSettingsDialog(object):
update_func_args=(self.video_presets,))
self.wg.addVertex(self.channels_combo, signal="changed")
self.wg.addVertex(self.sample_rate_combo, signal="changed")
+ self.wg.addVertex(self.scaled_proxy_width_spin, signal="value-changed")
+ self.wg.addVertex(self.scaled_proxy_height_spin, signal="value-changed")
# Constrain width and height IFF the Link checkbox is checked.
+ # Video
self.wg.addEdge(self.width_spinbutton, self.height_spinbutton,
predicate=self.widthHeightLinked,
edge_func=self.updateHeight)
self.wg.addEdge(self.height_spinbutton, self.width_spinbutton,
predicate=self.widthHeightLinked,
edge_func=self.updateWidth)
+ # Proxy
+ self.wg.addEdge(self.scaled_proxy_width_spin,
+ self.scaled_proxy_height_spin,
+ predicate=self.proxy_res_linked,
+ edge_func=self.update_scaled_proxy_height)
+ self.wg.addEdge(self.scaled_proxy_height_spin,
+ self.scaled_proxy_width_spin,
+ predicate=self.proxy_res_linked,
+ edge_func=self.update_scaled_proxy_width)
# Keep the framerate combo and fraction widgets in sync.
self.wg.addBiEdge(
@@ -2079,6 +2167,9 @@ class ProjectSettingsDialog(object):
def widthHeightLinked(self):
return self.constrain_sar_button.props.active and not self.video_presets.ignore_update_requests
+ def proxy_res_linked(self):
+ return self.proxy_res_linked_check.props.active
+
def _updateFraction(self, unused, fraction, combo):
fraction.setWidgetValue(get_combo_value(combo))
@@ -2114,6 +2205,23 @@ class ProjectSettingsDialog(object):
height = int(fraction.num / fraction.denom)
self.height_spinbutton.set_value(height)
+ def _proxy_res_linked_toggle_cb(self, unused_button):
+ width = int(self.scaled_proxy_width_spin.get_value())
+ height = int(self.scaled_proxy_height_spin.get_value())
+ self.proxy_aspect_ratio = Gst.Fraction(width, height)
+
+ def update_scaled_proxy_width(self):
+ height = int(self.scaled_proxy_height_spin.get_value())
+ fraction = height * self.proxy_aspect_ratio
+ width = int(fraction.num / fraction.denom)
+ self.scaled_proxy_width_spin.set_value(width)
+
+ def update_scaled_proxy_height(self):
+ width = int(self.scaled_proxy_width_spin.get_value())
+ fraction = width / self.proxy_aspect_ratio
+ height = int(fraction.num / fraction.denom)
+ self.scaled_proxy_height_spin.set_value(height)
+
def updateUI(self):
# Video
self.width_spinbutton.set_value(self.project.videowidth)
@@ -2140,6 +2248,9 @@ class ProjectSettingsDialog(object):
year = datetime.datetime.now().year
self.year_spinbutton.get_adjustment().set_value(year)
+ self.scaled_proxy_width_spin.set_value(self.project.scaled_proxy_width)
+ self.scaled_proxy_height_spin.set_value(self.project.scaled_proxy_height)
+
def updateProject(self):
with self.app.action_log.started("change project settings",
toplevel=True):
@@ -2153,6 +2264,17 @@ class ProjectSettingsDialog(object):
self.project.audiochannels = get_combo_value(self.channels_combo)
self.project.audiorate = get_combo_value(self.sample_rate_combo)
+ proxy_width = int(self.scaled_proxy_width_spin.get_value())
+ proxy_height = int(self.scaled_proxy_height_spin.get_value())
+ # Update scaled proxy meta-data and trigger proxy regen
+ if not self.project.has_scaled_proxy_size() or \
+ self.project.scaled_proxy_width != proxy_width or \
+ self.project.scaled_proxy_height != proxy_height:
+ self.project.scaled_proxy_width = proxy_width
+ self.project.scaled_proxy_height = proxy_height
+
+ self.project.regenerate_scaled_proxies()
+
def _responseCb(self, unused_widget, response):
"""Handles the dialog being closed."""
if response == Gtk.ResponseType.OK:
diff --git a/pitivi/render.py b/pitivi/render.py
index e3216473..fdb7184d 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -444,7 +444,7 @@ class RenderDialog(Loggable):
# the current container format.
self.preferred_vencoder = self.project.vencoder
self.preferred_aencoder = self.project.aencoder
- self.__unproxiedClips = {}
+ self.__replaced_assets = {}
self.frame_rate_combo.set_model(frame_rates)
self.channels_combo.set_model(audio_channels)
@@ -935,7 +935,7 @@ class RenderDialog(Loggable):
self._time_spent_paused = 0
self._pipeline.set_state(Gst.State.NULL)
self.project.set_rendering(False)
- self.__useProxyAssets()
+ self._use_proxy_assets()
self._disconnectFromGst()
self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)
self._pipeline.set_state(Gst.State.PAUSED)
@@ -983,50 +983,61 @@ class RenderDialog(Loggable):
except GLib.Error as e:
self.warning("GSound failed to play: %s", e)
- def __maybeUseSourceAsset(self):
- if self.__always_use_proxies.get_active():
- self.debug("Rendering from proxies, not replacing assets")
- return
-
- for layer in self.app.project_manager.current_project.ges_timeline.get_layers():
- for clip in layer.get_clips():
- if not isinstance(clip, GES.UriClip):
- continue
-
- asset = clip.get_asset()
- asset_target = asset.get_proxy_target()
- if not asset_target:
- # The asset is not a proxy.
- continue
-
- if self.__automatically_use_proxies.get_active():
- if not self.app.proxy_manager.isAssetFormatWellSupported(
- asset_target):
- self.info("Original asset %s format not well supported, "
- "rendering from proxy.",
- asset_target.props.id)
- continue
-
- self.info("Original asset %s format well supported, "
- "rendering from real asset.",
- asset_target.props.id)
+ def _asset_replacement(self, clip):
+ if not isinstance(clip, GES.UriClip):
+ return None
- if asset_target.get_error():
- # The original asset cannot be used.
- continue
+ asset = clip.get_asset()
+ asset_target = asset.get_proxy_target()
+ if not asset_target:
+ # The asset is not a proxy.
+ return None
- clip.set_asset(asset_target)
- self.info("Using original asset %s (instead of proxy %s)",
- asset_target.get_id(),
- asset.get_id())
- self.__unproxiedClips[clip] = asset
+ # Replace all proxies
+ if self.__never_use_proxies.get_active():
+ return asset_target
- def __useProxyAssets(self):
- for clip, asset in self.__unproxiedClips.items():
+ # Use HQ Proxy (or equivalent) only for unsupported assets
+ if self.__automatically_use_proxies.get_active():
+ if self.app.proxy_manager.isAssetFormatWellSupported(
+ asset_target):
+ return asset_target
+ else:
+ proxy_unsupported = True
+
+ # Use HQ Proxy (or equivalent) whenever available
+ if self.__always_use_proxies.get_active() or proxy_unsupported:
+ if self.app.proxy_manager.is_hq_proxy(asset):
+ return None
+
+ if self.app.proxy_manager.is_scaled_proxy(asset):
+ width, height = self.project.getVideoWidthAndHeight(render=True)
+ stream = asset.get_info().get_video_streams()[0]
+ asset_res = [stream.get_width(), stream.get_height()]
+
+ if asset_res[0] == width and asset_res[1] == height:
+ # Check whether the scaled proxy size matches the render size
+ # exactly. If the size is same, render from the scaled proxy
+ # to avoid double scaling.
+ return None
+
+ hq_proxy = GES.Asset.request(GES.UriClip,
+ self.app.proxy_manager.getProxyUri(asset_target))
+ return hq_proxy or None
+
+ def __replace_proxies(self):
+ for clip in self.project.ges_timeline.ui.clips():
+ asset = self._asset_replacement(clip)
+ if asset:
+ self.__replaced_assets[clip] = clip.get_asset()
+ clip.set_asset(asset)
+
+ def _use_proxy_assets(self):
+ for clip, asset in self.__replaced_assets.items():
self.info("Reverting to using proxy asset %s", asset)
clip.set_asset(asset)
- self.__unproxiedClips = {}
+ self.__replaced_assets = {}
# ------------------- Callbacks ------------------------------------------ #
@@ -1042,7 +1053,7 @@ class RenderDialog(Loggable):
def _renderButtonClickedCb(self, unused_button):
"""Starts the rendering process."""
- self.__maybeUseSourceAsset()
+ self.__replace_proxies()
self.outfile = os.path.join(self.filebutton.get_uri(),
self.fileentry.get_text())
self.progress = RenderingProgressDialog(self.app, self)
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 74f35366..4094bed8 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -554,7 +554,7 @@ class AssetPreviewer(Previewer, Loggable):
self.thumb_height = THUMB_HEIGHT
self.thumb_width = 0
- self.thumb_cache = ThumbnailCache.get(self.uri)
+ self.thumb_cache = ThumbnailCache.get(self.asset)
self.thumb_width, unused_height = self.thumb_cache.image_size
self.pipeline = None
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 01e0d47b..d285655c 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -43,6 +43,7 @@ from pitivi.timeline.previewers import Previewer
from pitivi.timeline.ruler import ScaleRuler
from pitivi.undo.timeline import CommitTimelineFinalizingAction
from pitivi.utils.loggable import Loggable
+from pitivi.utils.proxy import get_proxy_target
from pitivi.utils.timeline import EditingContext
from pitivi.utils.timeline import SELECT
from pitivi.utils.timeline import Selection
@@ -831,6 +832,11 @@ class Timeline(Gtk.EventBox, Zoomable, Loggable):
return clips.union(grouped_clips)
+ def clips(self):
+ for layer in self.ges_timeline.get_layers():
+ for clip in layer.get_clips():
+ yield clip
+
def _motion_notify_event_cb(self, unused_widget, event):
if self.draggingElement:
if type(self.draggingElement) == TransitionClip and \
@@ -1421,28 +1427,19 @@ class TimelineContainer(Gtk.Grid, Zoomable, Loggable):
# Public API
- def switchProxies(self, asset):
- proxy = asset.props.proxy
- unproxy = False
-
- if not proxy:
- unproxy = True
- proxy_uri = self.app.proxy_manager.getProxyUri(asset)
- proxy = GES.Asset.request(GES.UriClip,
- proxy_uri)
- if not proxy:
- self.debug("proxy_uri: %s does not have an asset associated",
- proxy_uri)
- return
+ def update_clips_asset(self, asset, proxy):
+ """Updates the relevant clips to use the asset or the proxy.
- layers = self.ges_timeline.get_layers()
- for layer in layers:
- for clip in layer.get_clips():
- if unproxy:
- if clip.get_asset() == proxy:
- clip.set_asset(asset)
- elif clip.get_asset() == proxy.get_proxy_target():
- clip.set_asset(proxy)
+ Args:
+ asset (GES.Asset): Only the clips who's current asset's target is
+ is this will be updated.
+ proxy (Ges.Asset): The proxy to use, or None to use the asset itself.
+ """
+ original_asset = get_proxy_target(asset)
+ replacement_asset = proxy or asset
+ for clip in self.timeline.clips():
+ if get_proxy_target(clip) == original_asset:
+ clip.set_asset(replacement_asset)
self._project.pipeline.commit_timeline()
def insertAssets(self, assets, position=None):
diff --git a/pitivi/utils/proxy.py b/pitivi/utils/proxy.py
index 0eae303e..976bbeb8 100644
--- a/pitivi/utils/proxy.py
+++ b/pitivi/utils/proxy.py
@@ -18,6 +18,8 @@
# Boston, MA 02110-1301, USA.
import os
import time
+from fractions import Fraction
+from gettext import gettext as _
from gi.repository import GES
from gi.repository import Gio
@@ -28,6 +30,7 @@ from gi.repository import GstPbutils
from gi.repository import GstTranscoder
from pitivi.configure import get_gstpresets_dir
+from pitivi.dialogs.prefs import PreferencesDialog
from pitivi.settings import GlobalSettings
from pitivi.utils.loggable import Loggable
@@ -46,16 +49,47 @@ GlobalSettings.addConfigOption('proxyingStrategy',
section='proxy',
key='proxying-strategy',
default=ProxyingStrategy.AUTOMATIC)
+
GlobalSettings.addConfigOption('numTranscodingJobs',
section='proxy',
key='num-proxying-jobs',
- default=4)
+ default=4,
+ notify=True)
+PreferencesDialog.addNumericPreference('numTranscodingJobs',
+ description="",
+ section="_proxies",
+ label=_("Max number of parallel transcoding jobs"),
+ lower=1)
+
GlobalSettings.addConfigOption("max_cpu_usage",
section="proxy",
key="max-cpu-usage",
- default=10)
+ default=10,
+ notify=True)
+PreferencesDialog.addNumericPreference('max_cpu_usage',
+ description="",
+ section="_proxies",
+ label=_("Max CPU usage dedicated to transcoding"),
+ lower=1,
+ upper=100)
+GlobalSettings.addConfigOption("auto_scaling_enabled",
+ section="proxy",
+ key="s-proxy-enabled",
+ default=False,
+ notify=True)
+GlobalSettings.addConfigOption("default_scaled_proxy_width",
+ section="proxy",
+ key="s-proxy-width",
+ default=1920,
+ notify=True)
+GlobalSettings.addConfigOption("default_scaled_proxy_height",
+ section="proxy",
+ key="s-proxy-height",
+ default=1080,
+ notify=True)
+
ENCODING_FORMAT_PRORES = "prores-raw-in-matroska.gep"
ENCODING_FORMAT_JPEG = "jpeg-raw-in-matroska.gep"
@@ -86,7 +120,7 @@ class ProxyManager(GObject.Object, Loggable):
}
WHITELIST_CONTAINER_CAPS = ["video/quicktime", "application/ogg", "application/xges",
- "video/x-matroska", "video/webm"]
+ "video/x-matroska", "video/webm", "image/jpeg"]
WHITELIST_AUDIO_CAPS = ["audio/mpeg", "audio/x-vorbis",
"audio/x-raw", "audio/x-flac",
"audio/x-wav"]
@@ -105,7 +139,10 @@ class ProxyManager(GObject.Object, Loggable):
a = GstPbutils.EncodingAudioProfile.new(Gst.Caps(audio), None, None, 0)
WHITELIST_FORMATS.append(a)
- proxy_extension = "proxy.mkv"
+ hq_proxy_extension = "proxy.mkv"
+ scaled_proxy_extension = "scaledproxy.mkv"
+ # Suffix for filenames of proxies being created.
+ part_suffix = ".part"
def __init__(self, app):
GObject.Object.__init__(self)
@@ -119,6 +156,9 @@ class ProxyManager(GObject.Object, Loggable):
self._start_proxying_time = 0
self.__running_transcoders = []
self.__pending_transcoders = []
+ # The scaled proxy transcoders waiting for their corresponding shadow
+ # HQ proxy transcoder to finish.
+ self.__waiting_transcoders = []
self.__encoding_target_file = None
self.proxyingUnsupported = False
@@ -135,6 +175,29 @@ class ProxyManager(GObject.Object, Loggable):
self.error("Not supporting any proxy formats!")
return
+ def _scale_asset_resolution(self, asset, max_width, max_height):
+ stream = asset.get_info().get_video_streams()[0]
+ width = stream.get_width()
+ height = stream.get_height()
+ aspect_ratio = Fraction(width, height)
+
+ if aspect_ratio.numerator >= width or aspect_ratio.denominator >= height:
+ self.log("Unscalable aspect ratio.")
+ return width, height
+ if aspect_ratio.numerator >= max_width or aspect_ratio.denominator >= max_height:
+ self.log("Cannot scale to target resolution.")
+ return width, height
+
+ if width > max_width or height > max_height:
+ width_factor = max_width // aspect_ratio.numerator
+ height_factor = max_height // aspect_ratio.denominator
+ scaling_factor = min(height_factor, width_factor)
+
+ width = aspect_ratio.numerator * scaling_factor
+ height = aspect_ratio.denominator * scaling_factor
+
+ return width, height
+
def _assetMatchesEncodingFormat(self, asset, encoding_profile):
def capsMatch(info, profile):
return not info.get_caps().intersect(profile.get_format()).is_empty()
@@ -168,7 +231,8 @@ class ProxyManager(GObject.Object, Loggable):
return False
return True
- def __getEncodingProfile(self, encoding_target_file, asset=None):
+ def __getEncodingProfile(self, encoding_target_file, asset=None, width=None,
+ height=None):
encoding_target = GstPbutils.EncodingTarget.load_from_file(
os.path.join(get_gstpresets_dir(), encoding_target_file))
encoding_profile = encoding_target.get_profile("default")
@@ -188,6 +252,10 @@ class ProxyManager(GObject.Object, Loggable):
Gst.ELEMENT_FACTORY_TYPE_ENCODER, Gst.Rank.MARGINAL),
profile_format, Gst.PadDirection.SRC, False):
return None
+ if height and width and profile.get_type_nick() == "video":
+ profile.set_restriction(Gst.Caps.from_string(
+ "video/x-raw, width=%d, height=%d" % (width, height)))
+
if not Gst.ElementFactory.list_filter(
Gst.ElementFactory.list_get_elements(
Gst.ELEMENT_FACTORY_TYPE_DECODER, Gst.Rank.MARGINAL),
@@ -216,12 +284,25 @@ class ProxyManager(GObject.Object, Loggable):
@classmethod
def is_proxy_asset(cls, obj):
+ return cls.is_scaled_proxy(obj) or cls.is_hq_proxy(obj)
+
+ @classmethod
+ def is_scaled_proxy(cls, obj):
if isinstance(obj, GES.Asset):
uri = obj.props.id
else:
uri = obj
- return uri.endswith("." + cls.proxy_extension)
+ return uri.endswith("." + cls.scaled_proxy_extension)
+
+ @classmethod
+ def is_hq_proxy(cls, obj):
+ if isinstance(obj, GES.Asset):
+ uri = obj.props.id
+ else:
+ uri = obj
+
+ return uri.endswith("." + cls.hq_proxy_extension)
def checkProxyLoadingSucceeded(self, proxy):
if self.is_proxy_asset(proxy):
@@ -237,9 +318,12 @@ class ProxyManager(GObject.Object, Loggable):
else:
uri = obj
+ if cls.is_scaled_proxy(uri):
+ return ".".join(uri.split(".")[:-4])
+
return ".".join(uri.split(".")[:-3])
- def getProxyUri(self, asset):
+ def getProxyUri(self, asset, scaled=False):
"""Returns the URI of a possible proxy file.
The name looks like:
@@ -255,8 +339,16 @@ class ProxyManager(GObject.Object, Loggable):
return None
else:
raise
-
- return "%s.%s.%s" % (asset.get_id(), file_size, self.proxy_extension)
+ if scaled:
+ max_w = self.app.project_manager.current_project.scaled_proxy_width
+ max_h = self.app.project_manager.current_project.scaled_proxy_height
+ t_width, t_height = self._scale_asset_resolution(asset, max_w, max_h)
+ proxy_res = "%sx%s" % (t_width, t_height)
+ return "%s.%s.%s.%s" % (asset.get_id(), file_size, proxy_res,
+ self.scaled_proxy_extension)
+ else:
+ return "%s.%s.%s" % (asset.get_id(), file_size,
+ self.hq_proxy_extension)
def isAssetFormatWellSupported(self, asset):
for encoding_format in self.WHITELIST_FORMATS:
@@ -266,7 +358,17 @@ class ProxyManager(GObject.Object, Loggable):
return False
- def __assetNeedsTranscoding(self, asset):
+ def asset_matches_target_res(self, asset):
+ stream = asset.get_info().get_video_streams()[0]
+
+ asset_res = (stream.get_width(), stream.get_height())
+ target_res = self._scale_asset_resolution(asset,
+ self.app.project_manager.current_project.scaled_proxy_width,
+ self.app.project_manager.current_project.scaled_proxy_height)
+
+ return asset_res == target_res
+
+ def __assetNeedsTranscoding(self, asset, scaled=False):
if self.proxyingUnsupported:
self.info("No proxying supported")
return False
@@ -280,7 +382,11 @@ class ProxyManager(GObject.Object, Loggable):
return False
if self.app.settings.proxyingStrategy == ProxyingStrategy.AUTOMATIC \
- and not self.is_proxy_asset(asset) and \
+ and scaled and not self.asset_matches_target_res(asset):
+ return True
+
+ if self.app.settings.proxyingStrategy == ProxyingStrategy.AUTOMATIC \
+ and not scaled and not self.is_hq_proxy(asset) and \
self.isAssetFormatWellSupported(asset):
return False
@@ -319,9 +425,12 @@ class ProxyManager(GObject.Object, Loggable):
return
+ shadow = transcoder and self._is_shadow_transcoder(transcoder)
+
if not transcoder:
if not self.__assetsMatch(asset, proxy):
- return self.__createTranscoder(asset)
+ self.__createTranscoder(asset)
+ return
else:
transcoder.props.pipeline.props.video_filter.finalize()
transcoder.props.pipeline.props.audio_filter.finalize()
@@ -338,30 +447,53 @@ class ProxyManager(GObject.Object, Loggable):
)
)
- self.emit("proxy-ready", asset, proxy)
- self.__emitProgress(proxy, 100)
+ if shadow:
+ self.app.project_manager.current_project.finalize_proxy(proxy)
+ else:
+ self.emit("proxy-ready", asset, proxy)
+ self.__emitProgress(proxy, 100)
def __transcoderErrorCb(self, transcoder, error, unused_details, asset):
self.emit("error-preparing-asset", asset, None, error)
def __transcoderDoneCb(self, transcoder, asset):
+ transcoder.disconnect_by_func(self.__proxyingPositionChangedCb)
transcoder.disconnect_by_func(self.__transcoderDoneCb)
transcoder.disconnect_by_func(self.__transcoderErrorCb)
- transcoder.disconnect_by_func(self.__proxyingPositionChangedCb)
self.debug("Transcoder done with %s", asset.get_id())
self.__running_transcoders.remove(transcoder)
- proxy_uri = self.getProxyUri(asset)
+ proxy_uri = transcoder.props.dest_uri.rstrip(ProxyManager.part_suffix)
os.rename(Gst.uri_get_location(transcoder.props.dest_uri),
Gst.uri_get_location(proxy_uri))
- # Make sure that if it first failed loading, the proxy is forced to be
- # reloaded in the GES cache.
- GES.Asset.needs_reload(GES.UriClip, proxy_uri)
- GES.Asset.request_async(GES.UriClip, proxy_uri, None,
- self.__assetLoadedCb, asset, transcoder)
+ shadow = self._is_shadow_transcoder(transcoder)
+ second_transcoder = self._get_second_transcoder(transcoder)
+ if second_transcoder and not shadow:
+ # second_transcoder is the shadow for transcoder.
+ # Defer loading until the shadow transcoder finishes.
+ self.__waiting_transcoders.append([transcoder, asset])
+ else:
+ # Make sure that if it first failed loading, the proxy is forced to
+ # be reloaded in the GES cache.
+ GES.Asset.needs_reload(GES.UriClip, proxy_uri)
+ GES.Asset.request_async(GES.UriClip, proxy_uri, None,
+ self.__assetLoadedCb, asset, transcoder)
+
+ if shadow:
+ # Finish deferred loading for waiting scaled proxy transcoder.
+ for pair in self.__waiting_transcoders:
+ waiting_transcoder, waiting_asset = pair
+ if waiting_transcoder.props.src_uri == transcoder.props.src_uri:
+ proxy_uri = waiting_transcoder.props.dest_uri.rstrip(ProxyManager.part_suffix)
+ GES.Asset.needs_reload(GES.UriClip, proxy_uri)
+ GES.Asset.request_async(GES.UriClip, proxy_uri, None,
+ self.__assetLoadedCb, waiting_asset, waiting_transcoder)
+
+ self.__waiting_transcoders.remove(pair)
+ break
try:
self.__startTranscoder(self.__pending_transcoders.pop())
@@ -389,6 +521,10 @@ class ProxyManager(GObject.Object, Loggable):
self.info("Position changed after job cancelled!")
return
+ second_transcoder = self._get_second_transcoder(transcoder)
+ if second_transcoder is not None:
+ position = (position + second_transcoder.props.position) // 2
+
self._transcoded_durations[asset] = position / Gst.SECOND
duration = transcoder.props.duration
@@ -397,37 +533,80 @@ class ProxyManager(GObject.Object, Loggable):
if duration > 0 and duration != Gst.CLOCK_TIME_NONE:
creation_progress = 100 * position / duration
# Do not set to >= 100 as we need to notify about the proxy first.
+
asset.creation_progress = max(0, min(creation_progress, 99))
self.__emitProgress(asset, asset.creation_progress)
- def is_asset_queued(self, asset):
+ def _get_second_transcoder(self, transcoder):
+ """Gets the shadow of a scaled proxy or the other way around."""
+ all_transcoders = self.__running_transcoders + self.__pending_transcoders
+ for t in all_transcoders:
+ if t.props.position_update_interval != transcoder.props.position_update_interval \
+ and t.props.src_uri == transcoder.props.src_uri:
+ return t
+ return None
+
+ def _is_shadow_transcoder(self, transcoder):
+ if transcoder.props.position_update_interval == 1001:
+ return True
+ return False
+
+ def is_asset_queued(self, asset, optimisation=True, scaling=True):
"""Returns whether the specified asset is queued for transcoding.
Args:
asset (GES.Asset): The asset to check.
+ optimisation(bool): Whether to check optimisation queue
+ scaling(bool): Whether to check scaling queue
Returns:
- bool: True iff the asset is being transcoded or pending.
+ bool: True if the asset is being transcoded or pending.
"""
all_transcoders = self.__running_transcoders + self.__pending_transcoders
+ is_queued = False
for transcoder in all_transcoders:
- if asset.props.id == transcoder.props.src_uri:
- return True
+ transcoder_uri = transcoder.props.dest_uri
+ scaling_ext = "." + self.scaled_proxy_extension + ProxyManager.part_suffix
+ optimisation_ext = "." + self.hq_proxy_extension + ProxyManager.part_suffix
- return False
+ scaling_transcoder = transcoder_uri.endswith(scaling_ext)
+ optimisation_transcoder = transcoder_uri.endswith(optimisation_ext)
+
+ if transcoder.props.src_uri == asset.props.id:
+ if optimisation and optimisation_transcoder:
+ is_queued = True
+ break
+
+ if scaling and scaling_transcoder:
+ is_queued = True
+ break
+
+ return is_queued
- def __createTranscoder(self, asset):
+ def __createTranscoder(self, asset, width=None, height=None, shadow=False):
self._total_time_to_transcode += asset.get_duration() / Gst.SECOND
asset_uri = asset.get_id()
- proxy_uri = self.getProxyUri(asset)
+
+ if width and height:
+ proxy_uri = self.getProxyUri(asset, scaled=True)
+ else:
+ proxy_uri = self.getProxyUri(asset)
dispatcher = GstTranscoder.TranscoderGMainContextSignalDispatcher.new()
- encoding_profile = self.__getEncodingProfile(self.__encoding_target_file, asset)
+
+ enc_profile = self.__getEncodingProfile(self.__encoding_target_file,
+ asset, width, height)
+
transcoder = GstTranscoder.Transcoder.new_full(
- asset_uri, proxy_uri + ".part", encoding_profile,
+ asset_uri, proxy_uri + ProxyManager.part_suffix, enc_profile,
dispatcher)
- transcoder.props.position_update_interval = 1000
+
+ if shadow:
+ # Used to identify shadow transcoder
+ transcoder.props.position_update_interval = 1001
+ else:
+ transcoder.props.position_update_interval = 1000
thumbnailbin = Gst.ElementFactory.make("teedthumbnailbin")
thumbnailbin.props.uri = asset.get_id()
@@ -446,6 +625,7 @@ class ProxyManager(GObject.Object, Loggable):
transcoder.connect("done", self.__transcoderDoneCb, asset)
transcoder.connect("error", self.__transcoderErrorCb, asset)
+
if len(self.__running_transcoders) < self.app.settings.numTranscodingJobs:
self.__startTranscoder(transcoder)
else:
@@ -467,7 +647,6 @@ class ProxyManager(GObject.Object, Loggable):
transcoder.__grefcount__)
self.__running_transcoders.remove(transcoder)
self.emit("asset-preparing-cancelled", asset)
- return
for transcoder in self.__pending_transcoders:
if asset.props.id == transcoder.props.src_uri:
@@ -478,39 +657,70 @@ class ProxyManager(GObject.Object, Loggable):
# here, which means it will be stopped.
self.__pending_transcoders.remove(transcoder)
self.emit("asset-preparing-cancelled", asset)
- return
- def add_job(self, asset):
+ return
+
+ def add_job(self, asset, scaled=False, shadow=False):
"""Adds a transcoding job for the specified asset if needed.
Args:
asset (GES.Asset): The asset to be transcoded.
"""
- if self.is_asset_queued(asset):
- self.log("Asset already queued for proxying: %s", asset)
- return
-
force_proxying = asset.force_proxying
- if not force_proxying and not self.__assetNeedsTranscoding(asset):
- self.debug("Not proxying asset (proxying disabled: %s)",
+ # Handle Automatic scaling
+ if self.app.settings.auto_scaling_enabled and not force_proxying \
+ and not shadow and not self.asset_matches_target_res(asset):
+ scaled = True
+
+ # Create shadow proxies for unsupported assets
+ if not self.isAssetFormatWellSupported(asset) and not \
+ self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING \
+ and not shadow:
+ hq_uri = self.app.proxy_manager.getProxyUri(asset)
+ if not Gio.File.new_for_uri(hq_uri).query_exists(None):
+ self.add_job(asset, shadow=True)
+
+ if scaled:
+ if self.is_asset_queued(asset, optimisation=False):
+ self.log("Asset already queued for scaling: %s", asset)
+ return
+
+ else:
+ if self.is_asset_queued(asset, scaling=False):
+ self.log("Asset already queued for optimization: %s", asset)
+ return
+
+ if not force_proxying:
+ if not self.__assetNeedsTranscoding(asset, scaled):
+ self.debug("Not proxying asset (proxying disabled: %s)",
self.proxyingUnsupported)
- # Make sure to notify we do not need a proxy for that asset.
- self.emit("proxy-ready", asset, None)
- return
+ # Make sure to notify we do not need a proxy for that asset.
+ self.emit("proxy-ready", asset, None)
+ return
- proxy_uri = self.getProxyUri(asset)
+ proxy_uri = self.getProxyUri(asset, scaled)
if Gio.File.new_for_uri(proxy_uri).query_exists(None):
self.debug("Using proxy already generated: %s", proxy_uri)
GES.Asset.request_async(GES.UriClip,
- proxy_uri, None,
- self.__assetLoadedCb, asset,
- None)
+ proxy_uri, None,
+ self.__assetLoadedCb, asset,
+ None)
return
- self.debug("Creating a proxy for %s (strategy: %s, force: %s)",
+ self.debug("Creating a proxy for %s (strategy: %s, force: %s, scaled: %s)",
asset.get_id(), self.app.settings.proxyingStrategy,
- force_proxying)
- self.__createTranscoder(asset)
+ force_proxying, scaled)
+ if scaled:
+ project = self.app.project_manager.current_project
+ w = project.scaled_proxy_width
+ h = project.scaled_proxy_height
+ if not project.has_scaled_proxy_size():
+ project.scaled_proxy_width = w
+ project.scaled_proxy_height = h
+ t_width, t_height = self._scale_asset_resolution(asset, w, h)
+ self.__createTranscoder(asset, width=t_width, height=t_height, shadow=shadow)
+ else:
+ self.__createTranscoder(asset, shadow=shadow)
return
diff --git a/pitivi/utils/timeline.py b/pitivi/utils/timeline.py
index ac66b679..5e1048dd 100644
--- a/pitivi/utils/timeline.py
+++ b/pitivi/utils/timeline.py
@@ -23,8 +23,6 @@ from gi.repository import Gst
from gi.repository import Gtk
from pitivi.utils.loggable import Loggable
-from pitivi.utils.ui import set_children_state_recurse
-from pitivi.utils.ui import unset_children_state_recurse
# Selection modes
@@ -129,8 +127,10 @@ class Selection(GObject.Object, Loggable):
obj.selected.selected = selected
if obj.ui:
if selected:
+ from pitivi.utils.ui import set_children_state_recurse
set_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
else:
+ from pitivi.utils.ui import unset_children_state_recurse
unset_children_state_recurse(obj.ui, Gtk.StateFlags.SELECTED)
for element in obj.get_children(False):
if isinstance(obj, GES.BaseEffect) or\
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index d6d8a756..f3566214 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -46,7 +46,6 @@ from pitivi.utils.loggable import doLog
from pitivi.utils.loggable import ERROR
from pitivi.utils.loggable import INFO
from pitivi.utils.misc import path_from_uri
-from pitivi.utils.proxy import get_proxy_target
# Dimensions in pixels
EXPANDED_SIZE = 65
@@ -427,6 +426,7 @@ def beautify_asset(asset):
Args:
asset (GES.Asset): The asset to display.
"""
+ from pitivi.utils.proxy import get_proxy_target
uri = get_proxy_target(asset).props.id
path = path_from_uri(uri)
res = ["<b>" + GLib.markup_escape_text(path) + "</b>"]
@@ -494,6 +494,7 @@ def info_name(info):
info (GES.Asset or DiscovererInfo): The info to display.
"""
if isinstance(info, GES.Asset):
+ from pitivi.utils.proxy import get_proxy_target
filename = urllib.parse.unquote(os.path.basename(get_proxy_target(info).get_id()))
elif isinstance(info, DiscovererInfo):
filename = urllib.parse.unquote(os.path.basename(info.get_uri()))
diff --git a/pitivi/utils/widgets.py b/pitivi/utils/widgets.py
index d42b2030..fc440f90 100644
--- a/pitivi/utils/widgets.py
+++ b/pitivi/utils/widgets.py
@@ -214,7 +214,7 @@ class NumericWidget(Gtk.Box, DynamicWidget):
lower (Optional[int]): The lower limit for this widget.
"""
- def __init__(self, upper=None, lower=None, default=None, adjustment=None):
+ def __init__(self, upper=None, lower=None, default=None, adjustment=None, width_chars=None):
Gtk.Box.__init__(self)
DynamicWidget.__init__(self, default)
@@ -239,6 +239,8 @@ class NumericWidget(Gtk.Box, DynamicWidget):
self.adjustment.props.upper = upper
self.spinner = Gtk.SpinButton(adjustment=self.adjustment)
+ if width_chars:
+ self.spinner.props.width_chars = width_chars
self.pack_start(self.spinner, expand=False, fill=False, padding=0)
self.spinner.show()
diff --git a/tests/test_media_library.py b/tests/test_media_library.py
index dc6c51fc..be4c3fbf 100644
--- a/tests/test_media_library.py
+++ b/tests/test_media_library.py
@@ -18,6 +18,7 @@
# Boston, MA 02110-1301, USA.
import os
import tempfile
+from unittest import mock
from gi.repository import GES
from gi.repository import Gst
@@ -83,13 +84,13 @@ class BaseTestMediaLibrary(common.TestCase):
len(self.samples))
self.mainloop.quit()
- def _createAssets(self, samples):
+ def _create_assets(self, samples):
self.samples = samples
for sample_name in samples:
self.app.project_manager.current_project.create_asset(
common.get_sample_uri(sample_name), GES.UriClip)
- def check_import(self, assets, proxying_strategy=ProxyingStrategy.ALL,
+ def check_import(self, samples, proxying_strategy=ProxyingStrategy.ALL,
check_no_transcoding=False):
self._customSetUp(proxyingStrategy=proxying_strategy,
numTranscodingJobs=4,
@@ -99,13 +100,103 @@ class BaseTestMediaLibrary(common.TestCase):
self.medialibrary._progressbar.connect(
"notify::fraction", self._progressBarCb)
- self._createAssets(assets)
+ self._create_assets(samples)
self.mainloop.run()
self.assertFalse(self.medialibrary._progressbar.props.visible)
+ def check_add_proxy(self, asset, scaled=False, w=160, h=120,
+ check_progress=True):
+ self.assertFalse(self.app.proxy_manager.is_proxy_asset(asset))
+
+ # Check the inital state of the asset, nothing should be going on.
+ self.assertNotIn("Proxy creation progress:",
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+ self.assertIn(
+ self.medialibrary.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
+ [medialibrary.AssetThumbnail.NO_PROXY,
+ medialibrary.AssetThumbnail.UNSUPPORTED])
+
+ # Check proxy creation.
+ was_in_progress = False
+
+ project = self.app.project_manager.current_project
+ project.scaled_proxy_width = w
+ project.scaled_proxy_height = h
+
+ def check_set_state(self):
+ old_set_state(self)
+ if self.state == self.IN_PROGRESS:
+ nonlocal was_in_progress
+ was_in_progress = True
+
+ old_set_state = medialibrary.AssetThumbnail._set_state
+ medialibrary.AssetThumbnail._set_state = check_set_state
+ try:
+ project.use_proxies_for_assets([asset], scaled)
+
+ self.assertIn("Proxy creation progress:",
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+
+ self.mainloop.run(timeout_seconds=10)
+ finally:
+ medialibrary.AssetThumbnail._set_state = old_set_state
+
+ if check_progress:
+ self.assertTrue(was_in_progress)
+
+ # Finally, check the final staus of the asset after proxying.
+ self.assertNotIn("Proxy creation progress:",
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+ if scaled:
+ self.assertEqual(
+ self.medialibrary.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
+ medialibrary.AssetThumbnail.SCALED)
+ else:
+ self.assertEqual(
+ self.medialibrary.storemodel[0][medialibrary.COL_THUMB_DECORATOR].state,
+ medialibrary.AssetThumbnail.PROXIED)
+
+ proxy = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+ stream = proxy.get_info().get_video_streams()[0]
+ resolution = [stream.get_width(), stream.get_height()]
+ self.assertEqual(proxy.props.proxy_target.props.id, asset.props.id)
+ if scaled:
+ self.assertEqual(resolution, [w, h])
+
+ return proxy
+
+ def check_disable_proxy(self, proxy, asset, delete=False):
+ self.assertFalse(self.app.proxy_manager.is_proxy_asset(asset))
+ self.assertTrue(self.app.proxy_manager.is_proxy_asset(proxy))
+
+ self.app.project_manager.current_project.disable_proxies_for_assets(
+ [proxy], delete_proxy_file=delete)
+
+ self.assertIsNone(asset.get_proxy())
+ self.assertEqual(self.medialibrary.storemodel[0][medialibrary.COL_URI],
+ asset.props.id)
+
+ self.assertEqual(os.path.exists(Gst.uri_get_location(proxy.props.id)),
+ not delete)
+
class TestMediaLibrary(BaseTestMediaLibrary):
+ def test_import_dialog_proxy_filter(self):
+ mock_filter = mock.Mock()
+ mock_filter.mime_type = "video/mp4"
+
+ self._customSetUp()
+ mlib = self.medialibrary
+
+ # Test HQ Proxies are filtered
+ mock_filter.uri = "file:///home/user/Videos/video.mp4.2360382.proxy.mkv"
+ self.assertFalse(mlib._filter_unsupported(mock_filter))
+
+ # Test Scaled Proxies are filtered
+ mock_filter.uri = "file:///home/user/Videos/video.mp4.2360382.300x300.scaledproxy.mkv"
+ self.assertFalse(mlib._filter_unsupported(mock_filter))
+
def stop_using_proxies(self, delete_proxies=False):
sample_name = "30fps_numeroted_frames_red.mkv"
self.check_import([sample_name])
@@ -204,7 +295,7 @@ class TestMediaLibrary(BaseTestMediaLibrary):
# Check that the info column notifies the user about progress
self.assertTrue("Proxy creation progress:" in
- self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
# Run the mainloop and let _progressBarCb stop it when the proxy is
# ready
@@ -213,6 +304,114 @@ class TestMediaLibrary(BaseTestMediaLibrary):
self.assertEqual(asset.creation_progress, 100)
self.assertEqual(asset.get_proxy(), proxy)
+ def test_create_and_delete_scaled_proxy(self):
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.NOTHING)
+ asset = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+
+ # Create scaled proxy
+ proxy = self.check_add_proxy(asset, scaled=True)
+
+ # Delete scaled proxy
+ self.check_disable_proxy(proxy, asset, delete=True)
+
+ def test_mixed_proxies(self):
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.NOTHING)
+ asset = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+
+ # Create and disable scaled proxy
+ proxy = self.check_add_proxy(asset, scaled=True)
+ scaled_uri = self.app.proxy_manager.getProxyUri(asset, scaled=True)
+ self.check_disable_proxy(proxy, asset)
+
+ # Create and disable HQ proxy
+ proxy = self.check_add_proxy(asset)
+ hq_uri = self.app.proxy_manager.getProxyUri(asset)
+ self.check_disable_proxy(proxy, asset)
+
+ # Check both files exist
+ self.assertTrue(os.path.exists(Gst.uri_get_location(hq_uri)))
+ self.assertTrue(os.path.exists(Gst.uri_get_location(scaled_uri)))
+
+ # Enable and delete scaled proxy
+ proxy = self.check_add_proxy(asset, scaled=True,
+ check_progress=False)
+ self.check_disable_proxy(proxy, asset, delete=True)
+
+ # Check that only HQ Proxy exists
+ self.assertFalse(os.path.exists(Gst.uri_get_location(scaled_uri)))
+ self.assertTrue(os.path.exists(Gst.uri_get_location(hq_uri)))
+
+ # Enable and delete HQ proxy
+ proxy = self.check_add_proxy(asset, check_progress=False)
+ self.check_disable_proxy(proxy, asset, delete=True)
+
+ def test_regenerate_scaled_proxy(self):
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.NOTHING)
+ asset = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+ asset_uri = common.get_sample_uri(sample_name)
+
+ # Create scaled proxy
+ proxy = self.check_add_proxy(asset, scaled=True)
+ proxy_uri = self.app.proxy_manager.getProxyUri(asset, scaled=True)
+
+ # Change target resolution and trigger regeneration (1/4 Asset width)
+ self.app.project_manager.current_project.scaled_proxy_width = 80
+ self.app.project_manager.current_project.scaled_proxy_height = 60
+
+ self.app.project_manager.current_project.regenerate_scaled_proxies()
+ self.assertTrue("Proxy creation progress:" in
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+ self.mainloop.run()
+
+ proxy = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+ self.assertNotEqual(proxy.props.id, proxy_uri)
+
+ stream = proxy.get_info().get_video_streams()[0]
+ resolution = [stream.get_width(), stream.get_height()]
+ self.assertEqual(resolution, [80, 60])
+ self.assertEqual(proxy.props.proxy_target.props.id, asset_uri)
+
+ # Delete proxy
+ self.check_disable_proxy(proxy, asset, delete=True)
+ self.assertFalse(os.path.exists(Gst.uri_get_location(proxy_uri)))
+
+ def test_scaled_proxy_for_unsupported_asset(self):
+ sample_name = "1sec_simpsons_trailer.mp4"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.AUTOMATIC)
+ asset = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+
+ # Mark all formats as unsupported
+ with mock.patch.object(self.app.proxy_manager,
+ "isAssetFormatWellSupported",
+ return_value=False):
+ # Create scaled proxy
+ proxy = self.check_add_proxy(asset, scaled=True, w=80, h=34)
+ proxy_uri = self.app.proxy_manager.getProxyUri(asset, scaled=True)
+ self.mainloop.run(until_empty=True)
+
+ # Check that HQ proxy was created
+ hq_uri = self.app.proxy_manager.getProxyUri(asset)
+ self.assertTrue(os.path.exists(Gst.uri_get_location(hq_uri)))
+
+ # Delete scaled proxy
+ self.check_disable_proxy(proxy, asset, delete=True)
+ self.mainloop.run()
+
+ # Check that we revert to HQ proxy
+ proxy = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+ proxy_uri = self.app.proxy_manager.getProxyUri(asset, scaled=False)
+ self.assertEqual(proxy.props.id, proxy_uri)
+
+ # Delete HQ Proxy
+ self.check_disable_proxy(proxy, asset, delete=True)
+
def test_supported_out_of_container_audio(self):
sample = "mp3_sample.mp3"
with common.cloned_sample(sample):
diff --git a/tests/test_prefs.py b/tests/test_prefs.py
index 17d568cc..913a3706 100644
--- a/tests/test_prefs.py
+++ b/tests/test_prefs.py
@@ -16,12 +16,26 @@
# License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
# Boston, MA 02110-1301, USA.
+from unittest import mock
+
+from gi.repository import Gtk
+
from pitivi.dialogs.prefs import PreferencesDialog
from tests import common
class PreferencesDialogTest(common.TestCase):
+ def test_dialog_creation(self):
+ """Exercises the dialog creation."""
+ app = common.create_pitivi()
+ with mock.patch.object(Gtk.Dialog, "set_transient_for"):
+ PreferencesDialog(app)
+
+ app.project_manager.new_blank_project()
+ with mock.patch.object(Gtk.Dialog, "set_transient_for"):
+ PreferencesDialog(app)
+
def testNumeric(self):
PreferencesDialog.addNumericPreference('numericPreference1',
label="Open Range",
diff --git a/tests/test_project.py b/tests/test_project.py
index 5bc58475..f45236fd 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -633,6 +633,40 @@ class TestProjectSettings(common.TestCase):
project.uri = "file:///tmp/%40%23%24%5E%26%60.xges"
self.assertEqual(project.name, "@#$^&`")
+ def test_scaled_proxy_size(self):
+ app = common.create_pitivi_mock(default_scaled_proxy_width=123,
+ default_scaled_proxy_height=456)
+ manager = ProjectManager(app)
+ project = manager.new_blank_project()
+ self.assertFalse(project.has_scaled_proxy_size())
+ self.assertEqual(project.scaled_proxy_width, 123)
+ self.assertEqual(project.scaled_proxy_height, 456)
+
+ with tempfile.NamedTemporaryFile() as f:
+ uri = Gst.filename_to_uri(f.name)
+ manager.saveProject(uri=uri, backup=False)
+ app2 = common.create_pitivi_mock(default_scaled_proxy_width=12,
+ default_scaled_proxy_height=45)
+ project2 = ProjectManager(app2).load_project(uri)
+ self.assertFalse(project2.has_scaled_proxy_size())
+ self.assertEqual(project2.scaled_proxy_width, 12)
+ self.assertEqual(project2.scaled_proxy_height, 45)
+
+ project.scaled_proxy_width = 123
+ project.scaled_proxy_height = 456
+ self.assertTrue(project.has_scaled_proxy_size())
+ self.assertEqual(project.scaled_proxy_width, 123)
+ self.assertEqual(project.scaled_proxy_height, 456)
+
+ with tempfile.NamedTemporaryFile() as f:
+ manager.saveProject(uri=uri, backup=False)
+ app2 = common.create_pitivi_mock(default_scaled_proxy_width=1,
+ default_scaled_proxy_height=4)
+ project2 = ProjectManager(app2).load_project(uri)
+ self.assertTrue(project2.has_scaled_proxy_size())
+ self.assertEqual(project2.scaled_proxy_width, 123)
+ self.assertEqual(project2.scaled_proxy_height, 456)
+
class TestExportSettings(common.TestCase):
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
new file mode 100644
index 00000000..be8f5c61
--- /dev/null
+++ b/tests/test_proxy.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# Pitivi video editor
+# Copyright (c) 2019, Yatin Maan <yatinmaan1 gmail com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+# Boston, MA 02110-1301, USA.
+"""Tests for the utils.proxy module."""
+# pylint: disable=protected-access,too-many-arguments
+from unittest import mock
+
+from gi.repository import GES
+
+from tests import common
+
+
+class TestProxyManager(common.TestCase):
+ """Tests for the ProxyManager class."""
+
+ def _check_scale_asset_resolution(self, asset_res, max_res, expected_res):
+ app = common.create_pitivi_mock()
+ manager = app.proxy_manager
+
+ stream = mock.Mock()
+ stream.get_width.return_value = asset_res[0]
+ stream.get_height.return_value = asset_res[1]
+
+ asset = mock.Mock()
+ asset.get_info().get_video_streams.return_value = [stream]
+
+ result = manager._scale_asset_resolution(asset, max_res[0], max_res[1])
+ self.assertEqual(result, expected_res)
+
+ def test_scale_asset_resolution(self):
+ """Checks the _scale_asset_resolution method."""
+ self._check_scale_asset_resolution((1920, 1080), (100, 100), (96, 54))
+ self._check_scale_asset_resolution((1080, 1920), (100, 100), (54, 96))
+ self._check_scale_asset_resolution((1000, 1000), (100, 100), (100, 100))
+
+ # Unscalable resolutions.
+ self._check_scale_asset_resolution((1000, 10), (100, 100), (1000, 10))
+ self._check_scale_asset_resolution((10, 1000), (100, 100), (10, 1000))
+ self._check_scale_asset_resolution((100, 100), (200, 200), (100, 100))
+
+ def _check_getTargetUri(self, proxy_uri, expected_uri):
+ app = common.create_pitivi_mock()
+ manager = app.proxy_manager
+
+ asset = mock.Mock(spec=GES.Asset)
+ asset.props.id = proxy_uri
+
+ result = manager.getTargetUri(asset)
+ self.assertEqual(result, expected_uri)
+
+ def test_getTargetUri(self):
+ """Checks the getTargetUri method."""
+ self._check_getTargetUri("file:///home/filename.ext.size.scaled_res.scaledproxy.mkv",
+ "file:///home/filename.ext")
+ self._check_getTargetUri("file:///home/filename.ext.size.proxy.mkv",
+ "file:///home/filename.ext")
+ self._check_getTargetUri("file:///home/file.name.mp4.1927006.1280x720.scaledproxy.mkv",
+ "file:///home/file.name.mp4")
+ self._check_getTargetUri("file:///home/file.name.mp4.1927006.proxy.mkv",
+ "file:///home/file.name.mp4")
+
+ def _check_getProxyUri(self, asset_uri, expected_uri, size=10, scaled=False, scaled_res=(1280, 720)):
+ app = common.create_pitivi_mock()
+ manager = app.proxy_manager
+
+ asset = mock.Mock()
+ asset.get_id.return_value = asset_uri
+ with mock.patch.object(manager, "_scale_asset_resolution") as s_res:
+ s_res.return_value = scaled_res
+ with mock.patch("pitivi.utils.proxy.Gio.File") as gio:
+ gio.new_for_uri.return_value = gio
+ gio.query_info().get_size.return_value = size
+
+ result = manager.getProxyUri(asset, scaled=scaled)
+ self.assertEqual(result, expected_uri)
+
+ def test_getProxyUri(self):
+ """Checks the getProxyUri method."""
+ self._check_getProxyUri("file:///home/file.name.mp4",
+ "file:///home/file.name.mp4.10.proxy.mkv")
+ self._check_getProxyUri("file:///home/file.name.mp4",
+ "file:///home/file.name.mp4.10.1280x720.scaledproxy.mkv",
+ scaled=True)
diff --git a/tests/test_render.py b/tests/test_render.py
index f7c9de70..ac3d93cd 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -33,6 +33,7 @@ from pitivi.preset import EncodingTargetManager
from pitivi.render import Encoders
from pitivi.render import extension_for_muxer
from pitivi.timeline.timeline import TimelineContainer
+from pitivi.utils.proxy import ProxyingStrategy
from pitivi.utils.ui import get_combo_value
from pitivi.utils.ui import set_combo_value
from tests import common
@@ -340,6 +341,91 @@ class TestRender(BaseTestMediaLibrary):
self.assertEqual(video_source.get_child_property("width")[1], 320)
self.assertEqual(video_source.get_child_property("height")[1], 240)
+ # pylint: disable=invalid-name
+ def test_rendering_with_scaled_proxies(self):
+ """Tests rendering with scaled proxies."""
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.NOTHING)
+
+ project = self.app.project_manager.current_project
+ proxy_manager = self.app.proxy_manager
+ timeline_container = TimelineContainer(self.app)
+ timeline_container.setProject(project)
+ rendering_asset = None
+
+ asset, = project.list_assets(GES.UriClip)
+ proxy = self.check_add_proxy(asset, scaled=True)
+
+ layer, = project.ges_timeline.get_layers()
+ clip = proxy.extract()
+ layer.add_clip(clip)
+
+ # Patch the function that reverts assets to proxies after rendering.
+ from pitivi.render import RenderDialog
+ old_use_proxy_assets = RenderDialog._use_proxy_assets
+
+ def check_use_proxy_assets(self):
+ nonlocal layer, asset, rendering_asset
+ clip, = layer.get_clips()
+ rendering_asset = clip.get_asset()
+ old_use_proxy_assets(self)
+
+ RenderDialog._use_proxy_assets = check_use_proxy_assets
+ dialog = self.create_rendering_dialog(project)
+ self.render(dialog)
+ self.mainloop.run(until_empty=True)
+ RenderDialog._use_proxy_assets = old_use_proxy_assets
+
+ # Check rendering did not use scaled proxy
+ self.assertFalse(proxy_manager.is_scaled_proxy(rendering_asset))
+ # Check asset was replaced with scaled proxy after rendering
+ self.assertTrue(proxy_manager.is_scaled_proxy(clip.get_asset()))
+
+ # pylint: disable=invalid-name
+ def test_rendering_with_unsupported_asset_scaled_proxies(self):
+ """Tests rendering with scaled proxies."""
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ with common.cloned_sample(sample_name):
+ self.check_import([sample_name], proxying_strategy=ProxyingStrategy.AUTOMATIC)
+
+ project = self.app.project_manager.current_project
+ proxy_manager = self.app.proxy_manager
+ timeline_container = TimelineContainer(self.app)
+ timeline_container.setProject(project)
+ rendering_asset = None
+
+ asset, = project.list_assets(GES.UriClip)
+ with mock.patch.object(proxy_manager,
+ "isAssetFormatWellSupported",
+ return_value=False):
+ proxy = self.check_add_proxy(asset, scaled=True)
+
+ # Check that HQ proxy was created
+ hq_uri = self.app.proxy_manager.getProxyUri(asset)
+ self.assertTrue(os.path.exists(Gst.uri_get_location(hq_uri)), hq_uri)
+
+ layer, = project.ges_timeline.get_layers()
+ clip = proxy.extract()
+ layer.add_clip(clip)
+
+ def _use_proxy_assets():
+ nonlocal layer, asset, rendering_asset
+ clip, = layer.get_clips()
+ rendering_asset = clip.get_asset()
+ old_use_proxy_assets()
+
+ dialog = self.create_rendering_dialog(project)
+ old_use_proxy_assets = dialog._use_proxy_assets
+ dialog._use_proxy_assets = _use_proxy_assets
+ self.render(dialog)
+ self.mainloop.run(until_empty=True)
+
+ # Check rendering used HQ proxy
+ self.assertTrue(proxy_manager.is_hq_proxy(rendering_asset))
+ # Check asset was replaced with scaled proxy after rendering
+ self.assertTrue(proxy_manager.is_scaled_proxy(clip.get_asset()))
+
@skipUnless(*encoding_target_exists("youtube"))
# pylint: disable=invalid-name
def test_rendering_with_youtube_profile(self):
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]