[pitivi] Implement proxy editing



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.
+&lt;i&gt;Use at your own risk!&lt;/i&gt;</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]