[pitivi/T3404-intermediary_format: 4/4] Implement proxy editing



commit 6d87adf1982a22a306feb05c13d115facb6929b7
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, allowing the user to
    avoid doing so.
    
    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
    
    Differential Revision: https://phabricator.freedesktop.org/D505

 bin/pitivi-git-environment.sh                      |    7 +-
 bin/pitivi.in                                      |   10 +
 configure.ac                                       |    1 +
 data/Makefile.am                                   |    2 +-
 data/encoding-profiles/Makefile.am                 |    7 +
 data/encoding-profiles/jpeg-flac-in-matroska.gep   |   45 ++
 data/encoding-profiles/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                         |   18 +
 data/videopresets/GstJpegEnc.prs                   |   15 +
 data/videopresets/Makefile.am                      |    6 +-
 pitivi/application.py                              |    2 +
 pitivi/check.py                                    |    2 +
 pitivi/medialibrary.py                             |  503 ++++++++++++++++----
 pitivi/project.py                                  |  212 +++++++--
 pitivi/render.py                                   |   28 ++
 pitivi/timeline/elements.py                        |   12 +-
 pitivi/timeline/layer.py                           |    5 +-
 pitivi/timeline/previewers.py                      |  295 ++++++++++--
 pitivi/timeline/timeline.py                        |   24 +
 pitivi/utils/Makefile.am                           |    1 +
 pitivi/utils/misc.py                               |   24 +
 pitivi/utils/transcoding.py                        |  348 ++++++++++++++
 pitivi/utils/ui.py                                 |   20 +-
 tests/Makefile.am                                  |    2 +
 tests/common.py                                    |   56 ++-
 tests/test_media_library.py                        |  194 ++++++++
 tests/test_previewers.py                           |   61 +++
 tests/test_project.py                              |   67 ++-
 tests/test_timeline_layer.py                       |    2 +-
 tests/test_timeline_timeline.py                    |    7 +-
 tests/test_undo_timeline.py                        |    2 +-
 35 files changed, 2365 insertions(+), 236 deletions(-)
---
diff --git a/bin/pitivi-git-environment.sh b/bin/pitivi-git-environment.sh
index 724029d..68f9b6b 100755
--- a/bin/pitivi-git-environment.sh
+++ b/bin/pitivi-git-environment.sh
@@ -91,6 +91,7 @@ else
 fi
 EXTRA_PATH="$EXTRA_PATH:$PITIVI/gst-devtools/validate/tools"
 EXTRA_PATH="$EXTRA_PATH:$PITIVI_PREFIX/bin"
+EXTRA_PATH="$EXTRA_PATH:$PITIVI/gst-plugins-bad/tools"
 
 # Make the built tools available.
 export PATH="$EXTRA_PATH:$PATH"
@@ -120,7 +121,7 @@ else
     done
 
     # GStreamer plugins bad libraries
-    for path in basecamerabinsrc codecparsers uridownloader egl gl insertbin interfaces mpegts; do
+    for path in basecamerabinsrc codecparsers uridownloader egl gl insertbin interfaces mpegts transcoding; 
do
         LD_LIBRARY_PATH=$PITIVI/gst-plugins-bad/gst-libs/gst/$path/.libs:$LD_LIBRARY_PATH
         DYLD_LIBRARY_PATH=$PITIVI/gst-plugins-bad/gst-libs/gst/$path/.libs:$DYLD_LIBRARY_PATH
         GI_TYPELIB_PATH=$PITIVI/gst-plugins-bad/gst-libs/gst/$path:$GI_TYPELIB_PATH
@@ -143,7 +144,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\
@@ -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
diff --git a/bin/pitivi.in b/bin/pitivi.in
index 6d94d87..ad3b3a7 100644
--- a/bin/pitivi.in
+++ b/bin/pitivi.in
@@ -73,10 +73,20 @@ def _add_pitivi_path():
         root = os.path.split(dir)[0]
         sys.path.append(os.path.join(root, "pitivi", "coptimizations", ".libs"))
         localedir = os.path.join(os.path.split(dir)[0], 'locale')
+        _prepend_env_path("GST_PRESET_PATH", [os.path.join(
+            root, "data", "videopresets"), os.path.join(root, "data", "audiopresets")])
+        _prepend_env_path("GST_ENCODING_TARGET_PATH", [os.path.join(
+            root, "data", "encoding-profiles")])
     else:
         root = os.path.join(LIBDIR, 'pitivi', 'python')
         localedir = os.path.join(DATADIR, "locale")
 
+        data_dir = os.path.join(DATADIR, "pitivi")
+        _prepend_env_path("GST_PRESET_PATH", [os.path.join(
+            data_dir, "videopresets"), os.path.join(data_dir, "audiopresets")])
+        _prepend_env_path("GST_ENCODING_TARGET_PATH", [os.path.join(
+            data_dir, "encoding-profiles")])
+
     if not root in sys.path:
         sys.path.append(root)
 
diff --git a/configure.ac b/configure.ac
index efd90b6..2d48f98 100644
--- a/configure.ac
+++ b/configure.ac
@@ -161,4 +161,5 @@ data/ui/Makefile
 data/renderpresets/Makefile
 data/audiopresets/Makefile
 data/videopresets/Makefile
+data/encoding-profiles/Makefile
 )
diff --git a/data/Makefile.am b/data/Makefile.am
index 8f9d34e..e796302 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 encoding-profiles
 
 desktopdir = $(datadir)/applications
 desktop_in_files = pitivi.desktop.in
diff --git a/data/encoding-profiles/Makefile.am b/data/encoding-profiles/Makefile.am
new file mode 100644
index 0000000..cadc27f
--- /dev/null
+++ b/data/encoding-profiles/Makefile.am
@@ -0,0 +1,7 @@
+encodingprofilesdir = $(pkgdatadir)/encoding-profiles
+encodingprofiles_DATA = \
+       jpeg-flac-in-matroska.gep \
+       prores-flac-in-matroska.gep
+
+EXTRA_DIST = \
+       $(encodingprofiles_DATA)
diff --git a/data/encoding-profiles/jpeg-flac-in-matroska.gep 
b/data/encoding-profiles/jpeg-flac-in-matroska.gep
new file mode 100644
index 0000000..92cf32b
--- /dev/null
+++ b/data/encoding-profiles/jpeg-flac-in-matroska.gep
@@ -0,0 +1,45 @@
+[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
+
+[profile-high-quality]
+name=high-quality
+type=container
+description[c]=Matroska muxer with default configs
+format=video/x-matroska
+
+[streamprofile-flac-high-quality]
+parent=high-quality
+type=audio
+format=audio/x-flac
+presence=0
+
+[streamprofile-jpeg-high-quality]
+parent=high-quality
+type=video
+format=image/jpeg
+presence=0
+pass=0
+variableframerate=false
+preset=Quality High
diff --git a/data/encoding-profiles/prores-flac-in-matroska.gep 
b/data/encoding-profiles/prores-flac-in-matroska.gep
new file mode 100644
index 0000000..be5884f
--- /dev/null
+++ b/data/encoding-profiles/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 e16f017..ac62bdd 100644
--- a/data/pixmaps/Makefile.am
+++ b/data/pixmaps/Makefile.am
@@ -20,7 +20,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..bdf127b 100644
--- a/data/ui/renderingdialog.ui
+++ b/data/ui/renderingdialog.ui
@@ -744,6 +744,24 @@
                 <property name="top_attach">1</property>
               </packing>
             </child>
+            <child>
+              <object class="GtkCheckButton" id="render_from_proxies">
+                <property name="label" translatable="yes">Render from proxies</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="xalign">0</property>
+                <property name="yalign">0.31000000238418579</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">2</property>
+              </packing>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
           </object>
           <packing>
             <property name="expand">False</property>
diff --git a/data/videopresets/GstJpegEnc.prs b/data/videopresets/GstJpegEnc.prs
new file mode 100644
index 0000000..22894de
--- /dev/null
+++ b/data/videopresets/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/videopresets/Makefile.am b/data/videopresets/Makefile.am
index 67adc30..7ee1146 100644
--- a/data/videopresets/Makefile.am
+++ b/data/videopresets/Makefile.am
@@ -4,7 +4,9 @@ videopresets_DATA = \
     720p24.json \
     720p30.json        \
     HDV_1080i30.json \
-    iPod.json
+    iPod.json \
+    GstJpegEnc.prs
 
 EXTRA_DIST = \
-       $(videopresets_DATA)
+       $(videopresets_DATA) \
+       $(gstvideopresets_DATA)
diff --git a/pitivi/application.py b/pitivi/application.py
index ef10a5a..e20dc7a 100644
--- a/pitivi/application.py
+++ b/pitivi/application.py
@@ -43,6 +43,7 @@ from pitivi.utils.misc import quote_uri, path_from_uri
 from pitivi.utils.system import getSystem
 from pitivi.utils.loggable import Loggable
 from pitivi.utils.timeline import Zoomable
+from pitivi.utils.transcoding import ProxyManager
 import pitivi.utils.loggable as log
 
 
@@ -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/medialibrary.py b/pitivi/medialibrary.py
index cd237b6..ffd75dc 100644
--- a/pitivi/medialibrary.py
+++ b/pitivi/medialibrary.py
@@ -49,9 +49,12 @@ from pitivi.dialogs.filelisterrordialog import FileListErrorDialog
 from pitivi.mediafilespreviewer import PreviewWidget
 from pitivi.settings import GlobalSettings
 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,\
+    getProxyTarget
+from pitivi.timeline.previewers import getThumbnailCache
+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 +78,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 +86,10 @@ 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)))
+
+MIN_PROGRESS_COLUMN_WIDTH = 70
 
 ui = '''
 <ui>
@@ -98,14 +104,99 @@ ui = '''
 # 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 FileChooseExtraWidget(Gtk.Box, Loggable):
+    def __init__(self, app):
+        Loggable.__init__(self)
+        Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=SPACING)
+        self.app = app
+
+        self.__close_after = Gtk.CheckButton(label=_("Close after importing files"))
+        self.__close_after.set_active(self.app.settings.closeImportDialog)
+        self.pack_start(self.__close_after, False, False, PADDING)
+        self.__use_proxies = Gtk.CheckButton(label=_("Use proxy files"))
+        self.__use_proxies.set_active(self.app.settings.useProxies)
+        self.pack_start(self.__use_proxies, False, False, PADDING)
+        self.show_all()
+
+    def saveValues(self):
+        self.app.settings.closeImportDialog = self.__close_after.get_active()
+        self.app.settings.useProxies = self.__use_proxies.get_active()
+
+
+class ThumbnailsDecorator():
+    EMBLEMS = {}
+    PROXIED = "asset-proxied"
+    NO_PROXY = "no-proxy"
+    IN_PROGRESS = "asset-proxy-in-progress"
+
+    DEFAULT_ALPHA = 255
+
+    for status in [PROXIED, IN_PROGRESS]:
+        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):
+        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 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):
@@ -130,7 +221,8 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
         self.dragged = False
         self.clip_view = self.app.settings.lastClipView
         self.import_start_time = time.time()
-        self._last_imported_uris = []
+        self._last_imported_uris = set()
+        self.__last_transcoding_estimate_time = _("Unknown")
 
         self.set_orientation(Gtk.Orientation.VERTICAL)
         builder = Gtk.Builder()
@@ -163,6 +255,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()
@@ -219,7 +313,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)
 
@@ -258,7 +352,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)
 
@@ -319,6 +413,24 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
 
         self.thumbnailer = MediaLibraryWidget._getThumbnailer()
 
+    def finalize(self):
+        if not self._project:
+            return
+
+        for asset in self._project.list_assets(GES.Extractable):
+            try:
+                asset.disconnect_by_func(self.__assetProxiedCb)
+                asset.disconnect_by_func(self.__assetProxyingCb)
+            except TypeError:
+                pass
+
+        self.debug("Finalizing %s", self)
+        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:
@@ -353,7 +465,7 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
                 self.debug("Found asset: %s for uri: %s" % (asset, uri))
                 return asset
 
-        self.warning("Did not find any asser for uri: %s" % (uri))
+        self.warning("Did not find any asset for uri: %s" % (uri))
 
     def _setupViewAsDragAndDropSource(self, view):
         view.drag_source_set(0, [], Gdk.DragAction.COPY)
@@ -364,6 +476,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()
 
@@ -420,17 +538,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("error-loading-asset2", self._errorCreatingAssetCb)
         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.
@@ -454,8 +567,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"""
@@ -465,16 +594,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 = FileChooseExtraWidget(self.app)
         self._importDialog.set_default_response(Gtk.ResponseType.OK)
         self._importDialog.set_select_multiple(True)
         self._importDialog.set_modal(True)
@@ -489,46 +615,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
@@ -561,7 +659,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
@@ -589,18 +686,21 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
         LARGE_SIZE = 96
         info = asset.get_info()
 
+        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 = getProxyTarget(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']
@@ -622,78 +722,143 @@ 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)
+        uri = getProxyTarget(asset).props.id
+        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_transcoding_files = [asset for asset in project.loading_assets if not asset.ready]
+                if estimated_time:
+                    self.__last_transcoding_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_transcoding_files), progress,
+                     self.__last_transcoding_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.__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):
+        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 isinstance(asset, GES.UriClipAsset) and not asset.error:
+            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):
+        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.error("Trying to removed %s but that was not found"
+                       "in the liststore", uri)
 
     def _errorCreatingAssetCb(self, unused_project, error, id, type):
         """ The given uri isn't a media file """
         if GObject.type_is_a(type, GES.UriClip):
             error = (id, str(error.domain), error)
             self._errors.append(error)
-            self._updateProgressbar()
 
-    def _sourcesStartedImportingCb(self, project):
+    def _startImporting(self, project):
+        self.__last_transcoding_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()
@@ -742,7 +907,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:
@@ -781,8 +946,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:
@@ -811,8 +975,14 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
         self.app.action_log.begin("remove clip from source list")
         for row in rows:
             asset = model[row.get_path()][COL_ASSET]
+            proxy_target = asset.get_proxy_target()
             self.app.project_manager.current_project.remove_asset(asset)
             self.app.gui.timeline_ui.purgeObject(asset.props.id)
+            if proxy_target:
+                self.app.project_manager.current_project.remove_asset(
+                    proxy_target)
+                self.app.gui.timeline_ui.purgeObject(proxy_target.props.id)
+            self.debug("Removing %s", asset.get_id())
         self.app.action_log.commit()
 
     def _sourceIsUsed(self, asset):
@@ -914,7 +1084,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))
@@ -922,6 +1106,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))
@@ -964,6 +1149,98 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
         elif self.clip_view == SHOW_ICONVIEW:
             self.iconview.unselect_all()
 
+    def __stopUsingProxyCb(self,
+                           unused_simple_action,
+                           unused_parametter):
+        self._project.disableProxiesForAssets(self.getSelectedAssets())
+
+    def __useProxiesCb(self, unused_simple_action, unused_parametter):
+        self._project.useProxiesForAssets(self.getSelectedAssets())
+
+    def __deleteProxiesCb(self, unused_simple_action, unused_parametter):
+        self._project.disableProxiesForAssets(self.getSelectedAssets(), True)
+
+    def __createMenuModel(self):
+        assets = self.getSelectedAssets()
+        action_group = Gio.SimpleActionGroup()
+        menu_model = Gio.Menu()
+
+        if self.app.proxy_manager.proxyingUnsupported:
+            return None, None
+
+        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 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 proxy file",
+                            "Delete 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
+
+        res = True
+        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:
+                    ts = view.get_selection()
+                    ts.unselect_all()
+                    ts.select_path(path)
+
+        model, action_group = self.__createMenuModel()
+        if not model:
+            return res
+
+        popover = Gtk.Popover.new_from_model(view, model)
+        popover.insert_action_group("assets", action_group)
+        popover.props.position = Gtk.PositionType.BOTTOM
+        rect = Gdk.Rectangle()
+        rect.x = event.x
+        rect.y = event.y
+        rect.width = 1
+        rect.height = 1
+        popover.set_pointing_to(rect)
+        popover.show_all()
+
+        return res
+
     def _treeViewButtonPressEventCb(self, treeview, event):
         self._updateDraggedPaths(treeview, event)
 
@@ -994,12 +1271,16 @@ class MediaLibraryWidget(Gtk.Box, Loggable):
         else:
             self._draggedPaths = None
 
-    def _treeViewButtonReleaseEventCb(self, unused_treeview, event):
+    def _treeViewButtonReleaseEventCb(self, treeview, event):
         ts = self.treeview.get_selection()
         state = event.get_state() & (
             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()
             if path:
@@ -1048,6 +1329,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)
@@ -1057,13 +1343,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._errorCreatingAssetCb)
+        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:
@@ -1119,9 +1420,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 a0e9af0..f8a8741 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 :/
+        "error-loading-asset2": (GObject.SignalFlags.RUN_LAST, None,
+                                 (object, object, GObject.TYPE_GTYPE)),
         "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,
@@ -731,6 +737,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.__errorTranscodingCb)
+        self.app.proxy_manager.connect("asset-preparing-cancelled",
+                                       self.__assetTranscodingCancelledCb)
+        self.app.proxy_manager.connect("proxy-ready",
+                                       self.__proxyReadyCb)
 
         # GstValidate
         self.scenario = scenario
@@ -1013,34 +1028,151 @@ 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):
+        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) * 100
+            self.asset_loading_progress += (asset_weight *
+                                            asset.creation_progress) / 100
+
+            if asset.creation_progress < 100:
+                all_ready = False
+            elif not asset.ready:
+                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, True)
+        self.__updateAssetLoadingProgress(0)
+
+    def __errorTranscodingCb(self, unused_proxy_manager, asset, proxy,
+                             error):
+        asset.error = error
+
+        self.emit("error-loading-asset2", error, asset.get_id(),
+                  asset.get_extractable_type())
+
+        asset.creation_progress = 100
+        self.__updateAssetLoadingProgress(0)
+
+    def __proxyReadyCb(self, unused_proxy_manager, asset, proxy):
+        self.__setProxy(asset, proxy)
+
+    def __setProxy(self, asset, proxy, no_emit=False):
+        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 not no_emit:
+            self.emit("asset-added", asset)
+
+        if proxy:
+            self.loading_assets.append(proxy)
+
+        self.__updateAssetLoadingProgress(0)
+
     # ------------------------------------------ #
     # 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)
+
+        asset.error = None
+        asset.creation_progress = 0
+        asset.ready = False
+        asset.force_proxying = False
+        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 a "
+                       "not proxied asset", asset.get_id())
+            return
+
+        if self.loaded:
+            if not asset.get_proxy_target() in self.list_assets(GES.Extractable):
+                # We do not create proxies when loading a project but
+                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(0)
 
-    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(0)
 
     def do_loaded(self, unused_timeline):
         """ vmethod, get called on "loaded" """
@@ -1048,11 +1180,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.
@@ -1090,6 +1222,38 @@ 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.__errorTranscodingCb)
+        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:
+            asset_target = asset.get_proxy_target()
+            if not asset_target:
+                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:
+            asset_target = asset.get_proxy_target()
+            if asset_target:
+                self.debug("Stop proxying %s", asset_target.props.id)
+                asset_target.set_proxy(None)
+                if delete_proxy_file:
+                    os.remove(Gst.uri_get_location(asset.props.id))
+            else:
+                self.app.proxy_manager.cancelJob(asset)
+
     def hasDefaultName(self):
         return DEFAULT_NAME == self.name
 
@@ -1116,8 +1280,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)
@@ -1154,7 +1316,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 = []
@@ -1370,19 +1531,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 e905bd2..d588ed5 100644
--- a/pitivi/render.py
+++ b/pitivi/render.py
@@ -369,6 +369,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()
@@ -503,6 +504,7 @@ class RenderDialog(Loggable):
         self.resolution_label = builder.get_object("resolution_label")
         self.presets_combo = builder.get_object("presets_combo")
         self.preset_menubutton = builder.get_object("preset_menubutton")
+        self.__render_from_proxies = builder.get_object("render_from_proxies")
 
         self.render_presets.setupUi(self.presets_combo, self.preset_menubutton)
 
@@ -682,6 +684,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)
@@ -727,6 +730,30 @@ class RenderDialog(Loggable):
         canberra = pycanberra.Canberra()
         canberra.play(1, pycanberra.CA_PROP_EVENT_ID, "complete-media", None)
 
+    def __useSourceAsset(self):
+        if self.__render_from_proxies.get_active():
+            self.debug("Rendering from proxies, not replacing assets")
+            return
+
+
+        for layer in self.timeline_ui.bTimeline.get_layers():
+            for clip in layer.get_clips():
+                if isinstance(clip, GES.UriClip):
+                    asset = clip.get_asset()
+                    asset_target = asset.get_proxy_target()
+                    if asset_target and 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
@@ -739,6 +766,7 @@ class RenderDialog(Loggable):
         The render button inside the render dialog has been clicked,
         start the rendering process.
         """
+        self.__useSourceAsset()
         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 dec45af..21e3f7a 100644
--- a/pitivi/timeline/elements.py
+++ b/pitivi/timeline/elements.py
@@ -413,6 +413,7 @@ class TimelineElement(Gtk.Layout, timelineUtils.Zoomable, Loggable):
     # Gtk implementation
     def do_set_property(self, property_id, value, pspec):
         Gtk.Layout.do_set_property(self, property_id, value, pspec)
+        self.passthrough = os.path.exists(self.wavefile)
 
     def __showKeyframes(self):
         if self.timeline.app.project_manager.current_project.pipeline.getState() == Gst.State.PLAYING:
@@ -912,10 +913,17 @@ 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(self.bClip.get_uri()))
 
-        self.set_tooltip_markup(misc.filename_from_uri(bClip.get_uri()))
         self.bClip.selected.connect("selected-changed", self._selectedChangedCb)
 
+    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)
 
@@ -977,7 +985,7 @@ class TransitionClip(Clip):
             markup = str(self.bClip.props.vtype.value_nick)
         else:
             markup = _("Audio crossfade")
-        self.set_tooltip_markup(markup)
+        tooltip.set_markup(markup)
 
         return True
 
diff --git a/pitivi/timeline/layer.py b/pitivi/timeline/layer.py
index ad9be0d..1d5ed78 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
diff --git a/pitivi/timeline/previewers.py b/pitivi/timeline/previewers.py
index 6f09a45..ab8495a 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, \
+        getProxyTarget
 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.error("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(getProxyTarget(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()
@@ -390,6 +582,7 @@ class VideoPreviewer(Gtk.Layout, PreviewGenerator, Zoomable, Loggable):
                 self.thumbs[current_time].set_from_pixbuf(gdkpixbuf)
                 self.thumbs[current_time].set_visible(True)
             else:
+                self.thumb_cache._cur.execute("SELECT Time FROM Thumbs")
                 self.wishlist.append(current_time)
 
         return True
@@ -514,7 +707,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 = getProxyTarget(obj).props.id
+
     if uri in caches:
         return caches[uri]
     else:
@@ -535,13 +733,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()
@@ -551,6 +756,15 @@ 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:
+            self.error("%s NO FUCKING TIMESAMPS", self._filename)
+            return None
+
+        return self[timestamps[int(len(timestamps) / 2)][0]]
+
     def __getPixbufFromRow(self, row):
         jpeg = row[1]
         loader = GdkPixbuf.PixbufLoader.new()
@@ -591,6 +805,8 @@ class ThumbnailCache(Loggable):
         self._db.commit()
         self.log("Saved thumbnail cache file: %s" % self._filehash)
 
+        return False
+
 
 class PipelineCpuAdapter(Loggable):
 
@@ -690,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):
 
     """
@@ -715,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(getProxyTarget(bElement).props.id)
 
         self._num_failures = 0
         self.adapter = None
@@ -734,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:
@@ -751,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()
@@ -773,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)
@@ -793,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()
@@ -840,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 89e12b3..81d3138 100644
--- a/pitivi/timeline/timeline.py
+++ b/pitivi/timeline/timeline.py
@@ -1150,6 +1150,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..ee091f4 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     \
+       transcoding.py     \
        widgets.py
 
 clean-local:
diff --git a/pitivi/utils/misc.py b/pitivi/utils/misc.py
index b383413..715ff08 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 _
@@ -38,6 +39,14 @@ from pitivi.utils.threads import Thread
 from pitivi.configure import APPMANUALURL_OFFLINE, APPMANUALURL_ONLINE, APPNAME
 
 
+def createActionEntry(name, callback):
+    entry = Gio.ActionEntry()
+    entry.name = name
+    entry.activate = callback
+
+    return entry
+
+
 def format_ns(timestamp):
     if timestamp is None:
         return None
@@ -64,6 +73,21 @@ def call_false(function, *args, **kwargs):
     return False
 
 
+def getProxyTarget(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/transcoding.py b/pitivi/utils/transcoding.py
new file mode 100644
index 0000000..ebb94a8
--- /dev/null
+++ b/pitivi/utils/transcoding.py
@@ -0,0 +1,348 @@
+# Pitivi video editor
+#
+#       pitivi/transcoding.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 time
+import os
+
+from gi.repository import GObject
+from gi.repository import GES
+from gi.repository import Gio
+from gi.repository import Gst
+from gi.repository import GstPbutils
+from gi.repository import GstTranscoder
+from pitivi.settings import GlobalSettings
+
+from pitivi.utils.loggable import Loggable
+
+
+GlobalSettings.addConfigSection("proxy")
+GlobalSettings.addConfigOption('useProxies',
+                               section='proxy',
+                               key='use-proxies',
+                               default=True)
+GlobalSettings.addConfigOption('numTranscodingJobs',
+                               section='proxy',
+                               key='num-transcoding-jobs',
+                               default=4)
+
+
+ENCODING_FORMAT_PRORES = "prores-flac-in-matroska/default"
+ENCODING_FORMAT_JPEG = "jpeg-flac-in-matroska/default"
+
+
+def _discoInfoMatchesEncodingProfile(disco_info, encoding_profile):
+    return not disco_info.get_caps().intersect(
+        encoding_profile.get_format()).is_empty()
+
+
+class ProxyManager(GObject.Object, Loggable):
+    """
+
+    """
+    __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)),
+    }
+
+    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_transcoding_time = 0
+        self._estimated_time = 0
+        self.__max_jobs = self.app.settings.numTranscodingJobs
+        self.proxy_extension = "proxy.mkv"
+        self.__running_transcoder = []
+        self.__pending_transcoders = []
+
+        self.proxyingUnsupported = False
+        self.__encoding_target_name = None
+        for format in [ENCODING_FORMAT_JPEG, ENCODING_FORMAT_PRORES]:
+            if self.__supportsFormat(format):
+                self.__encoding_target_name = format
+                break
+
+        if not self.__encoding_target_name:
+            self.proxyingUnsupported = True
+
+            self.error("Not supporting any proxy formats!")
+            return
+
+        self.debug("Using: %s as a proxy format",
+                   self.__encoding_target_name)
+
+        self.__encoding_profile = GstPbutils.EncodingProfile.find(
+            *self.__encoding_target_name.split("/"))
+        for profile in self.__encoding_profile.get_profiles():
+            if isinstance(profile, GstPbutils.EncodingAudioProfile):
+                self.__audio_profile = profile
+            elif isinstance(profile, GstPbutils.EncodingVideoProfile):
+                self.__video_profile = profile
+
+    def __supportsFormat(self, format):
+        encoding_profile = GstPbutils.EncodingProfile.find(*format.split("/"))
+
+        if not encoding_profile:
+            return False
+
+        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 False
+            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 False
+        return True
+
+    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 __assetNeedsTranscoding(self, asset):
+        info = asset.get_info()
+        container = info.get_stream_info()
+        if not container:
+            return True
+
+        if not _discoInfoMatchesEncodingProfile(container,
+                                                self.__encoding_profile):
+            return True
+
+        audios = info.get_audio_streams()
+        for audio_stream in audios:
+            if not _discoInfoMatchesEncodingProfile(audio_stream,
+                                                    self.__audio_profile):
+                return True
+
+        videos = info.get_video_streams()
+        for video_stream in videos:
+            if not _discoInfoMatchesEncodingProfile(video_stream,
+                                                    self.__video_profile):
+                return True
+
+        self.debug("%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_transcoding_time == 0:
+            self._start_transcoding_time = time.time()
+        transcoder.run_async()
+        self.__running_transcoder.append(transcoder)
+
+    def __assetsMatch(self, asset, proxy):
+        if self.__assetNeedsTranscoding(proxy):
+            return False
+
+        dinfo = asset.get_info()
+        if dinfo.get_duration() != asset.get_duration():
+            return False
+
+        return True
+
+    def __assetLoadedCb(self, proxy, res, asset, transcoder):
+        try:
+            GES.Asset.request_finish(res)
+        except GObject.GError 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.__transcodingPositionChangedCb)
+
+        self.debug("Transcoder for with %s", asset.get_id())
+
+        self.__running_transcoder.remove(transcoder)
+
+        proxy_uri = self.getProxyUri(asset)
+        os.rename(Gst.uri_get_location(transcoder.props.dest_uri),
+                  Gst.uri_get_location(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_transcoder:
+                self._total_transcoded_time = 0
+                self._total_time_to_transcode = 0
+                self._start_transcoding_time = 0
+
+    def __emitProgress(self, asset, progress):
+        if self._total_transcoded_time:
+            time_spent = time.time() - self._start_transcoding_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 __transcodingPositionChangedCb(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_transcoder + 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)
+
+        transcoder = GstTranscoder.Transcoder.new_full(
+            asset_uri, proxy_uri + ".part", self.__encoding_profile,
+            GstTranscoder.TranscoderGMainContextSignalDispatcher.new())
+        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.__transcodingPositionChangedCb,
+                           asset)
+
+        transcoder.connect("done", self.__transcoderDoneCb, asset)
+        transcoder.connect("error", self.__transcoderErrorCb, asset)
+        if len(self.__running_transcoder) < self.__max_jobs:
+            self.__startTranscoder(transcoder)
+        else:
+            self.__pending_transcoders.append(transcoder)
+
+    def cancelJob(self, asset):
+        if not self.__assetQueued(asset):
+            return
+
+        for transcoder in self.__running_transcoder:
+            if asset.props.id == transcoder.props.src_uri:
+                self.__running_transcoder.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:
+                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",
+                   asset.get_id())
+        if (not self.app.settings.useProxies and not force_proxying) or not \
+                self.__assetNeedsTranscoding(asset) \
+                or self.proxyingUnsupported:
+
+            self.debug("Not proxying asset (settings.useProxies: %s, proxy "
+                       "support disabled: %s)", self.app.settings.useProxies,
+                       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("%s exists, trying to use it instead of transcoding it",
+                       proxy_uri)
+            GES.Asset.request_async(GES.UriClip,
+                                    proxy_uri, None,
+                                    self.__assetLoadedCb, asset, False)
+            return True
+
+        self.__createTranscoder(asset)
+        return True
diff --git a/pitivi/utils/ui.py b/pitivi/utils/ui.py
index 2882059..bf994b4 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, getProxyTarget
 from pitivi.configure import get_pixmap_dir
 
 
@@ -249,7 +249,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.
 
@@ -267,6 +267,8 @@ def beautify_info(info):
         except KeyError:
             return len(ranks)
 
+    info = asset.get_info()
+    uri = getProxyTarget(asset).props.id
     info.get_stream_list().sort(key=stream_sort_key)
     nice_streams_txts = []
     for stream in info.get_stream_list():
@@ -278,8 +280,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):
@@ -289,7 +296,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(getProxyTarget(info).get_id()))
     elif isinstance(info, DiscovererInfo):
         filename = urllib.parse.unquote(os.path.basename(info.get_uri()))
     else:
@@ -299,6 +306,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 de0c81f..93f06fc 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -2,19 +2,51 @@
 A collection of objects to use for testing
 """
 
-from gi.repository import Gst
+import mock
 import os
 import gc
 import unittest
+import tempfile
+
+from gi.repository import Gst
 
+from pitivi.application import Pitivi
 from pitivi.utils.timeline import Selected
+from pitivi.utils.loggable import Loggable
+from pitivi.utils.transcoding import ProxyManager
+
+detect_leaks = os.environ.get("PITIVI_TEST_DETECT_LEAKS", "0") not in ("0", "")
+os.environ["PITIVI_USER_CACHE_DIR"] = tempfile.mkdtemp("pitiviTestsuite")
+
+
+def cleanPitiviMock(self):
+    self.settings = None
+    self.proxy_manager = None
+
+
+def getPitiviMock(settings=None, useProxies=True, numTranscodingJobs=4):
+    ptv = mock.MagicMock()
 
-detect_leaks = os.environ.get("PITIVI_TEST_DETECT_LEAKS", "1") not in ("0", "")
+    ptv.write_action = mock.MagicMock(spec=Pitivi.write_action)
 
+    if not settings:
+        settings = mock.MagicMock()
+        settings.useProxies = useProxies
+        settings.numTranscodingJobs = numTranscodingJobs
 
-class TestCase(unittest.TestCase):
+    ptv.settings = settings
+    ptv.proxy_manager = ProxyManager(ptv)
+
+    return ptv
+
+
+class TestCase(unittest.TestCase, Loggable):
     _tracked_types = (Gst.MiniObject, Gst.Element, Gst.Pad, Gst.Caps)
 
+    def __init__(self, *args):
+        Loggable.__init__(self)
+        unittest.TestCase.__init__(self, *args)
+
     def gctrack(self):
         self.gccollect()
         self._tracked = []
@@ -77,10 +109,20 @@ class TestCase(unittest.TestCase):
         self._result = result
         unittest.TestCase.run(self, result)
 
-    @staticmethod
-    def getSampleUri(sample):
-        dir = os.path.dirname(os.path.abspath(__file__))
-        return "file://%s/samples/%s" % (dir, sample)
+
+def getSampleUri(sample):
+    dir = os.path.dirname(os.path.abspath(__file__))
+    return "file://%s/samples/%s" % (dir, sample)
+
+
+def cleanProxySamples():
+    _dir = "/%s/samples/" % (os.path.dirname(os.path.abspath(__file__)))
+    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/test_media_library.py b/tests/test_media_library.py
new file mode 100644
index 0000000..3f8e83d
--- /dev/null
+++ b/tests/test_media_library.py
@@ -0,0 +1,194 @@
+# -*- 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 sys
+
+from unittest import mock
+from gettext import gettext as _
+
+import tests.common as ptvtest
+from tests.common import TestCase, getPitiviMock, getSampleUri, \
+    cleanProxySamples
+
+from gi.repository import GES
+from gi.repository import Gst
+from gi.repository import GLib
+
+from pitivi import medialibrary as ml
+from pitivi.project import ProjectManager
+from pitivi.utils.transcoding import ProxyManager
+from pitivi.mainwindow import PitiviMainWindow
+from pitivi.timeline import timeline
+from pitivi.utils import loggable
+
+
+def fakeSwitchProxies(asset):
+    timeline.TimelineContainer.switchProxies(mock.MagicMock(), asset)
+
+
+class TestMediaLibrary(TestCase):
+    def __init__(self, *args):
+        TestCase.__init__(self, *args)
+        self.app = None
+        self.medialibrary = None
+        self.mainloop = None
+
+    def tearDown(self):
+        self.clean()
+        TestCase.tearDown(self)
+
+    def clean(self):
+        self.mainloop = None
+
+        if self.app:
+            self.app = ptvtest.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 = getPitiviMock(settings)
+        self.app.project_manager = ProjectManager(self.app)
+        self.medialibrary = ml.MediaLibraryWidget(
+            self.app, mock.MagicMock())
+        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(
+                getSampleUri(sample_name), GES.UriClip,)
+
+    def runCheckImport(self, assets, use_proxies=True, check_no_transcoding=False,
+                       clean_proxies=True):
+        settings = mock.MagicMock()
+        settings.useProxies = use_proxies
+        settings.numTranscodingJobs = 4
+        settings.lastClipView = ml.SHOW_TREEVIEW
+
+        self._customSetUp(settings)
+        self.check_no_transcoding = check_no_transcoding
+
+        self.medialibrary._progressbar.connect(
+            "notify::fraction", self._progressBarCb)
+
+        if clean_proxies:
+            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 = getSampleUri(sample_name)
+        proxy = self.medialibrary.storemodel[0][ml.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][ml.COL_URI],
+                         asset_uri)
+
+    def testTranscoding(self):
+        self.runCheckImport(["30fps_numeroted_frames_red.mkv"])
+
+    def testDisableProxies(self):
+        self.runCheckImport(["30fps_numeroted_frames_red.mkv"], False, 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][ml.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.assertNotEqual(proxy, None)
+        self.assertFalse(os.path.exists(Gst.uri_get_location(proxy_uri)))
+
+        self.assertEqual(asset.get_proxy(), None)
+
+        # And let's rectreate the proxy file.
+        self.app.project_manager.current_project.useProxiesForAssets(
+            [asset])
+        self.assertEqual(asset.creation_progress, 0)
+
+        # Check that the infobull notifies the user about transcoding progress
+        self.assertTrue(_("Proxy creation progress: ") in
+                        self.medialibrary.storemodel[0][ml.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..e880316
--- /dev/null
+++ b/tests/test_previewers.py
@@ -0,0 +1,61 @@
+# -*- 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, Gst
+
+from tests.test_media_library import TestMediaLibrary
+from tests.common import TestCase, getSampleUri
+from pitivi.timeline.previewers import getThumbnailCache, THUMB_HEIGHT, \
+    get_wavefile_location_for_uri
+
+
+class TestPreviewers(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 = 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 34444e7..c098069 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -18,12 +18,13 @@
 # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 # Boston, MA 02110-1301, USA.
 
+import mock
 import os
 import tempfile
 import time
 
 from unittest import TestCase
-
+from tests.common import getPitiviMock, getSampleUri
 from gi.repository import GES
 from gi.repository import GLib
 from gi.repository import Gst
@@ -36,7 +37,7 @@ from pitivi.utils.misc import uri_is_reachable
 
 
 def _createRealProject(name=None):
-    app = Pitivi()
+    app = getPitiviMock(useProxies=False)
     project_manager = ProjectManager(app)
     project_manager.newBlankProject()
     project = project_manager.current_project
@@ -60,6 +61,9 @@ class MockProject(object):
     def disconnect_by_function(self, ignored):
         pass
 
+    def finalize(self):
+        pass
+
 
 class ProjectManagerListener(object):
 
@@ -88,7 +92,8 @@ class ProjectManagerListener(object):
 class TestProjectManager(TestCase):
 
     def setUp(self):
-        self.manager = ProjectManager(None)
+        app = mock.MagicMock()
+        self.manager = ProjectManager(app)
         self.listener = ProjectManagerListener(self.manager)
         self.signals = self.listener.signals
 
@@ -357,32 +362,40 @@ class TestProjectLoading(common.TestCase):
         finally:
             os.remove(xges_path)
 
-    def testAssetAddingRemovingAdding(self):
-        def loaded(project, timeline, mainloop, result, uris):
-            result[0] = True
-            project.addUris(uris)
+    def loadedCb(self, project, timeline, mainloop, result, uris):
+        result[0] = True
+        project.addUris(uris)
 
-        def added(project, mainloop, result, uris):
-            result[1] = True
-            assets = project.list_assets(GES.UriClip)
-            asset = assets[0]
-            project.remove_asset(asset)
-            GLib.idle_add(readd, mainloop, result, uris)
+    def loadingProgressCb(self, project, progress, estimated_time,
+                          mainloop, result, uris):
 
         def readd(mainloop, result, uris):
             project.addUris(uris)
             result[2] = True
             mainloop.quit()
 
-        def quit(mainloop):
-            mainloop.quit()
+        if progress < 100:
+            return
 
+        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)
+
+    def testAssetAddingRemovingAdding(self):
         # Create a blank project and add an asset.
         project = _createRealProject()
         result = [False, False, False]
-        uris = [self.getSampleUri("tears_of_steel.webm")]
-        project.connect("loaded", loaded, self.mainloop, result, uris)
-        project.connect("done-importing", added, self.mainloop, result, uris)
+        uris = [getSampleUri("tears_of_steel.webm")]
+        project.connect("loaded", self.loadedCb, self.mainloop, result, uris)
+        project.connect("asset-loading-progress",
+                        self.loadingProgressCb, self.mainloop,
+                        result, uris)
+
+        def quit(mainloop):
+            mainloop.quit()
 
         self.assertTrue(project.createTimeline())
         GLib.timeout_add_seconds(5, quit, self.mainloop)
@@ -390,7 +403,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")
 
 
@@ -424,8 +438,9 @@ class TestProjectSettings(common.TestCase):
         def loaded(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()
@@ -435,11 +450,11 @@ class TestProjectSettings(common.TestCase):
         self.assertTrue(project._has_default_video_settings)
         self.assertTrue(project._has_default_audio_settings)
 
-        uris = [self.getSampleUri("flat_colour1_640x480.png"),
-                self.getSampleUri("tears_of_steel.webm"),
-                self.getSampleUri("1sec_simpsons_trailer.mp4")]
+        uris = [getSampleUri("flat_colour1_640x480.png"),
+                getSampleUri("tears_of_steel.webm"),
+                getSampleUri("1sec_simpsons_trailer.mp4")]
         project.connect("loaded", loaded, self.mainloop, uris)
-        project.connect("done-importing", added, self.mainloop)
+        project.connect("asset-loading-progress", progressCb, self.mainloop)
 
         self.assertTrue(project.createTimeline())
         GLib.timeout_add_seconds(5, quit, self.mainloop)
@@ -462,7 +477,7 @@ class TestProjectSettings(common.TestCase):
         self.assertEqual(Gst.Fraction(1, 1), project.videopar)
 
     def testLoad(self):
-        project = Project(uri="fake.xges", app=None)
+        project = Project(uri="fake.xges", app=getPitiviMock(useProxies=False))
         self.assertFalse(project._has_default_video_settings)
         self.assertFalse(project._has_default_audio_settings)
 
diff --git a/tests/test_timeline_layer.py b/tests/test_timeline_layer.py
index aeb4817..7e560c1 100644
--- a/tests/test_timeline_layer.py
+++ b/tests/test_timeline_layer.py
@@ -46,7 +46,7 @@ class TestLayer(common.TestCase):
 
     def testCheckMediaTypesWhenNoUI(self):
         bLayer = GES.Layer()
-        png = self.getSampleUri("flat_colour1_640x480.png")
+        png = common.getSampleUri("flat_colour1_640x480.png")
         video_clip = GES.UriClipAsset.request_sync(png).extract()
         self.assertTrue(bLayer.add_clip(video_clip))
         self.assertEqual(1, len(bLayer.get_clips()))
diff --git a/tests/test_timeline_timeline.py b/tests/test_timeline_timeline.py
index aac5834..3666e1f 100644
--- a/tests/test_timeline_timeline.py
+++ b/tests/test_timeline_timeline.py
@@ -18,7 +18,7 @@
 # Boston, MA 02110-1301, USA.
 
 from unittest import TestCase, mock
-
+from tests.common import getPitiviMock
 from gi.repository import Gdk
 
 from pitivi.project import Project, ProjectManager
@@ -34,10 +34,11 @@ THICK = ui.LAYER_HEIGHT
 class TestLayers(TestCase):
 
     def createTimeline(self, layers_heights):
-        project_manager = ProjectManager(app=None)
+        app = getPitiviMock()
+        project_manager = ProjectManager(app)
         project_manager.newBlankProject()
         project = project_manager.current_project
-        timeline = Timeline(container=None, app=None)
+        timeline = Timeline(container=mock.MagicMock, app=app)
         timeline.get_parent = mock.MagicMock()
         timeline.setProject(project)
         y = 0
diff --git a/tests/test_undo_timeline.py b/tests/test_undo_timeline.py
index aa50489..3c87b63 100644
--- a/tests/test_undo_timeline.py
+++ b/tests/test_undo_timeline.py
@@ -315,7 +315,7 @@ class TestTimelineUndo(TestCase):
         self.assertEqual(20, clip1.get_priority())
 
     def testUngroup(self):
-        uri = common.TestCase.getSampleUri("tears_of_steel.webm")
+        uri = common.getSampleUri("tears_of_steel.webm")
         asset = GES.UriClipAsset.request_sync(uri)
         clip1 = asset.extract()
         self.layer.add_clip(clip1)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]