[pitivi] Implement proxy editing
- From: Thibault Saunier <tsaunier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [pitivi] Implement proxy editing
- Date: Mon, 18 Jan 2016 14:54:13 +0000 (UTC)
commit 0555da67a642e12544f33b6c3aae40e0b6839ae9
Author: Thibault Saunier <tsaunier gnome org>
Date: Fri Nov 6 22:47:09 2015 +0100
Implement proxy editing
And create proxies by default for all assets in a format we do not
consider as properly handled.
Make use of GstTranscoder to do the transcoding from the file format
in use to the intermediary format
Fixes https://phabricator.freedesktop.org/T3404
Fixes https://phabricator.freedesktop.org/T3405
Reviewed-by: Alex Băluț <alexandru balut gmail com>
Differential Revision: https://phabricator.freedesktop.org/D505
bin/pitivi-git-environment.sh | 53 ++-
build/xdg-app/pitivi.template.json | 39 ++-
configure.ac | 1 +
data/Makefile.am | 2 +-
data/gstpresets/GstJpegEnc.prs | 15 +
data/gstpresets/Makefile.am | 8 +
data/gstpresets/jpeg-flac-in-matroska.gep | 25 ++
data/gstpresets/prores-flac-in-matroska.gep | 24 ++
data/pixmaps/Makefile.am | 5 +-
data/pixmaps/asset-proxied.svg | 196 +++++++++
data/pixmaps/asset-proxy-in-progress.svg | 222 +++++++++++
data/pixmaps/asset-proxying-error.svg | 176 ++++++++
data/ui/renderingdialog.ui | 66 +++
data/videopresets/Makefile.am | 3 +-
pitivi/application.py | 2 +
pitivi/check.py | 2 +
pitivi/configure.py.in | 4 +
pitivi/mainwindow.py | 24 +-
pitivi/medialibrary.py | 572 ++++++++++++++++++++++-----
pitivi/project.py | 241 ++++++++++--
pitivi/render.py | 52 +++
pitivi/timeline/elements.py | 11 +-
pitivi/timeline/layer.py | 10 +-
pitivi/timeline/previewers.py | 293 +++++++++++---
pitivi/timeline/timeline.py | 24 ++
pitivi/utils/Makefile.am | 1 +
pitivi/utils/misc.py | 16 +
pitivi/utils/proxy.py | 426 ++++++++++++++++++++
pitivi/utils/ui.py | 20 +-
tests/Makefile.am | 2 +
tests/common.py | 34 ++-
tests/runtests.py | 13 +
tests/test_media_library.py | 189 +++++++++
tests/test_previewers.py | 63 +++
tests/test_project.py | 56 ++-
tests/test_timeline_timeline.py | 1 -
36 files changed, 2619 insertions(+), 272 deletions(-)
---
diff --git a/bin/pitivi-git-environment.sh b/bin/pitivi-git-environment.sh
index 5eb2a9c..1161249 100755
--- a/bin/pitivi-git-environment.sh
+++ b/bin/pitivi-git-environment.sh
@@ -85,7 +85,7 @@ EXTRA_PATH="$EXTRA_PATH:$PITIVI/gst-editing-services/tests/tools"
if pkg-config gstreamer-1.0 --atleast-version=$GST_MIN_VERSION --print-errors; then
MODULES="gst-editing-services gst-python"
else
- MODULES="gstreamer gst-plugins-base gst-plugins-good gst-plugins-ugly gst-plugins-bad gst-ffmpeg
gst-editing-services gst-python"
+ MODULES="gstreamer gst-plugins-base gst-plugins-good gst-plugins-ugly gst-plugins-bad gst-ffmpeg
gst-editing-services gst-python gst-transcoder"
EXTRA_PATH="$EXTRA_PATH:$PITIVI/gstreamer/tools"
EXTRA_PATH="$EXTRA_PATH:$PITIVI/gst-plugins-base/tools"
fi
@@ -143,7 +143,7 @@ else
export GST_VALIDATE_APPS_DIR=$GST_VALIDATE_APPS_DIR:$PITIVI/gst-editing-services/tests/validate/
export
GST_VALIDATE_SCENARIOS_PATH=$PITIVI/gst-devtools/validate/data/scenarios/:$GST_VALIDATE_SCENARIOS_PATH
export GST_VALIDATE_PLUGIN_PATH=$GST_VALIDATE_PLUGIN_PATH:$PITIVI/gst-devtools/validate/plugins/
- export
GST_ENCODING_TARGET_PATH=$GST_VALIDATE_PLUGIN_PATH:$PITIVI/gst-devtools/validate/data/encoding-profiles/
+ export GST_ENCODING_TARGET_PATH=$GST_VALIDATE_PLUGIN_PATH:$PITIVI/pitivi/data/encoding-profiles/
export PKG_CONFIG_PATH="$PITIVI/gstreamer/pkgconfig\
:$PITIVI/gst-plugins-base/pkgconfig\
@@ -180,6 +180,7 @@ $PITIVI/gstreamer/plugins\
:$PITIVI/libnice/gst\
:$PITIVI/gst-editing-services/plugins/nle/\
:$PITIVI/gst-editing-services/plugins/ges/\
+:$PITIVI/gst-transcoder/build/\
:${GST_PLUGIN_PATH:+:$GST_PLUGIN_PATH}"
export GST_PRESET_PATH="\
@@ -191,6 +192,8 @@ $PITIVI/gst-plugins-good/gst/equalizer/\
:$PITIVI/gst-plugins-ugly/ext/amrnb\
:$PITIVI/gst-plugins-bad/gst/freeverb\
:$PITIVI/gst-plugins-bad/ext/voamrwbenc\
+:$PITIVI/pitivi/data/videopresets/\
+:$PITIVI/pitivi/data/audiopresets/\
${GST_PRESET_PATH:+:$GST_PRESET_PATH}"
# don't use any system-installed plug-ins at all
@@ -217,6 +220,12 @@ export PATH=$PITIVI/gst-editing-services/tools:$PATH
GI_TYPELIB_PATH=$PITIVI/gst-editing-services/ges:$GI_TYPELIB_PATH
GI_TYPELIB_PATH=$PITIVI_PREFIX/share/gir-1.0:${GI_TYPELIB_PATH:+:$GI_TYPELIB_PATH}:/usr/lib64/girepository-1.0:/usr/lib/girepository-1.0
+# And anyway add GstTranscoder
+export LD_LIBRARY_PATH=$PITIVI/gst-transcoder/build/:$LD_LIBRARY_PATH
+export DYLD_LIBRARY_PATH=$PITIVI/gst-transcoder/build/:$DYLD_LIBRARY_PATH
+export PATH=$PITIVI/gst-transcoder/build/:$PATH
+GI_TYPELIB_PATH=$PITIVI/gst-transcoder/build/:$GI_TYPELIB_PATH
+
# And python
PYTHONPATH=$PYTHONPATH:$MYPITIVI/gst-python:$MYPITIVI/gst-editing-services/bindings/python
export LD_LIBRARY_PATH=$PITIVI/pygobject/gi/.libs:$LD_LIBRARY_PATH
@@ -381,7 +390,12 @@ if [ "$ready_to_run" != "1" ]; then
# If the folder doesn't exist, check out the module. Later on, we will
# update it anyway.
if test ! -d $m; then
- git clone git://anongit.freedesktop.org/gstreamer/$m
+ if [ "$m" == "gst-transcoder" ]; then
+ git clone https://github.com/thiblahute/gst-transcoder.git
+ else
+ git clone git://anongit.freedesktop.org/gstreamer/$m
+ fi
+
if [ $? -ne 0 ]; then
echo "Could not checkout $m ; result: $?"
exit 1
@@ -422,16 +436,31 @@ if [ "$ready_to_run" != "1" ]; then
git checkout -- acinclude.m4
fi
- if test ! -f ./configure || [ "$force_autogen" = "1" ]; then
- # Allow passing per-module arguments when running autogen.
- # For example, specify the following environment variable
- # to pass --disable-eglgles to gst-plugins-bad's autogen.sh:
- # gst_plugins_bad_AUTOGEN_EXTRA="--disable-eglgles"
- EXTRA_VAR="$(echo $m | sed "s/-/_/g")_AUTOGEN_EXTRA"
- if $BUILD_DOCS; then
- ./autogen.sh ${!EXTRA_VAR}
+ needs_configure="0"
+ if [ "$force_autogen" = "1" ]; then
+ needs_configure="1"
+ elif [ "$m" == "gst-transcoder" ]; then
+ if test ! -f build/build.ninja; then
+ needs_configure='1'
+ fi
+ elif test ! -f ./configure; then
+ needs_configure='1'
+ fi
+
+ if [ "$needs_configure" = "1" ]; then
+ if [ "$m" == "gst-transcoder" ]; then
+ ./configure
else
- ./autogen.sh --disable-gtk-doc --disable-docbook ${!EXTRA_VAR}
+ # Allow passing per-module arguments when running autogen.
+ # For example, specify the following environment variable
+ # to pass --disable-eglgles to gst-plugins-bad's autogen.sh:
+ # gst_plugins_bad_AUTOGEN_EXTRA="--disable-eglgles"
+ EXTRA_VAR="$(echo $m | sed "s/-/_/g")_AUTOGEN_EXTRA"
+ if $BUILD_DOCS; then
+ ./autogen.sh ${!EXTRA_VAR}
+ else
+ ./autogen.sh --disable-gtk-doc --disable-docbook ${!EXTRA_VAR}
+ fi
fi
if [ $? -ne 0 ]; then
echo "Could not run autogen for $m ; result: $?"
diff --git a/build/xdg-app/pitivi.template.json b/build/xdg-app/pitivi.template.json
index cc6cf15..89967f4 100644
--- a/build/xdg-app/pitivi.template.json
+++ b/build/xdg-app/pitivi.template.json
@@ -142,19 +142,6 @@
]
},
{
- "name": "ipdb",
- "build-options" : {
- "build-args": ["--share=network"]
- },
- "sources": [
- {
- "type": "file",
- "path": "ipdb-configure",
- "dest-filename": "configure"
- }
- ]
- },
- {
"name": "gstreamer",
"sources": [
{
@@ -238,6 +225,32 @@
]
},
{
+
+ "name": "meson",
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://github.com/mesonbuild/meson.git"
+
+ },
+ {
+ "type": "file",
+ "path": "meson-configure",
+ "dest-filename": "configure"
+ }
+ ]
+ },
+ {
+ "name": "gst-transcoder",
+ "config-opts": ["--libdir=lib"],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://github.com/thiblahute/gst-transcoder.git"
+ }
+ ]
+ },
+ {
"name": "pitivi",
"sources": [
{
diff --git a/configure.ac b/configure.ac
index efd90b6..1a93fe3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -161,4 +161,5 @@ data/ui/Makefile
data/renderpresets/Makefile
data/audiopresets/Makefile
data/videopresets/Makefile
+data/gstpresets/Makefile
)
diff --git a/data/Makefile.am b/data/Makefile.am
index 8f9d34e..57d77a6 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS=icons pixmaps ui renderpresets audiopresets videopresets
+SUBDIRS=icons pixmaps ui renderpresets audiopresets videopresets gstpresets
desktopdir = $(datadir)/applications
desktop_in_files = pitivi.desktop.in
diff --git a/data/gstpresets/GstJpegEnc.prs b/data/gstpresets/GstJpegEnc.prs
new file mode 100644
index 0000000..22894de
--- /dev/null
+++ b/data/gstpresets/GstJpegEnc.prs
@@ -0,0 +1,15 @@
+[_presets_]
+version=1.0
+element-name=GstJpegEnc
+
+[Quality Low]
+_meta/comment=Low quality
+quality=50
+
+[Quality Normal]
+_meta/comment=Normal quality
+quality=80
+
+[Quality High]
+_meta/comment=High quality
+quality=95
diff --git a/data/gstpresets/Makefile.am b/data/gstpresets/Makefile.am
new file mode 100644
index 0000000..0453ed7
--- /dev/null
+++ b/data/gstpresets/Makefile.am
@@ -0,0 +1,8 @@
+gstpresetsdir = $(pkgdatadir)/gstpresets
+gstpresets_DATA = \
+ GstJpegEnc.prs \
+ jpeg-flac-in-matroska.gep \
+ prores-flac-in-matroska.gep
+
+EXTRA_DIST = \
+ $(audiopresets_DATA)
diff --git a/data/gstpresets/jpeg-flac-in-matroska.gep b/data/gstpresets/jpeg-flac-in-matroska.gep
new file mode 100644
index 0000000..a6a99e9
--- /dev/null
+++ b/data/gstpresets/jpeg-flac-in-matroska.gep
@@ -0,0 +1,25 @@
+[GStreamer Encoding Target]
+name=matroska
+category=device
+description=Standard config for jpeg and FLAC in matroska
+
+[profile-default]
+name=default
+type=container
+description[c]=Matroska muxer with default configs
+format=video/x-matroska
+
+[streamprofile-flac]
+parent=default
+type=audio
+format=audio/x-flac
+presence=0
+
+[streamprofile-jpeg]
+parent=default
+type=video
+format=image/jpeg
+presence=0
+pass=0
+variableframerate=false
+preset=Quality High
diff --git a/data/gstpresets/prores-flac-in-matroska.gep b/data/gstpresets/prores-flac-in-matroska.gep
new file mode 100644
index 0000000..be5884f
--- /dev/null
+++ b/data/gstpresets/prores-flac-in-matroska.gep
@@ -0,0 +1,24 @@
+[GStreamer Encoding Target]
+name=matroskaproresflac
+category=device
+description=Standard config for prores and FLAC in matroska
+
+[profile-default]
+name=default
+type=container
+description[c]=Matroska muxer with default configs
+format=video/x-matroska
+
+[streamprofile-flac]
+parent=default
+type=audio
+format=audio/x-flac
+presence=0
+
+[streamprofile-prores]
+parent=default
+type=video
+format=video/x-prores
+presence=0
+pass=0
+variableframerate=false
diff --git a/data/pixmaps/Makefile.am b/data/pixmaps/Makefile.am
index eb8beac..e2ee623 100644
--- a/data/pixmaps/Makefile.am
+++ b/data/pixmaps/Makefile.am
@@ -18,7 +18,10 @@ pixmap_DATA = \
processing-clip.png \
processing-clip.svg \
trimbar-focused.png \
- trimbar-normal.png
+ trimbar-normal.png \
+ asset-proxied.svg \
+ asset-proxying-error.svg \
+ asset-proxy-in-progress.svg
effectspixmapdir = $(pkgdatadir)/pixmaps/effects
effectspixmap_DATA = \
diff --git a/data/pixmaps/asset-proxied.svg b/data/pixmaps/asset-proxied.svg
new file mode 100644
index 0000000..b4876fc
--- /dev/null
+++ b/data/pixmaps/asset-proxied.svg
@@ -0,0 +1,196 @@
+<?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="svg5243"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="exported - proxy status - ready.svg">
+ <defs
+ id="defs5245">
+ <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,660.16152,-337.56481)"
+ 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="3.959798"
+ inkscape:cx="17.271176"
+ inkscape:cy="31.063057"
+ 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" />
+ <metadata
+ id="metadata5248">
+ <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 />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-697.95764,-656.53235)">
+ <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="697.95764"
+ y="656.53235" />
+ <g
+ style="display:inline;fill:#000000"
+ id="g4199-0"
+ transform="matrix(1.3502829,0,0,1.3502829,704.35019,716.59025)">
+ <g
+ style="display:inline;fill:#000000"
+ id="layer9-6"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="status" />
+ <g
+ id="layer10-7"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="devices"
+ style="fill:#000000" />
+ <g
+ id="layer11-9"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="apps"
+ style="fill:#000000" />
+ <g
+ id="layer13-62"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="places"
+ style="fill:#000000" />
+ <g
+ id="layer14-3"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="mimetypes"
+ style="fill:#000000" />
+ <g
+ style="display:inline;fill:#000000"
+ id="layer15-38"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="emblems" />
+ <g
+ style="display:inline;fill:#000000"
+ id="g71291-8"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="emotes" />
+ <g
+ style="display:inline;fill:#000000"
+ id="g4953-8"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="categories" />
+ <g
+ style="display:inline;fill:#000000"
+ id="layer12-49"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="actions">
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3;marker:none;enable-background:accumulate"
+ id="path8913-6-7-1-5-5"
+ d="M 72.9375,790.9375 68,795.875 l -1.9375,-1.9375 -2.125,2.125 3,3 1.0625,1.0625 1.0625,-1.0625
6,-6 -2.125,-2.125 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ style="display:inline"
+ id="g4199"
+ transform="matrix(1.3502829,0,0,1.3502829,704.35029,715.84813)">
+ <g
+ style="display:inline"
+ id="layer9"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="status" />
+ <g
+ id="layer10"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="devices" />
+ <g
+ id="layer11"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="apps" />
+ <g
+ id="layer13"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="places" />
+ <g
+ id="layer14"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="mimetypes" />
+ <g
+ style="display:inline"
+ id="layer15"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="emblems" />
+ <g
+ style="display:inline"
+ id="g71291"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="emotes" />
+ <g
+ style="display:inline"
+ id="g4953"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="categories" />
+ <g
+ style="display:inline"
+ id="layer12"
+ transform="translate(-61.000665,-787)"
+ inkscape:label="actions">
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:3;marker:none;enable-background:accumulate"
+ id="path8913-6-7-1-5"
+ d="M 72.9375,790.9375 68,795.875 l -1.9375,-1.9375 -2.125,2.125 3,3 1.0625,1.0625 1.0625,-1.0625
6,-6 -2.125,-2.125 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/data/pixmaps/asset-proxy-in-progress.svg b/data/pixmaps/asset-proxy-in-progress.svg
new file mode 100644
index 0000000..20ef74d
--- /dev/null
+++ b/data/pixmaps/asset-proxy-in-progress.svg
@@ -0,0 +1,222 @@
+<?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.91 r13725"
+ sodipodi:docname="exported - proxy status - processing.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,74.447237,-200.42196)"
+ 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="3.959798"
+ inkscape:cx="41.425845"
+ inkscape:cy="34.711907"
+ 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" />
+ <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 />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Calque 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-112.24338,-793.67523)">
+ <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="112.24338"
+ y="793.67523" />
+ <g
+ style="display:inline;fill:#000000"
+ id="g4312-6"
+ transform="matrix(0.92575,0,0,0.92578125,123.19146,857.56885)">
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="status"
+ id="layer9-5-5" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="devices"
+ id="layer10-5-5" />
+ <g
+ transform="translate(-501.0002,-381)"
+ inkscape:label="apps"
+ id="layer11-1-4"
+ style="fill:#000000" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="places"
+ id="layer13-5-1" />
+ <g
+ transform="translate(-501.0002,-381)"
+ inkscape:label="mimetypes"
+ id="layer14-76-5"
+ style="fill:#000000" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="emblems"
+ id="layer15-3-1">
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccccccccc"
+ id="path4597-1-7"
+ d="m 515.90195,383.0005 c -0.0423,0.008 -0.0841,0.0181 -0.125,0.0312 -0.44715,0.10014
-0.79228,0.5419 -0.78125,1 l 0,1.6875 c 0.004,1.31255 0.004,1.31255 -1.5625,1.3125 l -1.4375,0 c
-0.52358,5e-5 -0.99995,0.47642 -1,1 -0.008,0.0726 -0.008,0.14613 0,0.21875 l 0,0.78125 6,0 0,-1 0,-4 c
0.006,-0.0623 0.006,-0.12518 0,-0.1875 l 0,-0.8125 -0.8125,0 c -0.0916,-0.0236 -0.18665,-0.0342
-0.28125,-0.0312 z"
+ inkscape:connector-curvature="0" />
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccccccccc"
+ id="path10913-9"
+ d="m 501.0047,389 0,1 0,4 c -0.006,0.0623 -0.006,0.12518 0,0.1875 l 0,0.8125 0.8125,0 c
0.0916,0.0236 0.18665,0.0342 0.28125,0.0312 0.0423,-0.008 0.0841,-0.0181 0.125,-0.0312 0.44715,-0.10014
0.79228,-0.5419 0.78125,-1 l 0,-1.6875 C 503.00029,391 503.00029,391 504.5672,391 l 1.4375,0 c 0.52358,-5e-5
0.99995,-0.47642 1,-1 0.008,-0.0726 0.008,-0.14613 0,-0.21875 l 0,-0.78125 z"
+ inkscape:connector-curvature="0" />
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.33333325;marker:none;enable-background:accumulate"
+ id="path1483-0"
+ d="m 509.0002,382 c -3.15321,0 -5.81948,2.12571 -6.6875,5 l 2.09375,0 c 0.7734,-1.76501
2.53819,-3 4.59375,-3 2.05556,0 3.82035,1.23499 4.59375,3 l 2.09375,0 c -0.86802,-2.87429 -3.53429,-5
-6.6875,-5 z m -6.6875,9 c 0.86802,2.87429 3.53429,5 6.6875,5 3.15321,0 5.81948,-2.12571 6.6875,-5 l
-2.09375,0 c -0.7734,1.76501 -2.53819,3 -4.59375,3 -2.05556,0 -3.82035,-1.23499 -4.59375,-3 l -2.09375,0 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="emotes"
+ id="g71291-9-3" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="categories"
+ id="g4953-0-8" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline;fill:#000000"
+ inkscape:label="actions"
+ id="layer12-4-9" />
+ </g>
+ <g
+ style="display:inline"
+ id="g4312"
+ transform="matrix(0.92578125,0,0,0.92578125,122.70336,857.03883)">
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="status"
+ id="layer9-5" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="devices"
+ id="layer10-5" />
+ <g
+ transform="translate(-501.0002,-381)"
+ inkscape:label="apps"
+ id="layer11-1" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="places"
+ id="layer13-5" />
+ <g
+ transform="translate(-501.0002,-381)"
+ inkscape:label="mimetypes"
+ id="layer14-76" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="emblems"
+ id="layer15-3">
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccccccccc"
+ id="path4597-1"
+ d="m 515.90195,383.0005 c -0.0423,0.008 -0.0841,0.0181 -0.125,0.0312 -0.44715,0.10014
-0.79228,0.5419 -0.78125,1 l 0,1.6875 c 0.004,1.31255 0.004,1.31255 -1.5625,1.3125 l -1.4375,0 c
-0.52358,5e-5 -0.99995,0.47642 -1,1 -0.008,0.0726 -0.008,0.14613 0,0.21875 l 0,0.78125 6,0 0,-1 0,-4 c
0.006,-0.0623 0.006,-0.12518 0,-0.1875 l 0,-0.8125 -0.8125,0 c -0.0916,-0.0236 -0.18665,-0.0342
-0.28125,-0.0312 z"
+ inkscape:connector-curvature="0" />
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccccccccccc"
+ id="path10913"
+ d="m 501.0047,389 0,1 0,4 c -0.006,0.0623 -0.006,0.12518 0,0.1875 l 0,0.8125 0.8125,0 c
0.0916,0.0236 0.18665,0.0342 0.28125,0.0312 0.0423,-0.008 0.0841,-0.0181 0.125,-0.0312 0.44715,-0.10014
0.79228,-0.5419 0.78125,-1 l 0,-1.6875 C 503.00029,391 503.00029,391 504.5672,391 l 1.4375,0 c 0.52358,-5e-5
0.99995,-0.47642 1,-1 0.008,-0.0726 0.008,-0.14613 0,-0.21875 l 0,-0.78125 z"
+ inkscape:connector-curvature="0" />
+ <path
+
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:Sans;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:2.33333325;marker:none;enable-background:accumulate"
+ id="path1483"
+ d="m 509.0002,382 c -3.15321,0 -5.81948,2.12571 -6.6875,5 l 2.09375,0 c 0.7734,-1.76501
2.53819,-3 4.59375,-3 2.05556,0 3.82035,1.23499 4.59375,3 l 2.09375,0 c -0.86802,-2.87429 -3.53429,-5
-6.6875,-5 z m -6.6875,9 c 0.86802,2.87429 3.53429,5 6.6875,5 3.15321,0 5.81948,-2.12571 6.6875,-5 l
-2.09375,0 c -0.7734,1.76501 -2.53819,3 -4.59375,3 -2.05556,0 -3.82035,-1.23499 -4.59375,-3 l -2.09375,0 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="emotes"
+ id="g71291-9" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="categories"
+ id="g4953-0" />
+ <g
+ transform="translate(-501.0002,-381)"
+ style="display:inline"
+ inkscape:label="actions"
+ id="layer12-4" />
+ </g>
+ </g>
+</svg>
diff --git a/data/pixmaps/asset-proxying-error.svg b/data/pixmaps/asset-proxying-error.svg
new file mode 100644
index 0000000..db11549
--- /dev/null
+++ b/data/pixmaps/asset-proxying-error.svg
@@ -0,0 +1,176 @@
+<?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.91 r13725"
+ sodipodi:docname="exported - proxy status - error.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="3.959798"
+ inkscape:cx="45.745908"
+ inkscape:cy="71.284479"
+ 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" />
+ <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 />
+ </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" />
+ <g
+ id="g4282-4"
+ transform="matrix(1.8699342,0,0,1.8699342,111.37055,886.54896)"
+ style="display:inline;fill:#000000;fill-opacity:1">
+ <g
+ style="display:inline;fill:#000000;fill-opacity:1"
+ id="layer9-2-8"
+ transform="translate(-60,-518)" />
+ <g
+ id="layer10-3-7"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1" />
+ <g
+ id="layer11-8-6"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1" />
+ <g
+ id="layer12-0-9"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1">
+ <g
+ style="display:inline;fill:#000000;fill-opacity:1"
+ id="layer4-4-1-4"
+ transform="translate(19,-242)">
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale
Mono';-inkscape-font-specification:'Andale
Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
+ id="path10839-9-9"
+ d="m 45,764 1,0 c 0.01037,-1.2e-4 0.02079,-4.6e-4 0.03125,0 0.254951,0.0112 0.50987,0.12858
0.6875,0.3125 L 49,766.59375 51.3125,764.3125 C 51.578125,764.082 51.759172,764.007 52,764 l 1,0 0,1 c
0,0.28647 -0.03434,0.55065 -0.25,0.75 l -2.28125,2.28125 2.25,2.25 C 52.906938,770.46942 52.999992,770.7347
53,771 l 0,1 -1,0 c -0.265301,-10e-6 -0.530586,-0.0931 -0.71875,-0.28125 L 49,769.4375 46.71875,771.71875 C
46.530586,771.90694 46.26529,772 46,772 l -1,0 0,-1 c -3e-6,-0.26529 0.09306,-0.53058 0.28125,-0.71875 l
2.28125,-2.25 L 45.28125,765.75 C 45.070508,765.55537 44.97809,765.28075 45,765 l 0,-1 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ id="layer13-7-6"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1" />
+ <g
+ id="layer14-7-6"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1" />
+ <g
+ id="layer15-1-5"
+ transform="translate(-60,-518)"
+ style="fill:#000000;fill-opacity:1" />
+ </g>
+ <g
+ id="g4282"
+ transform="matrix(1.8699342,0,0,1.8699342,110.87089,886.04866)"
+ style="display:inline;fill:#f57900;fill-opacity:1">
+ <g
+ style="display:inline;fill:#f57900;fill-opacity:1"
+ id="layer9-2"
+ transform="translate(-60,-518)" />
+ <g
+ id="layer10-3"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1" />
+ <g
+ id="layer11-8"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1" />
+ <g
+ id="layer12-0"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1">
+ <g
+ style="display:inline;fill:#f57900;fill-opacity:1"
+ id="layer4-4-1"
+ transform="translate(19,-242)">
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale
Mono';-inkscape-font-specification:'Andale
Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#f57900;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
+ id="path10839-9"
+ d="m 45,764 1,0 c 0.01037,-1.2e-4 0.02079,-4.6e-4 0.03125,0 0.254951,0.0112 0.50987,0.12858
0.6875,0.3125 L 49,766.59375 51.3125,764.3125 C 51.578125,764.082 51.759172,764.007 52,764 l 1,0 0,1 c
0,0.28647 -0.03434,0.55065 -0.25,0.75 l -2.28125,2.28125 2.25,2.25 C 52.906938,770.46942 52.999992,770.7347
53,771 l 0,1 -1,0 c -0.265301,-10e-6 -0.530586,-0.0931 -0.71875,-0.28125 L 49,769.4375 46.71875,771.71875 C
46.530586,771.90694 46.26529,772 46,772 l -1,0 0,-1 c -3e-6,-0.26529 0.09306,-0.53058 0.28125,-0.71875 l
2.28125,-2.25 L 45.28125,765.75 C 45.070508,765.55537 44.97809,765.28075 45,765 l 0,-1 z"
+ inkscape:connector-curvature="0" />
+ </g>
+ </g>
+ <g
+ id="layer13-7"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1" />
+ <g
+ id="layer14-7"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1" />
+ <g
+ id="layer15-1"
+ transform="translate(-60,-518)"
+ style="fill:#f57900;fill-opacity:1" />
+ </g>
+ </g>
+</svg>
diff --git a/data/ui/renderingdialog.ui b/data/ui/renderingdialog.ui
index da5301c..2a76dc5 100644
--- a/data/ui/renderingdialog.ui
+++ b/data/ui/renderingdialog.ui
@@ -744,6 +744,72 @@
<property name="top_attach">1</property>
</packing>
</child>
+ <child>
+ <object class="GtkBox" id="box5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRadioButton" id="automatically_use_proxies">
+ <property name="label" translatable="yes">Automatically render from proxy
files</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_markup" translatable="yes">Use proxy files if they are available
and the source asset media format is not officially supported.
+
+This option is a good trade of between quality of the rendered video and stability.</property>
+ <property name="xalign">0.05000000074505806</property>
+ <property name="yalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="always_use_proxies">
+ <property name="label" translatable="yes">Always render from proxy files</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_markup" translatable="yes">Render all proxied clips from the
proxy assets. There might be some quality loss during the rendering process.</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="never_use_proxies">
+ <property name="label" translatable="yes">Never render from proxy files</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="tooltip_markup" translatable="yes">Always use source assets for
rendering. It is the best choice for the quality of the redered video , but you might hit some bugs because
of the use of not officially supported media formats.
+<i>Use at your own risk!</i></property>
+ <property name="xalign">0</property>
+ <property name="yalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="expand">False</property>
diff --git a/data/videopresets/Makefile.am b/data/videopresets/Makefile.am
index 67adc30..61b7430 100644
--- a/data/videopresets/Makefile.am
+++ b/data/videopresets/Makefile.am
@@ -7,4 +7,5 @@ videopresets_DATA = \
iPod.json
EXTRA_DIST = \
- $(videopresets_DATA)
+ $(videopresets_DATA) \
+ $(gstvideopresets_DATA)
diff --git a/pitivi/application.py b/pitivi/application.py
index 458a0ea..ca6517b 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -42,6 +42,7 @@ from pitivi.utils.threads import ThreadMaster
from pitivi.utils import loggable
from pitivi.utils.loggable import Loggable
from pitivi.utils.misc import quote_uri, path_from_uri
+from pitivi.utils.proxy import ProxyManager
from pitivi.utils.system import getSystem
from pitivi.utils.timeline import Zoomable
@@ -135,6 +136,7 @@ class Pitivi(Gtk.Application, Loggable):
self.settings = GlobalSettings()
self.threads = ThreadMaster()
self.effects = EffectsManager()
+ self.proxy_manager = ProxyManager(self)
self.system = getSystem()
self.action_log.connect("commit", self._actionLogCommit)
diff --git a/pitivi/check.py b/pitivi/check.py
index 99ca9ef..8503e09 100644
--- a/pitivi/check.py
+++ b/pitivi/check.py
@@ -337,6 +337,7 @@ def initialize_modules():
require_version("Gst", GST_API_VERSION)
require_version("GstController", GST_API_VERSION)
from gi.repository import Gst
+ from pitivi.configure import get_audiopresets_dir, get_videopresets_dir
Gst.init(None)
require_version("GES", GST_API_VERSION)
@@ -377,6 +378,7 @@ HARD_DEPENDENCIES = [GICheck("3.14.0"),
CairoDependency("1.10.0"),
GstDependency("Gst", GST_API_VERSION, "1.6.0"),
GstDependency("GES", GST_API_VERSION, "1.6.0.0"),
+ GIDependency("GstTranscoder", GST_API_VERSION),
GtkDependency("Gtk", GTK_API_VERSION, "3.10.0"),
ClassicDependency("numpy"),
GIDependency("Gio", "2.0"),
diff --git a/pitivi/configure.py.in b/pitivi/configure.py.in
index 93675ac..e2ec898 100644
--- a/pitivi/configure.py.in
+++ b/pitivi/configure.py.in
@@ -93,3 +93,7 @@ def get_audiopresets_dir():
def get_videopresets_dir():
""" Returns the directory for Video Presets files """
return os.path.join(get_data_dir(), 'videopresets')
+
+def get_gstpresets_dir():
+ """ Returns the directory for Video Presets files """
+ return os.path.join(get_data_dir(), 'gstpresets')
diff --git a/pitivi/mainwindow.py b/pitivi/mainwindow.py
index e26a8b3..21e7715 100644
--- a/pitivi/mainwindow.py
+++ b/pitivi/mainwindow.py
@@ -999,17 +999,19 @@ class PitiviMainWindow(Gtk.ApplicationWindow, Loggable):
False)
else:
dialog.hide()
- # Reset the project manager and disconnect all the signals.
- self.app.project_manager.newBlankProject(
- ignore_unsaved_changes=True)
- # Signal the project loading failure.
- # You have to do this *after* successfully creating a blank project,
- # or the startupwizard will still be connected to that signal too.
- reason = _('No replacement file was provided for "<i>%s</i>".\n\n'
- 'Pitivi does not currently support partial projects.'
- % info_name(asset))
- self.app.project_manager.emit(
- "new-project-failed", project.uri, reason)
+
+ if self.app.proxy_manager.checkProxyLoadingSucceeded(asset):
+ # Reset the project manager and disconnect all the signals.
+ self.app.project_manager.newBlankProject(
+ ignore_unsaved_changes=True)
+ # Signal the project loading failure.
+ # You have to do this *after* successfully creating a blank project,
+ # or the startupwizard will still be connected to that signal too.
+ reason = _('No replacement file was provided for "<i>%s</i>".\n\n'
+ 'Pitivi does not currently support partial projects.'
+ % info_name(asset))
+ self.app.project_manager.emit(
+ "new-project-failed", project.uri, reason)
dialog.destroy()
return new_uri
diff --git a/pitivi/medialibrary.py b/pitivi/medialibrary.py
index 5f0058e..a1d9594 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -48,10 +48,14 @@ from pitivi.dialogs.clipmediaprops import ClipMediaPropsDialog
from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
from pitivi.mediafilespreviewer import PreviewWidget
from pitivi.settings import GlobalSettings
+from pitivi.timeline.previewers import getThumbnailCache
from pitivi.utils.loggable import Loggable
-from pitivi.utils.misc import PathWalker, quote_uri, path_from_uri
-from pitivi.utils.ui import beautify_info, beautify_length, info_name, \
- URI_TARGET_ENTRY, FILE_TARGET_ENTRY, SPACING
+from pitivi.utils.misc import PathWalker, quote_uri, path_from_uri,\
+ get_proxy_target, disconnectAllByFunc
+from pitivi.utils.proxy import ProxyingStrategy
+from pitivi.utils.ui import beautify_asset, beautify_length, info_name, \
+ URI_TARGET_ENTRY, FILE_TARGET_ENTRY, SPACING, \
+ beautify_ETA, PADDING
# Values used in the settings file.
SHOW_TREEVIEW = 1
@@ -75,7 +79,7 @@ GlobalSettings.addConfigOption('lastClipView',
STORE_MODEL_STRUCTURE = (
GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
- str, object, str, str, str)
+ str, object, str, str, str, object)
(COL_ICON_64,
COL_ICON_128,
@@ -83,7 +87,8 @@ STORE_MODEL_STRUCTURE = (
COL_ASSET,
COL_URI,
COL_LENGTH,
- COL_SEARCH_TEXT) = list(range(len(STORE_MODEL_STRUCTURE)))
+ COL_SEARCH_TEXT,
+ COL_THUMB_DECORATOR) = list(range(len(STORE_MODEL_STRUCTURE)))
# This whitelist is made from personal knowledge of file extensions in the wild,
# from gst-inspect |grep demux,
@@ -91,14 +96,139 @@ STORE_MODEL_STRUCTURE = (
# http://en.wikipedia.org/wiki/List_of_file_formats#Video
# ...and looking at the contents of /usr/share/mime
SUPPORTED_FILE_FORMATS = {
- "video": ("3gpp", "3gpp2", "dv", "mp2t", "mp4", "mpeg", "ogg", "quicktime", "webm", "x-flv",
"x-matroska", "x-mng", "x-ms-asf", "x-msvideo", "x-ms-wmp", "x-ms-wmv", "x-ogm+ogg", "x-theora+ogg"),
+ "video": ("3gpp", "3gpp2", "dv", "mp2t", "mp4", "mpeg", "ogg", "quicktime", "webm", "x-flv",
"x-matroska", "x-mng", "x-ms-asf", "x-msvideo", "x-ms-wmp", "x-ms-wmv", "x-ogm+ogg", "x-theora+ogg", "mp2t"),
# noqa
"application": ("mxf",),
# Don't forget audio formats
- "audio": ("aac", "ac3", "basic", "flac", "mp2", "mp4", "mpeg", "ogg", "opus", "webm", "x-adpcm",
"x-aifc", "x-aiff", "x-aiffc", "x-ape", "x-flac+ogg", "x-m4b", "x-matroska", "x-ms-asx", "x-ms-wma",
"x-speex", "x-speex+ogg", "x-vorbis+ogg", "x-wav"),
+ "audio": ("aac", "ac3", "basic", "flac", "mp2", "mp4", "mpeg", "ogg", "opus", "webm", "x-adpcm",
"x-aifc", "x-aiff", "x-aiffc", "x-ape", "x-flac+ogg", "x-m4b", "x-matroska", "x-ms-asx", "x-ms-wma",
"x-speex", "x-speex+ogg", "x-vorbis+ogg", "x-wav"), # noqa
# ...and image formats
"image": ("jp2", "jpeg", "png", "svg+xml")}
-# Stuff that we're not too confident about but might improve eventually:
-OTHER_KNOWN_FORMATS = ("video/mp2t",)
+
+SUPPORTED_MIMETYPES = []
+for category, mime_types in SUPPORTED_FILE_FORMATS.items():
+ for mime in mime_types:
+ SUPPORTED_MIMETYPES.append(category + "/" + mime)
+
+
+class FileChooserExtraWidget(Gtk.Grid, Loggable):
+ def __init__(self, app):
+ Loggable.__init__(self)
+ Gtk.Grid.__init__(self)
+ 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 H264, FLAC files contained in Quicktime will"
+ " not be proxied, but AAC, H264 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.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()
+
+ 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
+ else:
+ self.app.settings.proxyingStrategy = ProxyingStrategy.AUTOMATIC
+
+
+class ThumbnailsDecorator(Loggable):
+ EMBLEMS = {}
+ PROXIED = "asset-proxied"
+ NO_PROXY = "no-proxy"
+ IN_PROGRESS = "asset-proxy-in-progress"
+ ASSET_PROXYING_ERROR = "asset-proxying-error"
+
+ DEFAULT_ALPHA = 255
+
+ for status in [PROXIED, IN_PROGRESS, ASSET_PROXYING_ERROR]:
+ EMBLEMS[status] = []
+ for size in [32, 64]:
+ EMBLEMS[status].append(GdkPixbuf.Pixbuf.new_from_file_at_size(
+ os.path.join(get_pixmap_dir(), "%s.svg" % status), size, size))
+
+ def __init__(self, thumbs, asset):
+ Loggable.__init__(self)
+ self.src_64 = thumbs[0]
+ self.src_128 = thumbs[1]
+
+ self.__asset = asset
+ self.decorate()
+
+ def __setState(self):
+ asset = self.__asset
+ target = asset.get_proxy_target()
+ if target and not target.get_error():
+ self.state = self.PROXIED
+ elif asset.proxying_error:
+ self.state = self.ASSET_PROXYING_ERROR
+ elif not asset.creation_progress == 100:
+ self.state = self.IN_PROGRESS
+ else:
+ self.state = self.NO_PROXY
+
+ def decorate(self):
+ self.__setState()
+ if self.state == self.NO_PROXY:
+ self.thumb_64 = self.src_64
+ self.thumb_128 = self.src_128
+ return
+
+ self.thumb_64 = self.src_64.copy()
+ self.thumb_128 = self.src_128.copy()
+ for i, thumb in enumerate([self.thumb_64, self.thumb_128]):
+ emblems = self.EMBLEMS[self.state]
+ src = emblems[i]
+
+ # We need to set dest_y == offset_y for the source image
+ # not to be cropped, that API is weird.
+ if thumb.get_height() < src.get_height():
+ src = src.copy()
+ src = src.scale_simple(src.get_width(),
+ thumb.get_height(),
+ GdkPixbuf.InterpType.BILINEAR)
+
+ src.composite(thumb, dest_x=0,
+ dest_y=thumb.get_height() - src.get_height(),
+ dest_width=src.get_width(),
+ dest_height=src.get_height(),
+ offset_x=0,
+ offset_y=thumb.get_height() - src.get_height(),
+ scale_x=1.0, scale_y=1.0,
+ interp_type=GdkPixbuf.InterpType.BILINEAR,
+ overall_alpha=self.DEFAULT_ALPHA)
class MediaLibraryWidget(Gtk.Box, Loggable):
@@ -125,7 +255,8 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
if self.clip_view not in (SHOW_TREEVIEW, SHOW_ICONVIEW):
self.clip_view = SHOW_ICONVIEW
self.import_start_time = time.time()
- self._last_imported_uris = []
+ self._last_imported_uris = set()
+ self.__last_proxying_estimate_time = _("Unknown")
self.set_orientation(Gtk.Orientation.VERTICAL)
builder = Gtk.Builder()
@@ -156,6 +287,8 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
# Prefer to sort the media library elements by URI
# rather than show them randomly.
self.storemodel.set_sort_column_id(COL_URI, Gtk.SortType.ASCENDING)
+ self.storemodel.connect("row-deleted", self.__updateViewCb)
+ self.storemodel.connect("row-inserted", self.__updateViewCb)
# Scrolled Windows
self.treeview_scrollwin = Gtk.ScrolledWindow()
@@ -212,7 +345,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
namecol.set_sizing(Gtk.TreeViewColumnSizing.GROW_ONLY)
namecol.set_min_width(150)
txtcell = Gtk.CellRendererText()
- txtcell.set_property("ellipsize", Pango.EllipsizeMode.END)
+ txtcell.set_property("ellipsize", Pango.EllipsizeMode.START)
namecol.pack_start(txtcell, True)
namecol.add_attribute(txtcell, "markup", COL_INFOTEXT)
@@ -251,7 +384,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
cell.props.yalign = 0.0
cell.props.xpad = 0
cell.props.ypad = 0
- cell.set_property("ellipsize", Pango.EllipsizeMode.END)
+ cell.set_property("ellipsize", Pango.EllipsizeMode.START)
self.iconview.pack_start(cell, False)
self.iconview.add_attribute(cell, "markup", COL_SEARCH_TEXT)
@@ -313,6 +446,22 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self.thumbnailer = MediaLibraryWidget._getThumbnailer()
+ def finalize(self):
+ if not self._project:
+ self.debug("No project set...")
+ return
+
+ self.debug("Finalizing %s", self)
+ for asset in self._project.list_assets(GES.Extractable):
+ disconnectAllByFunc(asset, self.__assetProxiedCb)
+ disconnectAllByFunc(asset, self.__assetProxyingCb)
+
+ self.__disconnectFromProject()
+
+ self.app.project_manager.disconnect_by_func(self._newProjectCreatedCb)
+ self.app.project_manager.disconnect_by_func(self._newProjectLoadedCb)
+ self.app.project_manager.disconnect_by_func(self._newProjectFailedCb)
+
@staticmethod
def _getThumbnailer():
if "GnomeDesktop" in missing_soft_deps:
@@ -358,6 +507,12 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
view.connect("drag-begin", self._dndDragBeginCb)
view.connect("drag-end", self._dndDragEndCb)
+ def __updateViewCb(self, unused_model, unused_path, unused_iter=None):
+ if not len(self.storemodel):
+ self._welcome_infobar.show_all()
+ else:
+ self._welcome_infobar.hide()
+
def _importSourcesCb(self, unused_action):
self._showImportSourcesDialog()
@@ -442,17 +597,12 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
Connect signal handlers to a project.
"""
project.connect("asset-added", self._assetAddedCb)
+ project.connect("asset-loading-progress", self._assetLoadingProgressCb)
project.connect("asset-removed", self._assetRemovedCb)
project.connect("error-loading-asset", self._errorCreatingAssetCb)
- project.connect("done-importing", self._sourcesStoppedImportingCb)
- project.connect("start-importing", self._sourcesStartedImportingCb)
+ project.connect("proxying-error", self._proxyingErrorCb)
project.connect("settings-set-from-imported-asset", self.__projectSettingsSetFromImportedAssetCb)
- # The start-importing signal would have already been emited at that
- # time, make sure to catch if it is the case
- if project.nb_remaining_file_to_import > 0:
- self._sourcesStartedImportingCb(project)
-
def _setClipView(self, view_type):
"""
Set which clip view to use when medialibrary is showing clips.
@@ -476,8 +626,24 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
self.treeview_scrollwin.hide()
self.iconview_scrollwin.show_all()
- if not len(self.storemodel):
- self._welcome_infobar.show_all()
+ def __filterProxies(self, filter_info):
+ if filter_info.mime_type not in SUPPORTED_MIMETYPES:
+ return False
+
+ if filter_info.uri.endswith(".proxy.mkv"):
+ return False
+
+ source_uri, size = os.path.splitext(filter_info.uri.replace(
+ ".proxy.mkv", ""))
+ if os.path.exists(source_uri):
+ sfile = Gio.File.new_for_uri(source_uri)
+ file_size = sfile.query_info(
+ Gio.FILE_ATTRIBUTE_STANDARD_SIZEi,
+ Gio.FileQueryInfoFlags.NONE, None).get_size()
+ if file_size == size:
+ return False
+
+ return True
def _showImportSourcesDialog(self):
"""Pop up the "Import Sources" dialog box"""
@@ -487,16 +653,13 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
chooser_action = Gtk.FileChooserAction.OPEN
dialogtitle = _("Select One or More Files")
- close_after = Gtk.CheckButton(label=_("Close after importing files"))
- close_after.set_active(self.app.settings.closeImportDialog)
-
self._importDialog = Gtk.FileChooserDialog(
title=dialogtitle, transient_for=None, action=chooser_action)
self._importDialog.set_icon_name("pitivi")
self._importDialog.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL,
_("Add"), Gtk.ResponseType.OK)
- self._importDialog.props.extra_widget = close_after
+ self._importDialog.props.extra_widget = FileChooserExtraWidget(self.app)
self._importDialog.set_default_response(Gtk.ResponseType.OK)
self._importDialog.set_select_multiple(True)
self._importDialog.set_modal(True)
@@ -511,46 +674,18 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
'update-preview', previewer.add_preview_request)
# Filter for the "known good" formats by default
filt_supported = Gtk.FileFilter()
- filt_known = Gtk.FileFilter()
filt_supported.set_name(_("Supported file formats"))
- for category, mime_types in SUPPORTED_FILE_FORMATS.items():
- for mime in mime_types:
- filt_supported.add_mime_type(category + "/" + mime)
- filt_known.add_mime_type(category + "/" + mime)
- # Also allow showing known but not reliable demuxers
- filt_known.set_name(_("All known file formats"))
- for fullmime in OTHER_KNOWN_FORMATS:
- filt_known.add_mime_type(fullmime)
+ filt_supported.add_custom(Gtk.FileFilterFlags.URI |
+ Gtk.FileFilterFlags.MIME_TYPE,
+ self.__filterProxies)
# ...and allow the user to override our whitelists
default = Gtk.FileFilter()
default.set_name(_("All files"))
default.add_pattern("*")
self._importDialog.add_filter(filt_supported)
- self._importDialog.add_filter(filt_known)
self._importDialog.add_filter(default)
self._importDialog.show()
- def _updateProgressbar(self):
- """
- Update the _progressbar with the ratio of clips imported vs the total
- """
- # The clip iter has a +1 offset in the progressbar label (to refer to
- # the actual # of the clip we're processing), but there is no offset
- # in the progressbar itself (to reflect the process being incomplete).
- current_clip_iter = self.app.project_manager.current_project.nb_imported_files
- total_clips = self.app.project_manager.current_project.nb_remaining_file_to_import + \
- current_clip_iter
-
- progressbar_text = (_("Importing clip %(current_clip)d of %(total)d") %
- {"current_clip": current_clip_iter + 1,
- "total": total_clips})
- self._progressbar.set_text(progressbar_text)
- if current_clip_iter == 0:
- self._progressbar.set_fraction(0.0)
- elif total_clips != 0:
- self._progressbar.set_fraction(
- current_clip_iter / float(total_clips))
-
def _getThumbnailInDir(self, dir, hash):
"""
For a given thumbnail cache directory and file URI hash, see if there're
@@ -583,7 +718,6 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
def _generateThumbnails(self, uri):
if not self.thumbnailer:
- # TODO: Use thumbnails generated with GStreamer.
return None
# This way of getting the mimetype feels awfully convoluted but
# seems to be the proper/reliable way in a GNOME context
@@ -611,18 +745,27 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
LARGE_SIZE = 96
info = asset.get_info()
+ if self.app.proxy_manager.isProxyAsset(asset) and not \
+ asset.props.proxy_target:
+ self.info("%s is a proxy asset but has no target,"
+ "not displaying it.", asset.props.id)
+ return
+
+ self.debug("Adding asset %s", asset.props.id)
+
# The code below tries to read existing thumbnails from the freedesktop
# thumbnails directory (~/.thumbnails). The filenames are simply
# the file URI hashed with md5, so we can retrieve them easily.
video_streams = [
i for i in info.get_stream_list() if isinstance(i, DiscovererVideoInfo)]
+ real_uri = get_proxy_target(asset).props.id
if len(video_streams) > 0:
# From the freedesktop spec: "if the environment variable
# $XDG_CACHE_HOME is set and not blank then the directory
# $XDG_CACHE_HOME/thumbnails will be used, otherwise
# $HOME/.cache/thumbnails will be used."
# Older version of the spec also mentioned $HOME/.thumbnails
- quoted_uri = quote_uri(info.get_uri())
+ quoted_uri = quote_uri(real_uri)
thumbnail_hash = md5(quoted_uri.encode()).hexdigest()
try:
thumb_dir = os.environ['XDG_CACHE_HOME']
@@ -644,78 +787,160 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
thumb_128 = self._getIcon(
"image-x-generic", None, LARGE_SIZE)
else:
- thumb_64 = self._getIcon("video-x-generic")
- thumb_128 = self._getIcon(
- "video-x-generic", None, LARGE_SIZE)
- # TODO ideally gst discoverer should create missing thumbnails.
- self.log(
- "Missing a thumbnail for %s, queuing", path_from_uri(quoted_uri))
- self._missing_thumbs.append(quoted_uri)
+ thumb_cache = getThumbnailCache(asset)
+ thumb_64 = thumb_cache.getPreviewThumbnail()
+ if not thumb_64:
+ thumb_64 = self._getIcon("video-x-generic")
+ thumb_128 = self._getIcon("video-x-generic",
+ None, LARGE_SIZE)
+ else:
+ thumb_128 = thumb_64.scale_simple(
+ 128, thumb_64.get_height() * 2,
+ GdkPixbuf.InterpType.BILINEAR)
else:
thumb_64 = self._getIcon("audio-x-generic")
thumb_128 = self._getIcon("audio-x-generic", None, LARGE_SIZE)
+ thumbs_decorator = ThumbnailsDecorator([thumb_64, thumb_128], asset)
if info.get_duration() == Gst.CLOCK_TIME_NONE:
duration = ''
else:
duration = beautify_length(info.get_duration())
-
- name = info_name(info)
-
- self.pending_rows.append((thumb_64,
- thumb_128,
- beautify_info(info),
+ name = info_name(asset)
+ self.pending_rows.append((thumbs_decorator.thumb_64,
+ thumbs_decorator.thumb_128,
+ beautify_asset(asset),
asset,
- info.get_uri(),
+ asset.props.id,
duration,
- name))
- if len(self.pending_rows) > 50:
- self._flushPendingRows()
+ name,
+ thumbs_decorator))
+ self._flushPendingRows()
def _flushPendingRows(self):
self.debug("Flushing %d pending model rows", len(self.pending_rows))
for row in self.pending_rows:
self.storemodel.append(row)
+
del self.pending_rows[:]
# medialibrary callbacks
- def _assetAddedCb(self, unused_project, asset,
- unused_current_clip_iter=None, unused_total_clips=None):
+ def _assetLoadingProgressCb(self, project, progress, estimated_time):
+ self._progressbar.set_fraction(progress / 100)
+
+ for row in self.storemodel:
+ row[COL_INFOTEXT] = beautify_asset(row[COL_ASSET])
+
+ if progress == 0:
+ self._startImporting(project)
+ else:
+ if project.loaded:
+ num_proxying_files = [asset for asset in project.loading_assets if not asset.ready]
+ if estimated_time:
+ self.__last_proxying_estimate_time = beautify_ETA(int(
+ estimated_time * Gst.SECOND))
+
+ # Translators: this string indicates the estimated time
+ # remaining until an action (such as rendering) completes.
+ # The "%s" is an already-localized human-readable duration,
+ # such as "31 seconds", "1 minute" or "1 hours, 14 minutes".
+ # In some languages, "About %s left" can be expressed roughly as
+ # "There remains approximatively %s" (to handle gender and plurals)
+ progress_message = _("Transcoding %d assets: %d%% (About %s left)") % (
+ len(num_proxying_files), progress,
+ self.__last_proxying_estimate_time)
+ self._progressbar.set_text(progress_message)
+ self._last_imported_uris.update([asset.props.id for asset in
+ project.loading_assets])
+
+ self._progressbar.set_fraction(progress / 100)
+
+ if progress == 100:
+ self._doneImporting()
+
+ def __assetProxyingCb(self, proxy, unused_pspec):
+ self.debug("Proxy is %s", proxy.props.id)
+ self.__removeAsset(proxy)
+
+ if proxy.get_proxy_target() is not None:
+ # Re add the proxy so its emblem icon is updated.
+ self._addAsset(proxy)
+
+ def __assetProxiedCb(self, asset, unused_pspec):
+ self.debug("Asset proxied: %s -- %s", asset, asset.props.id)
+ proxy = asset.props.proxy
+ self.__removeAsset(asset)
+ if not proxy:
+ self._addAsset(asset)
+
+ self.app.gui.timeline_ui.switchProxies(asset)
+
+ def _assetAddedCb(self, unused_project, asset):
""" a file was added to the medialibrary """
- if isinstance(asset, GES.UriClipAsset):
- self._updateProgressbar()
+
+ if asset in [row[COL_ASSET] for row in self.storemodel]:
+ self.info("Asset %s already in!", asset.props.id)
+ return
+
+ if isinstance(asset, GES.UriClipAsset) and not asset.error:
+ self.debug("Asset %s added: %s", asset, asset.props.id)
+ asset.connect("notify::proxy", self.__assetProxiedCb)
+ asset.connect("notify::proxy-target", self.__assetProxyingCb)
+ if asset.get_proxy():
+ self.debug("Not adding asset %s "
+ "as it is proxied by %s",
+ asset.props.id,
+ asset.get_proxy().props.id)
+ return
+
self._addAsset(asset)
def _assetRemovedCb(self, unused_project, asset):
+ self.debug("%s Disconnecting %s - %s", self, asset, asset.props.id)
+ asset.disconnect_by_func(self.__assetProxiedCb)
+ asset.disconnect_by_func(self.__assetProxyingCb)
+ self.__removeAsset(asset)
+
+ def __removeAsset(self, asset):
""" the given uri was removed from the medialibrary """
# find the good line in the storemodel and remove it
model = self.storemodel
uri = asset.get_id()
+ found = False
for row in model:
if uri == row[COL_URI]:
model.remove(row.iter)
+ found = True
break
- if not len(model):
- self._welcome_infobar.show_all()
- self.debug("Removing: %s", uri)
+
+ if not found:
+ self.info("Trying to removed %s but that was not found"
+ "in the liststore", uri)
+
+ def _proxyingErrorCb(self, unused_project, asset):
+ self.__removeAsset(asset)
+ self._addAsset(asset)
def _errorCreatingAssetCb(self, unused_project, error, id, type):
""" The given uri isn't a media file """
+
if GObject.type_is_a(type, GES.UriClip):
+ if self.app.proxy_manager.isProxyAsset(id):
+ self.debug("Error %s with a proxy"
+ ", not showing the error message", error)
+ return
+
error = (id, str(error.domain), error)
self._errors.append(error)
- self._updateProgressbar()
- def _sourcesStartedImportingCb(self, project):
+ def _startImporting(self, project):
+ self.__last_proxying_estimate_time = _("Unknown")
self.import_start_time = time.time()
self._welcome_infobar.hide()
self._progressbar.show()
- if project.loaded:
- # Some new files are being imported.
- self._last_imported_uris += [asset.props.id for asset in project.get_loading_assets()]
- def _sourcesStoppedImportingCb(self, unused_project):
+ def _doneImporting(self):
self.debug("Importing took %.3f seconds",
time.time() - self.import_start_time)
self._flushPendingRows()
@@ -764,7 +989,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
if not self._last_imported_uris:
return
self._selectSources(self._last_imported_uris)
- self._last_imported_uris = []
+ self._last_imported_uris = set()
def _generateThumbnailsThread(self, missing_thumbs):
for uri in missing_thumbs:
@@ -803,8 +1028,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
if response == Gtk.ResponseType.OK:
lastfolder = dialogbox.get_current_folder()
self.app.settings.lastImportFolder = lastfolder
- self.app.settings.closeImportDialog = \
- dialogbox.props.extra_widget.get_active()
+ dialogbox.props.extra_widget.saveValues()
filenames = dialogbox.get_uris()
self.app.project_manager.current_project.addUris(filenames)
if self.app.settings.closeImportDialog:
@@ -915,7 +1139,21 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
else:
self._setClipView(SHOW_ICONVIEW)
+ def __getPathUnderMouse(self, view, event):
+ if isinstance(view, Gtk.TreeView):
+ path = None
+ tup = view.get_path_at_pos(int(event.x), int(event.y))
+ if tup:
+ path, column, x, y = tup
+ return path
+ elif isinstance(view, Gtk.IconView):
+ return view.get_path_at_pos(int(event.x), int(event.y))
+ else:
+ raise RuntimeError(
+ "Unknown media library view type: %s" % type(view))
+
def _rowUnderMouseSelected(self, view, event):
+ path = self.__getPathUnderMouse(view, event)
if isinstance(view, Gtk.TreeView):
path = None
tup = view.get_path_at_pos(int(event.x), int(event.y))
@@ -923,6 +1161,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
path, column, x, y = tup
if path:
selection = view.get_selection()
+
return selection.path_is_selected(path) and selection.count_selected_rows() > 0
elif isinstance(view, Gtk.IconView):
path = view.get_path_at_pos(int(event.x), int(event.y))
@@ -965,15 +1204,112 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
elif self.clip_view == SHOW_ICONVIEW:
self.iconview.unselect_all()
+ def __stopUsingProxyCb(self,
+ unused_action,
+ unused_parameter):
+ self._project.disableProxiesForAssets(self.getSelectedAssets())
+
+ def __useProxiesCb(self, unused_action, unused_parameter):
+ self._project.useProxiesForAssets(self.getSelectedAssets())
+
+ def __deleteProxiesCb(self, unused_action, unused_parameter):
+ self._project.disableProxiesForAssets(self.getSelectedAssets(), delete_proxy_file=True)
+
+ def __createMenuModel(self):
+ if self.app.proxy_manager.proxyingUnsupported:
+ return None, None
+
+ assets = self.getSelectedAssets()
+ action_group = Gio.SimpleActionGroup()
+ menu_model = Gio.Menu()
+
+ proxies = [asset.get_proxy_target() for asset in assets
+ if asset.get_proxy_target()]
+ in_progress = [asset.creation_progress for asset in assets
+ if asset.creation_progress < 100]
+
+ if proxies or 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",
+ 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 len(proxies) != len(assets) and len(in_progress) != len(assets):
+ 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))
+
+ menu_model.append(text, "assets.%s" %
+ action.get_name().replace(" ", "."))
+
+ return menu_model, action_group
+
+ def __maybeShowPopoverMenu(self, view, event):
+ res, button = event.get_button()
+ if not res or button != 3:
+ return False
+
+ if not self._rowUnderMouseSelected(view, event):
+ path = self.__getPathUnderMouse(view, event)
+ if path:
+ if isinstance(view, Gtk.IconView):
+ view.unselect_all()
+ view.select_path(path)
+ else:
+ selection = view.get_selection()
+ selection.unselect_all()
+ selection.select_path(path)
+
+ model, action_group = self.__createMenuModel()
+ if not model:
+ return True
+
+ popover = Gtk.Popover.new_from_model(view, model)
+ popover.insert_action_group("assets", action_group)
+ popover.props.position = Gtk.PositionType.BOTTOM
+
+ if self.clip_view == SHOW_TREEVIEW:
+ scrollwindow = self.treeview_scrollwin
+ elif self.clip_view == SHOW_ICONVIEW:
+ scrollwindow = self.iconview_scrollwin
+
+ rect = Gdk.Rectangle()
+ rect.x = event.x - scrollwindow.props.hadjustment.props.value
+ rect.y = event.y - scrollwindow.props.vadjustment.props.value
+ rect.width = 1
+ rect.height = 1
+ popover.set_pointing_to(rect)
+ popover.show_all()
+
+ return True
+
def _treeViewButtonPressEventCb(self, treeview, event):
self._updateDraggedPaths(treeview, event)
Gtk.TreeView.do_button_press_event(treeview, event)
- ts = self.treeview.get_selection()
+ selection = self.treeview.get_selection()
if self._draggedPaths:
for path in self._draggedPaths:
- ts.select_path(path)
+ selection.select_path(path)
return True
@@ -995,16 +1331,20 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
else:
self._draggedPaths = None
- def _treeViewButtonReleaseEventCb(self, unused_treeview, event):
- ts = self.treeview.get_selection()
- state = event.get_state() & (
+ def _treeViewButtonReleaseEventCb(self, treeview, event):
+ selection = self.treeview.get_selection()
+ state = selection() & (
Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
path = self.treeview.get_path_at_pos(event.x, event.y)
+ if self.__maybeShowPopoverMenu(treeview, event):
+ self.debug("Returning after showing popup menu")
+ return
+
if not state and not self.dragged:
- ts.unselect_all()
+ selection.unselect_all()
if path:
- ts.select_path(path[0])
+ selection.select_path(path[0])
def _viewSelectionChangedCb(self, unused):
self._updateActions()
@@ -1043,6 +1383,11 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
control_mask = event.get_state() & Gdk.ModifierType.CONTROL_MASK
shift_mask = event.get_state() & Gdk.ModifierType.SHIFT_MASK
modifier_active = control_mask or shift_mask
+
+ if self.__maybeShowPopoverMenu(iconview, event):
+ self.debug("Returning after showing popup menu")
+ return
+
if not modifier_active and self.iconview_cursor_pos:
current_cursor_pos = self.iconview.get_path_at_pos(
event.x, event.y)
@@ -1052,13 +1397,28 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
iconview.unselect_all()
iconview.select_path(current_cursor_pos)
+ def __disconnectFromProject(self):
+ if not self._project:
+ return
+
+ self._project.disconnect_by_func(self._assetAddedCb)
+ self._project.disconnect_by_func(self._assetLoadingProgressCb)
+ self._project.disconnect_by_func(self._assetRemovedCb)
+ self._project.disconnect_by_func(self._proxyingErrorCb)
+ self._project.disconnect_by_func(self._errorCreatingAssetCb)
+ self._project.disconnect_by_func(self.__projectSettingsSetFromImportedAssetCb)
+
def _newProjectCreatedCb(self, unused_app, project):
- if self._project is not project:
- self._project = project
- self._resetErrorList()
- self.storemodel.clear()
- self._welcome_infobar.show_all()
- self._connectToProject(project)
+ if self._project is project:
+ return
+
+ self.__disconnectFromProject()
+
+ self._project = project
+ self._resetErrorList()
+ self.storemodel.clear()
+ self._welcome_infobar.show_all()
+ self._connectToProject(project)
def _newProjectLoadedCb(self, unused_app, project, unused_fully_ready):
if self._project is not project:
@@ -1114,9 +1474,9 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
# library
self.app.threads.addThread(PathWalker, directories, self._addUris)
if filenames:
- self._last_imported_uris += filenames
+ self._last_imported_uris.update(filenames)
project = self.app.project_manager.current_project
- assets = project.assetsForUris(self._last_imported_uris)
+ assets = project.assetsForUris(list(self._last_imported_uris))
if assets:
# All the files have already been added.
self._selectLastImportedUris()
diff --git a/pitivi/project.py b/pitivi/project.py
index c184562..171edfc 100644
--- a/pitivi/project.py
+++ b/pitivi/project.py
@@ -557,6 +557,8 @@ class ProjectManager(GObject.Object, Loggable):
"Could not close project - this could be because there were unsaved changes and the user
cancelled when prompted about them")
return False
+ self.current_project.finalize()
+
self.emit("project-closed", self.current_project)
# We should never choke on silly stuff like disconnecting signals
# that were already disconnected. It blocks the UI for nothing.
@@ -580,6 +582,7 @@ class ProjectManager(GObject.Object, Loggable):
the creation of a new project without prompting the user about unsaved
changes. This is an "extreme" way to reset Pitivi's state.
"""
+ self.debug("New blank project")
if self.current_project is not None:
# This will prompt users about unsaved changes (if any):
if not ignore_unsaved_changes and not self.closeRunningProject():
@@ -702,12 +705,15 @@ class Project(Loggable, GES.Project):
Signals:
- C{project-changed}: Modifications were made to the project
- C{start-importing}: Started to import files
- - C{done-importing}: Done importing files
"""
__gsignals__ = {
+ "asset-loading-progress": (GObject.SignalFlags.RUN_LAST, None, (object, int)),
+ # Working around the fact that PyGObject does not let us emit error-loading-asset
+ # and bugzilla does not let me file a bug right now :/
+ "proxying-error": (GObject.SignalFlags.RUN_LAST, None,
+ (object,)),
"start-importing": (GObject.SignalFlags.RUN_LAST, None, ()),
- "done-importing": (GObject.SignalFlags.RUN_LAST, None, ()),
"project-changed": (GObject.SignalFlags.RUN_LAST, None, ()),
"rendering-settings-changed": (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_PYOBJECT,
@@ -730,6 +736,15 @@ class Project(Loggable, GES.Project):
self.loaded = False
self._at_least_one_asset_missing = False
self.app = app
+ self.loading_assets = []
+ self.asset_loading_progress = 100
+ self.app.proxy_manager.connect("progress", self.__assetTranscodingProgressCb)
+ self.app.proxy_manager.connect("error-preparing-asset",
+ self.__proxyErrorCb)
+ self.app.proxy_manager.connect("asset-preparing-cancelled",
+ self.__assetTranscodingCancelledCb)
+ self.app.proxy_manager.connect("proxy-ready",
+ self.__proxyReadyCb)
# GstValidate
self.scenario = scenario
@@ -1020,34 +1035,171 @@ class Project(Loggable, GES.Project):
if value:
self.set_meta("render-scale", value)
+ # ------------------------------#
+ # Proxy creation implementation #
+ # ------------------------------#
+ def __assetTranscodingProgressCb(self, unused_proxy_manager, asset,
+ creation_progress, estimated_time):
+ self.__updateAssetLoadingProgress(estimated_time)
+
+ def __updateAssetLoadingProgress(self, estimated_time=0):
+ num_loading_assets = len(self.loading_assets)
+
+ if num_loading_assets == 0:
+ self.emit("asset-loading-progress", 100, estimated_time)
+ return
+
+ total_import_duration = 0
+ for asset in self.loading_assets:
+ total_import_duration += asset.get_duration()
+
+ if total_import_duration == 0:
+ self.info("No known duration yet")
+ return
+
+ self.asset_loading_progress = 0
+ all_ready = True
+ for asset in self.loading_assets:
+ asset_weight = asset.get_duration() / total_import_duration
+ self.asset_loading_progress += asset_weight * asset.creation_progress
+
+ if asset.creation_progress < 100:
+ all_ready = False
+ elif not asset.ready:
+ self.setModificationState(True)
+ asset.ready = True
+
+ if all_ready:
+ self.asset_loading_progress = 100
+
+ self.emit("asset-loading-progress", self.asset_loading_progress,
+ estimated_time)
+
+ if all_ready:
+ self.info("No more loading assets")
+ self.loading_assets = []
+
+ def __assetTranscodingCancelledCb(self, unused_proxy_manager, asset):
+ self.__setProxy(asset, None, emit_asset_added=False)
+ self.__updateAssetLoadingProgress()
+
+ def __proxyErrorCb(self, unused_proxy_manager, asset, proxy,
+ error):
+ if asset is None:
+ asset_id = self.app.proxy_manager.getTargetUri(proxy)
+ asset = GES.Asset.request(proxy.get_extractable_type(),
+ asset_id)
+ if not asset:
+ for tmpasset in self.loading_assets.keys():
+ if tmpasset.props.id == asset_id:
+ asset = tmpasset
+ break
+
+ if not asset:
+ self.error("Could not get the asset %s from its proxy %s", asset_id,
+ proxy.props.id)
+
+ return
+
+ asset.proxying_error = error
+ asset.creation_progress = 100
+
+ self.emit("proxying-error", asset)
+ self.__updateAssetLoadingProgress()
+
+ def __proxyReadyCb(self, unused_proxy_manager, asset, proxy):
+ self.__setProxy(asset, proxy)
+
+ def __setProxy(self, asset, proxy, emit_asset_added=True):
+ asset.creation_progress = 100
+ if proxy:
+ proxy.ready = False
+ proxy.error = None
+ proxy.creation_progress = 100
+
+ asset.set_proxy(proxy)
+ try:
+ self.loading_assets.remove(asset)
+ except ValueError:
+ pass
+
+ if proxy:
+ self.add_asset(proxy)
+ elif emit_asset_added:
+ self.emit("asset-added", asset)
+
+ if proxy:
+ self.loading_assets.append(proxy)
+
+ self.__updateAssetLoadingProgress()
+
# ------------------------------------------ #
# GES.Project virtual methods implementation #
# ------------------------------------------ #
-
- def _handle_asset_loaded(self, asset=None):
+ def do_asset_loading(self, asset):
if asset and not GObject.type_is_a(asset.get_extractable_type(), GES.UriClip):
# Ignore for example the assets producing GES.TitleClips.
return
- self.nb_imported_files += 1
- self.nb_remaining_file_to_import = self.__countRemainingFilesToImport()
- if self.nb_remaining_file_to_import == 0:
- self.nb_imported_files = 0
- # We do not take into account asset comming from project
- if self.loaded is True:
- self.app.action_log.commit()
- self._emitChange("done-importing")
+
+ if not self.loading_assets:
+ # Progress == 0 means "starting to import"
+ self.emit("asset-loading-progress", 0, 0)
+
+ if not self.loaded:
+ self.debug("Project still loading, not using proxies: "
+ "%s", asset.props.id)
+ asset.creation_progress = 100
+ else:
+ asset.creation_progress = 0
+
+ asset.error = None
+ asset.ready = False
+ asset.force_proxying = False
+ asset.proxying_error = None
+ self.loading_assets.append(asset)
+
+ def do_asset_removed(self, asset):
+ self.app.proxy_manager.cancelJob(asset)
def do_asset_added(self, asset):
"""
When GES.Project emit "asset-added" this vmethod
get calls
"""
- self._handle_asset_loaded(asset=asset)
self._maybeInitSettingsFromAsset(asset)
+ if asset and not GObject.type_is_a(asset.get_extractable_type(),
+ GES.UriClip):
+ # Ignore for example the assets producing GES.TitleClips.
+ self.debug("Ignoring asset: %s", asset.props.id)
+ return
+
+ if asset not in self.loading_assets:
+ self.debug("Asset %s is not in loading assets, "
+ " it must not be proxied", asset.get_id())
+ return
+
+ if self.loaded:
+ if not asset.get_proxy_target() in self.list_assets(GES.Extractable):
+ self.app.proxy_manager.addJob(asset, asset.force_proxying)
+ else:
+ self.debug("Project still loading, not using proxies: "
+ "%s", asset.props.id)
+ asset.creation_progress = 100
+ self.__updateAssetLoadingProgress()
- def do_loading_error(self, unused_error, unused_asset_id, unused_type):
+ def do_loading_error(self, error, asset_id, unused_type):
""" vmethod, get called on "asset-loading-error"""
- self._handle_asset_loaded()
+ asset = None
+ for asset in self.loading_assets:
+ if asset.get_id() == asset_id:
+ break
+
+ self.error("Could not load %s: %s -> %s" % (asset_id, error,
+ asset))
+ asset.error = error
+ asset.creation_progress = 100
+ self.loading_assets.remove(asset)
+ self.__updateAssetLoadingProgress()
def do_loaded(self, unused_timeline):
""" vmethod, get called on "loaded" """
@@ -1055,11 +1207,11 @@ class Project(Loggable, GES.Project):
self._ensureTracks()
self.timeline.props.auto_transition = True
self._ensureLayer()
+ self.loaded = True
if self.scenario is not None:
return
- self.loaded = True
encoders = CachedEncoderList()
# The project just loaded, we need to check the new
# encoding profiles and make use of it now.
@@ -1097,6 +1249,47 @@ class Project(Loggable, GES.Project):
# Our API #
# ------------------------------------------ #
+ def finalize(self):
+ """
+ Disconnect all signals and everything so that the project won't
+ be doing anything after the call to the method.
+ """
+ if self._scenario:
+ self._scenario.disconnect_by_func(self._scenarioDoneCb)
+ self.app.proxy_manager.disconnect_by_func(self.__assetTranscodingProgressCb)
+ self.app.proxy_manager.disconnect_by_func(self.__proxyErrorCb)
+ self.app.proxy_manager.disconnect_by_func(self.__assetTranscodingCancelledCb)
+ self.app.proxy_manager.disconnect_by_func(self.__proxyReadyCb)
+
+ def useProxiesForAssets(self, assets):
+ for asset in assets:
+ proxy_target = asset.get_proxy_target()
+ if not proxy_target:
+ # Add and remove the asset to
+ # trigger the proxy creation code path
+ self.remove_asset(asset)
+ self.emit("asset-loading", asset)
+ asset.force_proxying = True
+ self.add_asset(asset)
+
+ def disableProxiesForAssets(self, assets, delete_proxy_file=False):
+ for asset in assets:
+ proxy_target = asset.get_proxy_target()
+ if proxy_target:
+ self.debug("Stop proxying %s", proxy_target.props.id)
+ proxy_target.set_proxy(None)
+ if delete_proxy_file:
+ if not self.app.proxy_manager.isProxyAsset(asset):
+ raise RuntimeError("Trying to remove proxy %s"
+ " but it does not look like one!",
+ asset.props.id)
+ os.remove(Gst.uri_get_location(asset.props.id))
+ else:
+ self.app.proxy_manager.cancelJob(asset)
+
+ if assets:
+ self.setModificationState(True)
+
def hasDefaultName(self):
return DEFAULT_NAME == self.name
@@ -1123,8 +1316,6 @@ class Project(Loggable, GES.Project):
return False
self.timeline.commit = self._commit
- self._calculateNbLoadingAssets()
-
self.pipeline = Pipeline(self.app)
try:
self.pipeline.set_timeline(self.timeline)
@@ -1163,7 +1354,6 @@ class Project(Loggable, GES.Project):
self.app.action_log.begin("Adding assets")
for uri in uris:
self.create_asset(quote_uri(uri), GES.UriClip)
- self._calculateNbLoadingAssets()
def assetsForUris(self, uris):
assets = []
@@ -1379,19 +1569,6 @@ class Project(Loggable, GES.Project):
return factories[0].get_name()
return None
- def _calculateNbLoadingAssets(self):
- nb_remaining_file_to_import = self.__countRemainingFilesToImport()
- if self.nb_remaining_file_to_import == 0 and nb_remaining_file_to_import:
- self.nb_remaining_file_to_import = nb_remaining_file_to_import
- self._emitChange("start-importing")
- return
- self.nb_remaining_file_to_import = nb_remaining_file_to_import
-
- def __countRemainingFilesToImport(self):
- assets = self.get_loading_assets()
- return len([asset for asset in assets if
- GObject.type_is_a(asset.get_extractable_type(), GES.UriClip)])
-
# ---------------------- UI classes ----------------------------------------- #
diff --git a/pitivi/render.py b/pitivi/render.py
index 8e40e34..500c115 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -370,6 +370,7 @@ class RenderDialog(Loggable):
# the current container format.
self.preferred_vencoder = self.project.vencoder
self.preferred_aencoder = self.project.aencoder
+ self.__unproxiedClips = {}
self._initializeComboboxModels()
self._displaySettings()
@@ -508,6 +509,15 @@ class RenderDialog(Loggable):
self.video_output_checkbutton.props.active = self.project.video_profile.is_enabled()
self.audio_output_checkbutton.props.active = self.project.audio_profile.is_enabled()
+ self.__automatically_use_proxies = builder.get_object(
+ "automatically_use_proxies")
+
+ self.__always_use_proxies = builder.get_object("always_use_proxies")
+ self.__always_use_proxies.props.group = self.__automatically_use_proxies
+
+ self.__never_use_proxies = builder.get_object("never_use_proxies")
+ self.__never_use_proxies.props.group = self.__automatically_use_proxies
+
self.render_presets.setupUi(self.presets_combo, self.preset_menubutton)
icon = os.path.join(configure.get_pixmap_dir(), "pitivi-render-16.png")
@@ -686,6 +696,7 @@ class RenderDialog(Loggable):
self._rendering_is_paused = False
self._time_spent_paused = 0
self._pipeline.set_state(Gst.State.NULL)
+ self.__useProxyAssets()
self._disconnectFromGst()
self._pipeline.set_mode(GES.PipelineFlags.FULL_PREVIEW)
self._pipeline.set_state(Gst.State.PAUSED)
@@ -731,6 +742,46 @@ class RenderDialog(Loggable):
canberra = pycanberra.Canberra()
canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)
+ def __maybeUseSourceAsset(self):
+ if self.__always_use_proxies.get_active():
+ self.debug("Rendering from proxies, not replacing assets")
+ return
+
+ for layer in self.app.gui.timeline_ui.bTimeline.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:
+ continue
+
+ if self.__automatically_use_proxies.get_active():
+ if self.app.proxy_manager.isAssetFormatWellSupported(
+ asset_target):
+ self.info("Asset %s format well supported, "
+ "rendering from real asset.",
+ asset_target.props.id)
+ else:
+ self.info("Asset %s format not well supported, "
+ "rendering from proxy.",
+ asset_target.props.id)
+ continue
+
+ if not asset_target.get_error():
+ clip.set_asset(asset_target)
+ self.error("Using %s as an asset (instead of %s)",
+ asset_target.get_id(),
+ asset.get_id())
+ self.__unproxiedClips[clip] = asset
+
+ def __useProxyAssets(self):
+ for clip, asset in self.__unproxiedClips.items():
+ clip.set_asset(asset)
+
+ self.__unproxiedClips = {}
+
# ------------------- Callbacks ------------------------------------------ #
# -- UI callbacks
@@ -743,6 +794,7 @@ class RenderDialog(Loggable):
The render button inside the render dialog has been clicked,
start the rendering process.
"""
+ self.__maybeUseSourceAsset()
self.outfile = os.path.join(self.filebutton.get_uri(),
self.fileentry.get_text())
self.progress = RenderingProgressDialog(self.app, self)
diff --git a/pitivi/timeline/elements.py b/pitivi/timeline/elements.py
index f398ef7..9d9c75f 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -463,10 +463,6 @@ class TimelineElement(Gtk.Layout, timelineUtils.Zoomable, Loggable):
if binding.props.name == self.__controlledProperty.name:
self.__createKeyframeCurve(binding)
- # Gtk implementation
- def do_set_property(self, property_id, value, pspec):
- Gtk.Layout.do_set_property(self, property_id, value, pspec)
-
def __showKeyframes(self):
if self.timeline.app.project_manager.current_project.pipeline.getState() == Gst.State.PLAYING:
return False
@@ -972,9 +968,16 @@ class UriClip(SourceClip):
def __init__(self, layer, bClip):
super(UriClip, self).__init__(layer, bClip)
+ self.props.has_tooltip = True
self.set_tooltip_markup(misc.filename_from_uri(bClip.get_uri()))
+ def do_query_tooltip(self, x, y, keyboard_mode, tooltip):
+ tooltip.set_markup(misc.filename_from_uri(
+ self.bClip.get_asset().props.id))
+
+ return True
+
def _childAdded(self, clip, child):
super(UriClip, self)._childAdded(clip, child)
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index 7d0da9d..84d5f3f 100644
--- a/pitivi/timeline/layer.py
+++ b/pitivi/timeline/layer.py
@@ -418,7 +418,10 @@ class Layer(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
bClips = self.bLayer.get_clips()
for bClip in bClips:
for child in bClip.get_children(False):
- self.media_types |= child.get_track().props.track_type
+ track = child.get_track()
+ if not track:
+ continue
+ self.media_types |= track.props.track_type
if self.media_types == GES.TrackType.AUDIO | GES.TrackType.VIDEO:
# Cannot find more types than these.
break
@@ -458,10 +461,7 @@ class Layer(Gtk.EventBox, timelineUtils.Zoomable, Loggable):
self.error("Implement UI for type %s?", bClip.__gtype__)
return
- if not hasattr(bClip, "ui") or bClip.ui is None:
- clip = ui_type(self, bClip)
- else:
- clip = bClip.ui
+ clip = ui_type(self, bClip)
self._layout.put(clip, self.nsToPixel(bClip.props.start), 0)
self.show_all()
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 0d7bb6d..f85f532 100644
--- a/pitivi/timeline/previewers.py
+++ b/pitivi/timeline/previewers.py
@@ -44,7 +44,8 @@ except ImportError:
from pitivi.settings import get_dir, xdg_cache_home
from pitivi.utils.loggable import Loggable
-from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file
+from pitivi.utils.misc import binary_search, filename_from_uri, quantize, quote_uri, hash_file, \
+ get_proxy_target
from pitivi.utils.system import CPUUsageTracker
from pitivi.utils.timeline import Zoomable
from pitivi.utils.ui import EXPANDED_SIZE
@@ -65,6 +66,7 @@ PREVIEW_GENERATOR_SIGNALS = {
"error": (GObject.SIGNAL_RUN_LAST, None, ()),
}
+THUMB_HEIGHT = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX
"""
Convention throughout this file:
@@ -73,6 +75,195 @@ is prefixed with a little b, example : bTimeline
"""
+class PreviewerBin(Gst.Bin, Loggable):
+ """
+ A baseclass for element specialized in gathering datas to create previews
+ """
+ def __init__(self, bin_desc):
+ Gst.Bin.__init__(self)
+ Loggable.__init__(self)
+
+ self.internal_bin = Gst.parse_bin_from_description(bin_desc, True)
+ self.add(self.internal_bin)
+ self.add_pad(Gst.GhostPad.new(None, self.internal_bin.sinkpads[0]))
+ self.add_pad(Gst.GhostPad.new(None, self.internal_bin.srcpads[0]))
+
+ def finalize(self):
+ pass
+
+
+class ThumbnailBin(PreviewerBin):
+ __gproperties__ = {
+ "uri": (str,
+ "uri of the media file",
+ "A URI",
+ "",
+ GObject.PARAM_READWRITE
+ ),
+ }
+
+ def __init__(self, bin_desc="videoconvert ! videorate ! "
+ "videoscale method=lanczos ! "
+ "capsfilter caps=video/x-raw,format=(string)RGBA,"
+ "height=(int)%d,pixel-aspect-ratio=(fraction)1/1,"
+ "framerate=2/1 ! gdkpixbufsink name=gdkpixbufsink " %
+ THUMB_HEIGHT):
+ PreviewerBin.__init__(self, bin_desc)
+
+ self.thumb_cache = None
+ self.gdkpixbufsink = self.internal_bin.get_by_name("gdkpixbufsink")
+
+ def addThumbnail(self, message):
+ struct = message.get_structure()
+ struct_name = struct.get_name()
+ if struct_name == "pixbuf":
+ stream_time = struct.get_value("stream-time")
+ self.log("%s new thumbnail %s", self.uri, stream_time)
+ pixbuf = struct.get_value("pixbuf")
+ self.thumb_cache[stream_time] = pixbuf
+
+ return False
+
+ def do_post_message(self, message):
+ if message.type == Gst.MessageType.ELEMENT and \
+ message.src == self.gdkpixbufsink:
+ GLib.idle_add(self.addThumbnail, message)
+
+ return Gst.Bin.do_post_message(self, message)
+
+ def finalize(self, proxy):
+ self.thumb_cache.commit()
+ if proxy:
+ self.thumb_cache.copy(proxy.get_id())
+
+ def do_get_property(self, prop):
+ if prop.name == 'uri':
+ return self.uri
+ else:
+ raise AttributeError('unknown property %s' % prop.name)
+
+ def do_set_property(self, prop, value):
+ if prop.name == 'uri':
+ self.uri = value
+ self.thumb_cache = getThumbnailCache(value)
+ else:
+ raise AttributeError('unknown property %s' % prop.name)
+
+
+class TeedThumbnailBin(ThumbnailBin):
+ def __init__(self):
+ ThumbnailBin.__init__(
+ self, bin_desc="tee name=t ! queue "
+ "max-size-buffers=0 max-size-bytes=0 max-size-time=0 ! "
+ "videoconvert ! videorate ! videoscale method=lanczos ! "
+ "capsfilter caps=video/x-raw,format=(string)RGBA,height=(int)%d,"
+ "pixel-aspect-ratio=(fraction)1/1,"
+ "framerate=2/1 ! gdkpixbufsink name=gdkpixbufsink "
+ "t. ! queue " % THUMB_HEIGHT)
+
+
+class WaveformPreviewer(PreviewerBin):
+ __gproperties__ = {
+ "uri": (str,
+ "uri of the media file",
+ "A URI",
+ "",
+ GObject.PARAM_READWRITE),
+ "duration": (GObject.TYPE_UINT64,
+ "Duration",
+ "Duration",
+ 0, GLib.MAXUINT64 - 1, 0, GObject.PARAM_READWRITE)
+ }
+
+ def __init__(self):
+ PreviewerBin.__init__(self,
+ "audioconvert ! audioresample ! level name=level"
+ " ! audioconvert ! audioresample")
+ self.level = self.internal_bin.get_by_name("level")
+ self.debug("Creating waveforms!!")
+ self.peaks = None
+
+ self.uri = None
+ self.wavefile = None
+ self.passthrough = False
+ self.nSamples = 0
+
+ def do_get_property(self, prop):
+ if prop.name == 'uri':
+ return self.uri
+ elif prop.name == 'duration':
+ return self.duration
+ else:
+ raise AttributeError('unknown property %s' % prop.name)
+
+ def do_set_property(self, prop, value):
+ if prop.name == 'uri':
+ self.uri = value
+ self.wavefile = get_wavefile_location_for_uri(self.uri)
+ self.passthrough = os.path.exists(self.wavefile)
+ elif prop.name == 'duration':
+ self.duration = value
+ self.nSamples = self.duration / 10000000
+ else:
+ raise AttributeError('unknown property %s' % prop.name)
+
+ def do_post_message(self, message):
+ if not self.passthrough and \
+ message.type == Gst.MessageType.ELEMENT and \
+ message.src == self.level:
+ s = message.get_structure()
+ p = None
+ if s:
+ p = s.get_value("rms")
+
+ if p:
+ st = s.get_value("stream-time")
+
+ if self.peaks is None:
+ self.peaks = []
+ for channel in p:
+ self.peaks.append([0] * int(self.nSamples))
+
+ pos = int(st / 10000000)
+ if pos >= len(self.peaks[0]):
+ return
+
+ for i, val in enumerate(p):
+ if val < 0:
+ val = 10 ** (val / 20) * 100
+ self.peaks[i][pos] = val
+ else:
+ self.peaks[i][pos] = self.peaks[i][pos - 1]
+
+ return Gst.Bin.do_post_message(self, message)
+
+ def finalize(self, proxy=None):
+ if not self.passthrough and self.peaks:
+ # Let's go mono.
+ if len(self.peaks) > 1:
+ samples = (
+ numpy.array(self.peaks[0]) + numpy.array(self.peaks[1])) / 2
+ else:
+ samples = numpy.array(self.peaks[0])
+
+ self.samples = list(samples)
+ with open(self.wavefile, 'wb') as wavefile:
+ pickle.dump(list(samples), wavefile)
+
+ if proxy:
+ proxy_wavefile = get_wavefile_location_for_uri(proxy.get_id())
+ self.debug("symlinking %s and %s", self.wavefile, proxy_wavefile)
+ os.symlink(self.wavefile, proxy_wavefile)
+
+
+Gst.Element.register(None, "waveformbin", Gst.Rank.NONE,
+ WaveformPreviewer)
+Gst.Element.register(None, "thumbnailbin", Gst.Rank.NONE,
+ ThumbnailBin)
+Gst.Element.register(None, "teedthumbnailbin", Gst.Rank.NONE,
+ TeedThumbnailBin)
+
+
class PreviewGeneratorManager():
"""
@@ -170,8 +361,9 @@ class VideoPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
# Variables related to the timeline objects
self.timeline = bElement.get_parent().get_timeline().ui
self.bElement = bElement
+
# Guard against malformed URIs
- self.uri = quote_uri(bElement.props.uri)
+ self.uri = quote_uri(get_proxy_target(bElement).props.id)
# Variables related to thumbnailing
self.wishlist = []
@@ -181,11 +373,11 @@ class VideoPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
# We should have one thumbnail per thumb_period.
# TODO: get this from the user settings
self.thumb_period = int(0.5 * Gst.SECOND)
- self.thumb_height = EXPANDED_SIZE - 2 * THUMB_MARGIN_PX
+ self.thumb_height = THUMB_HEIGHT
# Maps (quantized) times to Thumbnail objects
self.thumbs = {}
- self.thumb_cache = get_cache_for_uri(self.uri)
+ self.thumb_cache = getThumbnailCache(self.uri)
self.thumb_width, unused_height = self.thumb_cache.getImagesSize()
self.cpu_usage_tracker = CPUUsageTracker()
@@ -516,7 +708,12 @@ class Thumbnail(Gtk.Image):
caches = {}
-def get_cache_for_uri(uri):
+def getThumbnailCache(obj):
+ if isinstance(obj, str):
+ uri = obj
+ elif isinstance(obj, GES.UriClipAsset):
+ uri = get_proxy_target(obj).props.id
+
if uri in caches:
return caches[uri]
else:
@@ -537,13 +734,20 @@ class ThumbnailCache(Loggable):
self._filehash = hash_file(Gst.uri_get_location(uri))
self._filename = filename_from_uri(uri)
thumbs_cache_dir = get_dir(os.path.join(xdg_cache_home(), "thumbs"))
- dbfile = os.path.join(thumbs_cache_dir, self._filehash)
- self._db = sqlite3.connect(dbfile)
+ self._dbfile = os.path.join(thumbs_cache_dir, self._filehash)
+ self._db = sqlite3.connect(self._dbfile)
self._cur = self._db.cursor() # Use this for normal db operations
self._cur.execute("CREATE TABLE IF NOT EXISTS Thumbs\
(Time INTEGER NOT NULL PRIMARY KEY,\
Jpeg BLOB NOT NULL)")
+ def copy(self, uri):
+ filehash = hash_file(Gst.uri_get_location(uri))
+ thumbs_cache_dir = get_dir(os.path.join(xdg_cache_home(), "thumbs"))
+ dbfile = os.path.join(thumbs_cache_dir, filehash)
+
+ os.symlink(self._dbfile, dbfile)
+
def getImagesSize(self):
self._cur.execute("SELECT * FROM Thumbs LIMIT 1")
row = self._cur.fetchone()
@@ -553,6 +757,14 @@ class ThumbnailCache(Loggable):
pixbuf = self.__getPixbufFromRow(row)
return pixbuf.get_width(), pixbuf.get_height()
+ def getPreviewThumbnail(self):
+ self._cur.execute("SELECT Time FROM Thumbs")
+ timestamps = self._cur.fetchall()
+ if not timestamps:
+ return None
+
+ return self[timestamps[int(len(timestamps) / 2)][0]]
+
def __getPixbufFromRow(self, row):
jpeg = row[1]
loader = GdkPixbuf.PixbufLoader.new()
@@ -593,6 +805,8 @@ class ThumbnailCache(Loggable):
self._db.commit()
self.log("Saved thumbnail cache file: %s" % self._filehash)
+ return False
+
class PipelineCpuAdapter(Loggable):
@@ -692,6 +906,13 @@ class PipelineCpuAdapter(Loggable):
self.ready = False
+def get_wavefile_location_for_uri(uri):
+ filename = hash_file(Gst.uri_get_location(uri)) + ".wave"
+ cache_dir = get_dir(os.path.join(xdg_cache_home(), "waves"))
+
+ return os.path.join(cache_dir, filename)
+
+
class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
"""
@@ -717,7 +938,7 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
self._surface_x = 0
# Guard against malformed URIs
- self._uri = quote_uri(bElement.props.uri)
+ self._uri = quote_uri(get_proxy_target(bElement).props.id)
self._num_failures = 0
self.adapter = None
@@ -736,10 +957,7 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
GLib.idle_add(self._startLevelsDiscovery, priority=GLib.PRIORITY_LOW)
def _startLevelsDiscovery(self):
- self.log('Preparing waveforms for "%s"' % filename_from_uri(self._uri))
- filename = hash_file(Gst.uri_get_location(self._uri)) + ".wave"
- cache_dir = get_dir(os.path.join(xdg_cache_home(), "waves"))
- filename = os.path.join(cache_dir, filename)
+ filename = get_wavefile_location_for_uri(self._uri)
if os.path.exists(filename):
with open(filename, "rb") as samples:
@@ -753,11 +971,15 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
self.debug(
'Now generating waveforms for: %s', filename_from_uri(self._uri))
self.peaks = None
- self.pipeline = Gst.parse_launch("uridecodebin name=decode uri=" + self._uri +
- " ! audioconvert ! level name=wavelevel interval=10000000
post-messages=true ! fakesink qos=false name=faked")
+ self.pipeline = Gst.parse_launch("uridecodebin name=decode uri=" +
+ self._uri + " ! waveformbin name=wave"
+ " ! fakesink qos=false name=faked")
faked = self.pipeline.get_by_name("faked")
faked.props.sync = True
- self._wavelevel = self.pipeline.get_by_name("wavelevel")
+ self._wavebin = self.pipeline.get_by_name("wave")
+ asset = self.bElement.get_parent().get_asset()
+ self._wavebin.props.uri = asset.get_id()
+ self._wavebin.props.duration = asset.get_duration()
decode = self.pipeline.get_by_name("decode")
decode.connect("autoplug-select", self._autoplugSelectCb)
bus = self.pipeline.get_bus()
@@ -775,16 +997,8 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
self._force_redraw = True
def _prepareSamples(self):
- # Let's go mono.
- if len(self.peaks) > 1:
- samples = (
- numpy.array(self.peaks[0]) + numpy.array(self.peaks[1])) / 2
- else:
- samples = numpy.array(self.peaks[0])
-
- self.samples = samples.tolist()
- with open(self.wavefile, 'wb') as wavefile:
- pickle.dump(self.samples, wavefile)
+ self._wavebin.finalize()
+ self.samples = self._wavebin.samples
def _startRendering(self):
self.nbSamples = len(self.samples)
@@ -795,32 +1009,6 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
self.adapter.stop()
def _busMessageCb(self, bus, message):
- if message.src == self._wavelevel:
- s = message.get_structure()
- p = None
- if s:
- p = s.get_value("rms")
-
- if p:
- st = s.get_value("stream-time")
-
- if self.peaks is None:
- self.peaks = []
- for channel in p:
- self.peaks.append([0] * int(self.nSamples))
-
- pos = int(st / 10000000)
- if pos >= len(self.peaks[0]):
- return
-
- for i, val in enumerate(p):
- if val < 0:
- val = 10 ** (val / 20) * 100
- self.peaks[i][pos] = val
- else:
- self.peaks[i][pos] = self.peaks[i][pos - 1]
- return
-
if message.type == Gst.MessageType.EOS:
self._prepareSamples()
self._startRendering()
@@ -842,6 +1030,9 @@ class AudioPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
self._launchPipeline()
self.becomeControlled()
else:
+ Gst.debug_bin_to_dot_file_with_ts(self.pipeline,
+ Gst.DebugGraphDetails.ALL,
+ "error-generating-waveforms")
self.error("Issue during waveforms generation: %s"
"Abandonning", message.parse_error())
diff --git a/pitivi/timeline/timeline.py b/pitivi/timeline/timeline.py
index 830fe2f..067f5d2 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1167,6 +1167,30 @@ 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
+
+ layers = self.bTimeline.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)
+ self._project.pipeline.commit_timeline()
+
def insertAssets(self, assets, position=None):
"""
Add assets to the timeline and create clips on the longest layer.
diff --git a/pitivi/utils/Makefile.am b/pitivi/utils/Makefile.am
index 15a82e2..0d60648 100644
--- a/pitivi/utils/Makefile.am
+++ b/pitivi/utils/Makefile.am
@@ -12,6 +12,7 @@ utils_PYTHON = \
ripple_update_group.py \
misc.py \
validate.py \
+ proxy.py \
widgets.py
clean-local:
diff --git a/pitivi/utils/misc.py b/pitivi/utils/misc.py
index 235dc0e..9d33faf 100644
--- a/pitivi/utils/misc.py
+++ b/pitivi/utils/misc.py
@@ -29,6 +29,7 @@ from urllib.parse import urlparse, unquote, urlsplit
from gi.repository import GLib
from gi.repository import Gst
+from gi.repository import GES
from gi.repository import Gtk
from gettext import gettext as _
@@ -77,6 +78,21 @@ def call_false(function, *args, **kwargs):
return False
+def get_proxy_target(obj):
+ if isinstance(obj, GES.UriClip):
+ asset = obj.get_asset()
+ elif isinstance(obj, GES.TrackElement):
+ asset = obj.get_parent().get_asset()
+ else:
+ asset = obj
+
+ target = asset.get_proxy_target()
+ if target and target.get_error() is None:
+ asset = target
+
+ return asset
+
+
# ------------------------------ URI helpers --------------------------------
def isWritable(path):
diff --git a/pitivi/utils/proxy.py b/pitivi/utils/proxy.py
new file mode 100644
index 0000000..074c437
--- /dev/null
+++ b/pitivi/utils/proxy.py
@@ -0,0 +1,426 @@
+# Pitivi video editor
+#
+# pitivi/proxying.py
+#
+# Copyright (c) 2015, Thibault Saunier <tsaunier gnome org>
+#
+# 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.
+
+import os
+import time
+
+from gi.repository import GObject
+from gi.repository import GES
+from gi.repository import GLib
+from gi.repository import Gio
+from gi.repository import Gst
+from gi.repository import GstPbutils
+from gi.repository import GstTranscoder
+
+from pitivi.configure import get_gstpresets_dir
+from pitivi.settings import GlobalSettings
+from pitivi.utils.loggable import Loggable
+
+# Make sure gst knowns about our own GstPresets
+Gst.preset_set_app_dir(get_gstpresets_dir())
+
+
+class ProxyingStrategy:
+ AUTOMATIC = "automatic"
+ ALL = "all"
+ NOTHING = "nothing"
+
+
+GlobalSettings.addConfigSection("proxy")
+GlobalSettings.addConfigOption('proxyingStrategy',
+ section='proxy',
+ key='proxying-strategy',
+ default=ProxyingStrategy.AUTOMATIC)
+GlobalSettings.addConfigOption('numTranscodingJobs',
+ section='proxy',
+ key='num-proxying-jobs',
+ default=4)
+
+
+ENCODING_FORMAT_PRORES = "prores-flac-in-matroska.gep"
+ENCODING_FORMAT_JPEG = "jpeg-flac-in-matroska.gep"
+
+
+def createEncodingProfileSimple(container_caps, audio_caps, video_caps):
+ c = GstPbutils.EncodingContainerProfile.new(None, None,
+ Gst.Caps(container_caps),
+ None)
+ a = GstPbutils.EncodingAudioProfile.new(Gst.Caps(audio_caps),
+ None, None, 0)
+ v = GstPbutils.EncodingVideoProfile.new(Gst.Caps(video_caps),
+ None, None, 0)
+
+ c.add_profile(a)
+ c.add_profile(v)
+
+ return c
+
+
+class ProxyManager(GObject.Object, Loggable):
+ """
+ Transcodes assets and manages proxies
+ """
+ __gsignals__ = {
+ "progress": (GObject.SIGNAL_RUN_LAST, None, (object, int, int)),
+ "proxy-ready": (GObject.SIGNAL_RUN_LAST, None, (object, object)),
+ "asset-preparing-cancelled": (GObject.SIGNAL_RUN_LAST, None, (object,)),
+ "error-preparing-asset": (GObject.SIGNAL_RUN_LAST, None, (object,
+ object,
+ object)),
+ }
+
+ WHITELIST_FORMATS = []
+ for container in ["video/quicktime", "application/ogg",
+ "video/x-matroska", "video/webm"]:
+ for audio in ["audio/mpeg", "audio/x-vorbis",
+ "audio/x-raw", "audio/x-flac"]:
+ for video in ["video/x-h264", "image/jpeg",
+ "video/x-raw", "video/x-vp8",
+ "video/x-theora"]:
+ WHITELIST_FORMATS.append(createEncodingProfileSimple(
+ container, audio, video))
+
+ def __init__(self, app):
+ GObject.Object.__init__(self)
+ Loggable.__init__(self)
+
+ self.app = app
+ self._total_time_to_transcode = 0
+ self._total_transcoded_time = 0
+ self._start_proxying_time = 0
+ self._estimated_time = 0
+ self.proxy_extension = "proxy.mkv"
+ self.__running_transcoders = []
+ self.__pending_transcoders = []
+
+ self.proxyingUnsupported = False
+ for encoding_format in [ENCODING_FORMAT_JPEG, ENCODING_FORMAT_PRORES]:
+ self.__encoding_profile = self.__getEncodingProfile(encoding_format)
+ if self.__encoding_profile:
+ self.info("Using %s as proxying format", encoding_format)
+ break
+
+ if not self.__encoding_profile:
+ self.proxyingUnsupported = True
+
+ self.error("Not supporting any proxy formats!")
+ return
+
+ def _assetMatchesEncodingFormat(self, asset, encoding_profile):
+ def capsMatch(info, profile):
+ return not info.get_caps().intersect(profile.get_format()).is_empty()
+
+ info = asset.get_info()
+ container = info.get_stream_info()
+ if container:
+ if not capsMatch(container, encoding_profile):
+ return False
+
+ for profile in encoding_profile.get_profiles():
+ if isinstance(profile, GstPbutils.EncodingAudioProfile):
+ audios = info.get_audio_streams()
+ for audio_stream in audios:
+ if not capsMatch(audio_stream, profile):
+ return False
+ elif isinstance(profile, GstPbutils.EncodingVideoProfile):
+ videos = info.get_video_streams()
+ for video_stream in videos:
+ if not capsMatch(video_stream, profile):
+ return False
+ return True
+
+ def __getEncodingProfile(self, encoding_target_file):
+ encoding_target = GstPbutils.EncodingTarget.load_from_file(
+ os.path.join(get_gstpresets_dir(), encoding_target_file))
+ encoding_profile = encoding_target.get_profile("default")
+
+ if not encoding_profile:
+ return None
+
+ for profile in encoding_profile.get_profiles():
+ if not Gst.ElementFactory.list_filter(
+ Gst.ElementFactory.list_get_elements(
+ Gst.ELEMENT_FACTORY_TYPE_ENCODER, Gst.Rank.MARGINAL),
+ profile.get_format(), Gst.PadDirection.SRC, False):
+ return None
+ if not Gst.ElementFactory.list_filter(
+ Gst.ElementFactory.list_get_elements(
+ Gst.ELEMENT_FACTORY_TYPE_DECODER, Gst.Rank.MARGINAL),
+ profile.get_format(), Gst.PadDirection.SINK, False):
+ return None
+ return encoding_profile
+
+ def isProxyAsset(self, obj):
+ if isinstance(obj, GES.Asset):
+ uri = obj.props.id
+ else:
+ uri = obj
+
+ return uri.endswith("." + self.proxy_extension)
+
+ def checkProxyLoadingSucceeded(self, proxy):
+ if self.isProxyAsset(proxy):
+ return True
+
+ self.emit("error-preparing-asset", None, proxy, proxy.get_error())
+ return False
+
+ def getTargetUri(self, proxy_asset):
+ return ".".join(proxy_asset.props.id.split(".")[:-3])
+
+ def getProxyUri(self, asset):
+ """
+ Returns the URI of a possible proxy file. The name looks like:
+ <filename>.<file_size>.<proxy_extension>
+ """
+ asset_file = Gio.File.new_for_uri(asset.get_id())
+ file_size = asset_file.query_info(Gio.FILE_ATTRIBUTE_STANDARD_SIZE,
+ Gio.FileQueryInfoFlags.NONE,
+ None).get_size()
+
+ return "%s.%s.%s" % (asset.get_id(), file_size, self.proxy_extension)
+
+ def isAssetFormatWellSupported(self, asset):
+ for encoding_format in self.WHITELIST_FORMATS:
+ if self._assetMatchesEncodingFormat(asset, encoding_format):
+ self.info("Automatically not proxying")
+ return True
+
+ return False
+
+ def __assetNeedsTranscoding(self, asset, force_proxying=False):
+ if self.proxyingUnsupported:
+ self.info("No proxying supported")
+ return False
+
+ if force_proxying:
+ self.info("Forcing proxy creation")
+ return True
+
+ if self.app.settings.proxyingStrategy == ProxyingStrategy.NOTHING:
+ self.debug("Not proxying anything. %s",
+ self.app.settings.proxyingStrategy)
+ return False
+
+ if self.app.settings.proxyingStrategy == ProxyingStrategy.AUTOMATIC \
+ and not self.isProxyAsset(asset) and \
+ self.isAssetFormatWellSupported(asset):
+ return False
+
+ if not self._assetMatchesEncodingFormat(asset, self.__encoding_profile):
+ return True
+
+ self.info("%s does not need proxy", asset.get_id())
+ return False
+
+ def __startTranscoder(self, transcoder):
+ self.debug("Starting %s", transcoder.props.src_uri)
+ if self._start_proxying_time == 0:
+ self._start_proxying_time = time.time()
+ transcoder.run_async()
+ self.__running_transcoders.append(transcoder)
+
+ def __assetsMatch(self, asset, proxy):
+ if self.__assetNeedsTranscoding(proxy):
+ return False
+
+ info = asset.get_info()
+ if info.get_duration() != asset.get_duration():
+ return False
+
+ return True
+
+ def __assetLoadedCb(self, proxy, res, asset, transcoder):
+ try:
+ GES.Asset.request_finish(res)
+ except GLib.Error as e:
+ if transcoder:
+ self.emit("error-preparing-asset", asset, proxy, e)
+ del transcoder
+ else:
+ self.__createTranscoder(asset)
+
+ return
+
+ if not transcoder:
+ if not self.__assetsMatch(asset, proxy):
+ return self.__createTranscoder(asset)
+ else:
+ transcoder.props.pipeline.props.video_filter.finalize(proxy)
+ transcoder.props.pipeline.props.audio_filter.finalize(proxy)
+
+ del transcoder
+
+ self.emit("proxy-ready", asset, proxy)
+ self.__emitProgress(proxy, 100)
+
+ def __transcoderErrorCb(self, transcoder, error, asset):
+ self.emit("error-preparing-asset", asset, None, error)
+
+ def __transcoderDoneCb(self, transcoder, asset):
+ 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)
+ 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)
+
+ try:
+ self.__startTranscoder(self.__pending_transcoders.pop())
+ except IndexError:
+ if not self.__running_transcoders:
+ self._total_transcoded_time = 0
+ self._total_time_to_transcode = 0
+ self._start_proxying_time = 0
+
+ def __emitProgress(self, asset, progress):
+ if self._total_transcoded_time:
+ time_spent = time.time() - self._start_proxying_time
+ self._estimated_time = max(
+ 0, (time_spent * self._total_time_to_transcode /
+ self._total_transcoded_time) - time_spent)
+ else:
+ self._estimated_time = 0
+
+ asset.creation_progress = progress
+ self.emit("progress", asset, asset.creation_progress,
+ self._estimated_time)
+
+ def __proxyingPositionChangedCb(self, transcoder, position, asset):
+ # Do not set to >= 100 as we need to notify about the proxy first
+ self._total_transcoded_time -= (asset.creation_progress * (asset.get_duration() /
+ Gst.SECOND)) / 100
+ self._total_transcoded_time += position / Gst.SECOND
+
+ if transcoder.props.duration:
+ asset.creation_progress = max(
+ 0, min(99, (position / transcoder.props.duration) * 100))
+
+ self.__emitProgress(asset, asset.creation_progress)
+
+ def __assetQueued(self, asset):
+ all_transcoders = self.__running_transcoders + self.__pending_transcoders
+ for transcoder in all_transcoders:
+ if asset.props.id == transcoder.props.src_uri:
+ return True
+
+ return False
+
+ def __createTranscoder(self, asset):
+ self._total_time_to_transcode += asset.get_duration() / Gst.SECOND
+ asset_uri = asset.get_id()
+ proxy_uri = self.getProxyUri(asset)
+
+ dispatcher = GstTranscoder.TranscoderGMainContextSignalDispatcher.new()
+ transcoder = GstTranscoder.Transcoder.new_full(
+ asset_uri, proxy_uri + ".part", self.__encoding_profile,
+ dispatcher)
+ transcoder.props.position_update_interval = 1000
+
+ thumbnailbin = Gst.ElementFactory.make("teedthumbnailbin")
+ thumbnailbin.props.uri = asset.get_id()
+
+ waveformbin = Gst.ElementFactory.make("waveformbin")
+ waveformbin.props.uri = asset.get_id()
+ waveformbin.props.duration = asset.get_duration()
+
+ transcoder.props.pipeline.props.video_filter = thumbnailbin
+ transcoder.props.pipeline.props.audio_filter = waveformbin
+
+ transcoder.set_cpu_usage(10)
+ transcoder.connect("position-updated",
+ self.__proxyingPositionChangedCb,
+ asset)
+
+ 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:
+ self.__pending_transcoders.append(transcoder)
+
+ def cancelJob(self, asset):
+ if not self.__assetQueued(asset):
+ return
+
+ for transcoder in self.__running_transcoders:
+ if asset.props.id == transcoder.props.src_uri:
+ self.__running_transcoders.remove(transcoder)
+ self.info("Cancelling running transcoder %s %s",
+ transcoder.props.src_uri,
+ transcoder.__grefcount__)
+ self.emit("asset-preparing-cancelled", asset)
+ return
+
+ for transcoder in self.__pending_transcoders:
+ if asset.props.id == transcoder.props.src_uri:
+ # Removing the transcoder from the list
+ # will lead to its destruction (only reference)
+ # here, which means it will be stopped.
+ self.__pending_transcoders.remove(transcoder)
+ self.emit("asset-preparing-cancelled", asset)
+ self.info("Cancelling pending transcoder %s",
+ transcoder.props.src_uri)
+ return
+
+ return
+
+ def addJob(self, asset, force_proxying=False):
+ self.debug("Maybe create a proxy for %s (strategy: %s)",
+ asset.get_id(), self.app.settings.proxyingStrategy)
+
+ if not self.__assetNeedsTranscoding(asset, force_proxying):
+ self.debug("Not proxying asset (settings.proxyingStrategy: %s,"
+ " proxy support forced: %s disabled: %s)",
+ self.app.settings.proxyingStrategy,
+ force_proxying, self.proxyingUnsupported)
+
+ # Make sure to notify we do not need a proxy for
+ # that asset.
+ self.emit("proxy-ready", asset, None)
+ return True
+
+ if self.__assetQueued(asset):
+ return True
+
+ proxy_uri = self.getProxyUri(asset)
+ 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)
+ return True
+
+ self.__createTranscoder(asset)
+ return True
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index c73426c..0c3594b 100644
--- a/pitivi/utils/ui.py
+++ b/pitivi/utils/ui.py
@@ -45,7 +45,7 @@ from gi.repository.GstPbutils import DiscovererVideoInfo, DiscovererAudioInfo,\
DiscovererStreamInfo, DiscovererSubtitleInfo, DiscovererInfo
from pitivi.utils.loggable import doLog, ERROR
-from pitivi.utils.misc import path_from_uri
+from pitivi.utils.misc import path_from_uri, get_proxy_target
from pitivi.configure import get_pixmap_dir
@@ -253,7 +253,7 @@ def set_cairo_color(context, color):
context.set_source_rgb(*cairo_color)
-def beautify_info(info):
+def beautify_asset(asset):
"""
Formats the specified info for display.
@@ -271,6 +271,8 @@ def beautify_info(info):
except KeyError:
return len(ranks)
+ info = asset.get_info()
+ uri = get_proxy_target(asset).props.id
info.get_stream_list().sort(key=stream_sort_key)
nice_streams_txts = []
for stream in info.get_stream_list():
@@ -282,8 +284,13 @@ def beautify_info(info):
if beautified_string:
nice_streams_txts.append(beautified_string)
- return ("<b>" + path_from_uri(info.get_uri()) + "</b>\n" +
- "\n".join(nice_streams_txts))
+ res = "<b>" + path_from_uri(uri) + "</b>\n" + "\n".join(nice_streams_txts)
+
+ if asset.creation_progress < 100:
+ res += _("\n<b>Proxy creation progress: ") + \
+ "</b>%d%%" % asset.creation_progress
+
+ return res
def info_name(info):
@@ -293,7 +300,7 @@ def info_name(info):
@type info: L{GES.Asset} or L{DiscovererInfo}
"""
if isinstance(info, GES.Asset):
- filename = urllib.parse.unquote(os.path.basename(info.get_id()))
+ 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()))
else:
@@ -303,6 +310,9 @@ def info_name(info):
def beautify_stream(stream):
if type(stream) is DiscovererAudioInfo:
+ if stream.get_depth() == 0:
+ return None
+
templ = ngettext(
"<b>Audio:</b> %d channel at %d <i>Hz</i> (%d <i>bits</i>)",
"<b>Audio:</b> %d channels at %d <i>Hz</i> (%d <i>bits</i>)",
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 7b138c0..9d4744d 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -9,9 +9,11 @@ tests = \
test_common.py \
test_log.py \
test_mainwindow.py \
+ test_media_library.py \
test_misc.py \
test_prefs.py \
test_preset.py \
+ test_previewers.py \
test_project.py \
test_system.py \
test_timeline_layer.py \
diff --git a/tests/common.py b/tests/common.py
index f3ee7fa..442e9dd 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -18,27 +18,34 @@ from pitivi.application import Pitivi
from pitivi.utils.loggable import Loggable
from pitivi.utils.timeline import Selected
from pitivi.utils.validate import Event
+from pitivi.utils.proxy import ProxyManager, ProxyingStrategy
detect_leaks = os.environ.get("PITIVI_TEST_DETECT_LEAKS", "0") not in ("0", "")
os.environ["PITIVI_USER_CACHE_DIR"] = tempfile.mkdtemp("pitiviTestsuite")
-def cleanPitiviMock(ptv):
- ptv.settings = None
+def cleanPitiviMock(app):
+ app.settings = None
+ app.proxy_manager = None
-def getPitiviMock(settings=None):
- ptv = mock.MagicMock()
+def getPitiviMock(settings=None, proxyingStrategy=ProxyingStrategy.NOTHING,
+ numTranscodingJobs=4):
+ app = mock.MagicMock()
- ptv.write_action = mock.MagicMock(spec=Pitivi.write_action)
+ app.write_action = mock.MagicMock(spec=Pitivi.write_action)
check.check_requirements()
if not settings:
settings = mock.MagicMock()
- ptv.settings = settings
+ settings.proxyingStrategy = proxyingStrategy
+ settings.numTranscodingJobs = numTranscodingJobs
- return ptv
+ app.settings = settings
+ app.proxy_manager = ProxyManager(app)
+
+ return app
class TestCase(unittest.TestCase, Loggable):
@@ -129,7 +136,18 @@ class TestCase(unittest.TestCase, Loggable):
def getSampleUri(sample):
assets_dir = os.path.dirname(os.path.abspath(__file__))
- return "file://%s/samples/%s" % (assets_dir, sample)
+
+ return "file://%s" % os.path.join(assets_dir, "samples", sample)
+
+
+def cleanProxySamples():
+ _dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "samples")
+ proxy_manager = ProxyManager(mock.MagicMock())
+
+ for f in os.listdir(_dir):
+ if f.endswith(proxy_manager.proxy_extension):
+ f = os.path.join(_dir, f)
+ os.remove(f)
class SignalMonitor(object):
diff --git a/tests/runtests.py b/tests/runtests.py
index e7c5aba..4883fce 100644
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -45,12 +45,25 @@ def get_build_dir():
return os.path.abspath(build_dir)
+def _prepend_env_path(name, value):
+ os.environ[name] = os.pathsep.join(value +
+ os.environ.get(name, "").split(
+ os.pathsep))
+
+
def setup():
res = True
# Make available to configure.py the top level dir.
pitivi_dir = get_pitivi_dir()
os.environ.setdefault('PITIVI_TOP_LEVEL_DIR', pitivi_dir)
+ _prepend_env_path("GST_PRESET_PATH", [
+ os.path.join(pitivi_dir, "data", "videopresets"),
+ os.path.join(pitivi_dir, "data", "audiopresets")])
+
+ _prepend_env_path("GST_ENCODING_TARGET_PATH", [
+ os.path.join(pitivi_dir, "data", "encoding-profiles")])
+
# Make available the compiled C code.
build_dir = get_build_dir()
libs_dir = os.path.join(build_dir, "pitivi/coptimizations/.libs")
diff --git a/tests/test_media_library.py b/tests/test_media_library.py
new file mode 100644
index 0000000..4ea08a6
--- /dev/null
+++ b/tests/test_media_library.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2015, Thibault Saunier <tsaunier gnome org>
+#
+# 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.
+
+import os
+
+from unittest import mock
+from gettext import gettext as _
+
+from gi.repository import GES
+from gi.repository import Gst
+from gi.repository import GLib
+
+from pitivi import medialibrary
+from pitivi.project import ProjectManager
+from pitivi.timeline import timeline
+from pitivi.utils.proxy import ProxyingStrategy
+
+from tests import common
+
+
+def fakeSwitchProxies(asset):
+ timeline.TimelineContainer.switchProxies(mock.MagicMock(), asset)
+
+
+class TestMediaLibrary(common.TestCase):
+ def __init__(self, *args):
+ common.TestCase.__init__(self, *args)
+ self.app = None
+ self.medialibrary = None
+ self.mainloop = None
+
+ def tearDown(self):
+ self.clean()
+ common.TestCase.tearDown(self)
+
+ def clean(self):
+ self.mainloop = None
+
+ if self.app:
+ self.app = common.cleanPitiviMock(self.app)
+
+ if self.medialibrary:
+ self.medialibrary.finalize()
+ self.medialibrary = None
+
+ def _customSetUp(self, settings):
+ # Always make sure we start with a clean medialibrary, and no other
+ # is connected to some assets.
+ self.clean()
+
+ self.mainloop = GLib.MainLoop.new(None, False)
+ self.check_no_transcoding = False
+ self.app = common.getPitiviMock(settings)
+ self.app.project_manager = ProjectManager(self.app)
+ self.medialibrary = medialibrary.MediaLibraryWidget(self.app)
+ self.app.project_manager.newBlankProject(ignore_unsaved_changes=True)
+ self.app.project_manager.current_project.connect(
+ "loaded", self.projectLoadedCb)
+ self.mainloop.run()
+
+ def projectLoadedCb(self, unused_project, unused_timeline):
+ self.mainloop.quit()
+
+ def _progressBarCb(self, progressbar, unused_pspecunused):
+ if self.check_no_transcoding:
+ self.assertTrue(progressbar.props.fraction == 1.0 or
+ progressbar.props.fraction == 0.0,
+ "Some transcoding is happening, got progress: %f"
+ % progressbar.props.fraction)
+
+ if progressbar.props.fraction == 1.0:
+ self.assertEqual(len(self.medialibrary.storemodel),
+ len(self.samples))
+ self.mainloop.quit()
+
+ def _createAssets(self, samples):
+ self.samples = samples
+ for sample_name in samples:
+ self.app.project_manager.current_project.create_asset(
+ common.getSampleUri(sample_name), GES.UriClip,)
+
+ def runCheckImport(self, assets, proxying_strategy=ProxyingStrategy.ALL,
+ check_no_transcoding=False, clean_proxies=True):
+ settings = mock.MagicMock()
+ settings.proxyingStrategy = proxying_strategy
+ settings.numTranscodingJobs = 4
+ settings.lastClipView = medialibrary.SHOW_TREEVIEW
+
+ self._customSetUp(settings)
+ self.check_no_transcoding = check_no_transcoding
+
+ self.medialibrary._progressbar.connect(
+ "notify::fraction", self._progressBarCb)
+
+ if clean_proxies:
+ common.cleanProxySamples()
+
+ self._createAssets(assets)
+ self.mainloop.run()
+ self.assertFalse(self.medialibrary._progressbar.props.visible)
+
+ def stopUsingProxies(self, delete_proxies=False):
+ sample_name = "30fps_numeroted_frames_red.mkv"
+ self.runCheckImport([sample_name])
+
+ asset_uri = common.getSampleUri(sample_name)
+ proxy = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+
+ self.assertEqual(proxy.props.proxy_target.props.id, asset_uri)
+
+ self.app.project_manager.current_project.disableProxiesForAssets(
+ [proxy], delete_proxies)
+ self.assertEqual(len(self.medialibrary.storemodel),
+ len(self.samples))
+
+ self.assertEqual(self.medialibrary.storemodel[0][medialibrary.COL_URI],
+ asset_uri)
+
+ def testTranscoding(self):
+ self.runCheckImport(["30fps_numeroted_frames_red.mkv"])
+
+ def testDisableProxies(self):
+ self.runCheckImport(["30fps_numeroted_frames_red.mkv"],
+ ProxyingStrategy.NOTHING, True)
+
+ def testReuseProxies(self):
+ # Create proxies
+ self.runCheckImport(["30fps_numeroted_frames_red.mkv"])
+ self.info("Now trying to import again, checking that no"
+ " transcoding is done.")
+ self.runCheckImport(["30fps_numeroted_frames_red.mkv"],
+ check_no_transcoding=True,
+ clean_proxies=False)
+
+ def testNewlyImportedAssetSelected(self):
+ self.runCheckImport(["30fps_numeroted_frames_red.mkv",
+ "30fps_numeroted_frames_blue.webm"])
+
+ self.assertEqual(len(list(self.medialibrary.getSelectedPaths())),
+ len(self.samples))
+
+ def testStopUsingProxies(self, delete_proxies=False):
+ self.stopUsingProxies()
+
+ def testDeleteProxy(self):
+ self.stopUsingProxies(True)
+
+ asset = self.medialibrary.storemodel[0][medialibrary.COL_ASSET]
+ proxy_uri = self.app.proxy_manager.getProxyUri(asset)
+
+ # Requesting UriClip sync will return None if the asset is not in cache
+ # this way we make sure that this asset used to exist
+ proxy = GES.Asset.request(GES.UriClip, proxy_uri)
+ self.assertIsNotNone(proxy)
+ self.assertFalse(os.path.exists(Gst.uri_get_location(proxy_uri)))
+
+ self.assertIsNone(asset.get_proxy())
+
+ # And let's recreate the proxy file.
+ self.app.project_manager.current_project.useProxiesForAssets(
+ [asset])
+ self.assertEqual(asset.creation_progress, 0)
+
+ # Check that the info column notifies the user about progress
+ self.assertTrue(_("Proxy creation progress: ") in
+ self.medialibrary.storemodel[0][medialibrary.COL_INFOTEXT])
+
+ # Run the mainloop and let _progressBarCb stop it when the proxy is
+ # ready
+ self.mainloop.run()
+
+ self.assertEqual(asset.creation_progress, 100)
+ self.assertEqual(asset.get_proxy(), proxy)
diff --git a/tests/test_previewers.py b/tests/test_previewers.py
new file mode 100644
index 0000000..7fad643
--- /dev/null
+++ b/tests/test_previewers.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2015, Thibault Saunier <tsaunier gnome org>
+#
+# 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.
+
+import os
+import pickle
+
+from unittest import mock
+
+from gi.repository import GES
+from gi.repository import Gst
+
+from tests import common
+from tests.test_media_library import TestMediaLibrary
+
+from pitivi.timeline.previewers import getThumbnailCache, THUMB_HEIGHT, \
+ get_wavefile_location_for_uri
+
+
+class TestPreviewers(common.TestCase):
+ def testCreateThumbnailBin(self):
+ pipeline = Gst.parse_launch("uridecodebin name=decode uri=file:///some/thing"
+ " waveformbin name=wavebin ! fakesink qos=false name=faked")
+ self.assertTrue(pipeline)
+ wavebin = pipeline.get_by_name("wavebin")
+ self.assertTrue(wavebin)
+
+ def testWaveFormAndThumbnailCreated(self):
+ testmedialib = TestMediaLibrary()
+ sample_name = "1sec_simpsons_trailer.mp4"
+ testmedialib.runCheckImport([sample_name])
+
+ sample_uri = common.getSampleUri(sample_name)
+ asset = GES.UriClipAsset.request_sync(sample_uri)
+
+ thumb_cache = getThumbnailCache(asset)
+ width, height = thumb_cache.getImagesSize()
+ self.assertEqual(height, THUMB_HEIGHT)
+ self.assertTrue(thumb_cache[0] is not None)
+ self.assertTrue(thumb_cache[Gst.SECOND / 2] is not None)
+
+ wavefile = get_wavefile_location_for_uri(sample_uri)
+ self.assertTrue(os.path.exists(wavefile), wavefile)
+
+ with open(wavefile, "rb") as fsamples:
+ samples = pickle.load(fsamples)
+
+ self.assertTrue(bool(samples))
diff --git a/tests/test_project.py b/tests/test_project.py
index efa6448..d2be228 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -30,12 +30,13 @@ from gi.repository import Gst
from pitivi.application import Pitivi
from pitivi.project import ProjectManager, Project
from pitivi.utils.misc import uri_is_reachable
+from pitivi.utils.proxy import ProxyingStrategy
from tests import common
def _createRealProject(name=None):
- app = common.getPitiviMock()
+ app = common.getPitiviMock(proxyingStrategy=ProxyingStrategy.NOTHING)
project_manager = ProjectManager(app)
project_manager.newBlankProject()
project = project_manager.current_project
@@ -59,6 +60,9 @@ class MockProject(object):
def disconnect_by_function(self, ignored):
pass
+ def finalize(self):
+ pass
+
class ProjectManagerListener(object):
@@ -358,31 +362,39 @@ class TestProjectLoading(common.TestCase):
os.remove(xges_path)
def testAssetAddingRemovingAdding(self):
- def loaded(project, timeline, mainloop, result, uris):
- result[0] = True
- project.addUris(uris)
+ def loadingProgressCb(project, progress, estimated_time,
+ self, result, uris):
+
+ def readd(mainloop, result, uris):
+ project.addUris(uris)
+ result[2] = True
+ mainloop.quit()
+
+ if progress < 100:
+ return
- def added(project, mainloop, result, uris):
result[1] = True
assets = project.list_assets(GES.UriClip)
+ self.assertEqual(len(assets), 1)
asset = assets[0]
project.remove_asset(asset)
- GLib.idle_add(readd, mainloop, result, uris)
+ GLib.idle_add(readd, self.mainloop, result, uris)
- def readd(mainloop, result, uris):
+ def loadedCb(project, timeline, mainloop, result, uris):
+ result[0] = True
project.addUris(uris)
- result[2] = True
- mainloop.quit()
-
- def quit(mainloop):
- mainloop.quit()
# Create a blank project and add an asset.
project = _createRealProject()
result = [False, False, False]
uris = [common.getSampleUri("tears_of_steel.webm")]
- project.connect("loaded", loaded, self.mainloop, result, uris)
- project.connect("done-importing", added, self.mainloop, result, uris)
+ project.connect("loaded", loadedCb, self.mainloop, result, uris)
+ project.connect("asset-loading-progress",
+ loadingProgressCb, self,
+ result, uris)
+
+ def quit(mainloop):
+ mainloop.quit()
self.assertTrue(project.createTimeline())
GLib.timeout_add_seconds(5, quit, self.mainloop)
@@ -390,7 +402,8 @@ class TestProjectLoading(common.TestCase):
self.assertTrue(
result[0], "Project creation failed to trigger signal: loaded")
self.assertTrue(
- result[1], "Asset add failed to trigger signal: done-importing")
+ result[1], "Asset add failed to trigger asset-loading-progress"
+ "with progress == 100")
self.assertTrue(result[2], "Asset re-adding failed")
@@ -421,11 +434,12 @@ class TestProjectSettings(common.TestCase):
self.assertEqual(Gst.Fraction(2, 7), project.videopar)
def testInitialization(self):
- def loaded(project, timeline, mainloop, uris):
+ def loadedCb(project, timeline, mainloop, uris):
project.addUris(uris)
- def added(project, mainloop):
- mainloop.quit()
+ def progressCb(project, progress, estimated_time, mainloop):
+ if progress == 100:
+ mainloop.quit()
def quit(mainloop):
mainloop.quit()
@@ -438,8 +452,8 @@ class TestProjectSettings(common.TestCase):
uris = [common.getSampleUri("flat_colour1_640x480.png"),
common.getSampleUri("tears_of_steel.webm"),
common.getSampleUri("1sec_simpsons_trailer.mp4")]
- project.connect("loaded", loaded, self.mainloop, uris)
- project.connect("done-importing", added, self.mainloop)
+ project.connect("loaded", loadedCb, self.mainloop, uris)
+ project.connect("asset-loading-progress", progressCb, self.mainloop)
self.assertTrue(project.createTimeline())
GLib.timeout_add_seconds(5, quit, self.mainloop)
@@ -462,7 +476,7 @@ class TestProjectSettings(common.TestCase):
self.assertEqual(Gst.Fraction(1, 1), project.videopar)
def testLoad(self):
- ptv = common.getPitiviMock()
+ ptv = common.getPitiviMock(proxyingStrategy=ProxyingStrategy.NOTHING)
project = Project(uri="fake.xges", app=ptv)
self.assertFalse(project._has_default_video_settings)
self.assertFalse(project._has_default_audio_settings)
diff --git a/tests/test_timeline_timeline.py b/tests/test_timeline_timeline.py
index 8d1aebb..794a83f 100644
--- a/tests/test_timeline_timeline.py
+++ b/tests/test_timeline_timeline.py
@@ -18,7 +18,6 @@
# Boston, MA 02110-1301, USA.
from unittest import mock
-
from gi.repository import Gdk
from gi.repository import GES
from gi.repository import Gtk
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]