diff --git a/src/UI/DocumentViews/MpvWidget.cc b/src/UI/DocumentViews/MpvWidget.cc
new file mode 100644
index 0000000000000000000000000000000000000000..04e88150f739a35a892f171a905fc0d43917f4c8
--- /dev/null
+++ b/src/UI/DocumentViews/MpvWidget.cc
@@ -0,0 +1,341 @@
+#include "MpvWidget.hh"
+
+#include <stdexcept>
+#include <QtGui/QOpenGLContext>
+#include <QtCore/QMetaObject>
+#include <QApplication>
+#include <QKeyEvent>
+#include <QString>
+
+#include "QtHelper.hh"
+
+namespace Vivy
+{
+
+static void
+wakeup(void *ctx)
+{
+    QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "onMpvEvents", Qt::QueuedConnection);
+}
+
+static void *
+getProcAddress(void *ctx, const char *name)
+{
+    Q_UNUSED(ctx);
+    QOpenGLContext *glctx = QOpenGLContext::currentContext();
+    if (!glctx)
+        return nullptr;
+    return reinterpret_cast<void *>(glctx->getProcAddress(QByteArray(name)));
+}
+
+MpvWidget::MpvWidget(QWidget *parent)
+    : QOpenGLWidget(parent)
+{
+    mpv = mpv_create();
+    if (!mpv)
+        throw std::runtime_error("could not create mpv context");
+
+    setFocusPolicy(Qt::StrongFocus);
+
+    mpv_set_option_string(mpv, "terminal", "yes");
+    mpv_set_option_string(mpv, "msg-level", "all=v");
+    mpv_set_option_string(mpv, "osc", "yes");
+    if (mpv_initialize(mpv) < 0)
+        throw std::runtime_error("could not initialize mpv context");
+
+    // Request hw decoding, just for testing.
+    mpv_set_option_string(mpv, "hwdec", "auto");
+
+    mpv_observe_property(mpv, 0, "duration", MPV_FORMAT_DOUBLE);
+    mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_DOUBLE);
+    mpv_set_wakeup_callback(mpv, wakeup, this);
+}
+
+MpvWidget::~MpvWidget()
+{
+    makeCurrent();
+    if (mpv_gl)
+        mpv_render_context_free(mpv_gl);
+    mpv_terminate_destroy(mpv);
+}
+
+void
+MpvWidget::command(const QVariant &params)
+{
+    mpv::qt::command(mpv, params);
+}
+
+void
+MpvWidget::setProperty(const QString &name, const QVariant &value)
+{
+    mpv::qt::set_property(mpv, name, value);
+}
+
+QVariant
+MpvWidget::getProperty(const QString &name) const
+{
+    return mpv::qt::get_property(mpv, name);
+}
+
+void
+MpvWidget::initializeGL()
+{
+    mpv_opengl_init_params gl_init_params{ getProcAddress, nullptr, nullptr };
+    mpv_render_param params[]{ { MPV_RENDER_PARAM_API_TYPE,
+                                 const_cast<char *>(MPV_RENDER_API_TYPE_OPENGL) },
+                               { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params },
+                               { MPV_RENDER_PARAM_INVALID, nullptr } };
+
+    if (mpv_render_context_create(&mpv_gl, mpv, params) < 0)
+        throw std::runtime_error("failed to initialize mpv GL context");
+    mpv_render_context_set_update_callback(mpv_gl, MpvWidget::onUpdate,
+                                           reinterpret_cast<void *>(this));
+}
+
+void
+MpvWidget::paintGL()
+{
+    mpv_opengl_fbo mpfbo{ static_cast<int>(defaultFramebufferObject()), width(), height(), 0 };
+    int flip_y{ 1 };
+
+    mpv_render_param params[] = { { MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo },
+                                  { MPV_RENDER_PARAM_FLIP_Y, &flip_y },
+                                  { MPV_RENDER_PARAM_INVALID, nullptr } };
+    // See render_gl.h on what OpenGL environment mpv expects, and
+    // other API details.
+    mpv_render_context_render(mpv_gl, params);
+}
+
+void
+MpvWidget::onMpvEvents()
+{
+    // Process all events, until the event queue is empty.
+    while (mpv) {
+        mpv_event *event = mpv_wait_event(mpv, 0);
+        if (event->event_id == MPV_EVENT_NONE) {
+            break;
+        }
+        handleMpvEvent(event);
+    }
+}
+
+void
+MpvWidget::handleMpvEvent(mpv_event *event)
+{
+    size_t ao_volume;
+    mpv_event_property *prop;
+    (void)ao_volume;
+    (void)prop;
+
+    switch (event->event_id) {
+    case MPV_EVENT_PAUSE: m_state = PAUSE; break;
+    case MPV_EVENT_UNPAUSE: m_state = PLAY; break;
+
+    case MPV_EVENT_SHUTDOWN: m_state = STOP; break;
+
+    case MPV_EVENT_START_FILE: break;
+
+    case MPV_EVENT_END_FILE: break;
+
+    case MPV_EVENT_PROPERTY_CHANGE: {
+        prop = static_cast<mpv_event_property *>(event->data);
+        if (strcmp(prop->name, "time-pos") == 0) {
+            if (prop->format == MPV_FORMAT_DOUBLE) {
+                m_position = static_cast<int>(*reinterpret_cast<double *>(prop->data));
+            }
+        } else if (strcmp(prop->name, "duration") == 0) {
+            if (prop->format == MPV_FORMAT_DOUBLE) {
+                m_duration = static_cast<int>(*reinterpret_cast<double *>(prop->data));
+            }
+        }
+        break;
+    }
+
+    case MPV_EVENT_IDLE: emit titleChanged("[Lektord] Stopped"); break;
+
+    case MPV_EVENT_LOG_MESSAGE:
+    case MPV_EVENT_GET_PROPERTY_REPLY:
+    case MPV_EVENT_SET_PROPERTY_REPLY:
+    case MPV_EVENT_NONE:
+    case MPV_EVENT_COMMAND_REPLY:
+    case MPV_EVENT_FILE_LOADED:
+    case MPV_EVENT_TRACKS_CHANGED:
+    case MPV_EVENT_TRACK_SWITCHED:
+    case MPV_EVENT_TICK:
+    case MPV_EVENT_SCRIPT_INPUT_DISPATCH:
+    case MPV_EVENT_CLIENT_MESSAGE:
+    case MPV_EVENT_VIDEO_RECONFIG:
+    case MPV_EVENT_AUDIO_RECONFIG:
+    case MPV_EVENT_METADATA_UPDATE:
+    case MPV_EVENT_SEEK:
+    case MPV_EVENT_CHAPTER_CHANGE:
+    case MPV_EVENT_PLAYBACK_RESTART:
+    case MPV_EVENT_QUEUE_OVERFLOW:
+    case MPV_EVENT_HOOK: break;
+    }
+}
+
+// Make Qt invoke mpv_render_context_render() to draw a new/updated video frame.
+void
+MpvWidget::maybeUpdate()
+{
+    // If the Qt window is not visible, Qt's update() will just skip rendering.
+    // This confuses mpv's render API, and may lead to small occasional
+    // freezes due to video rendering timing out.
+    // Handle this by manually redrawing.
+    // Note: Qt doesn't seem to provide a way to query whether update() will
+    //       be skipped, and the following code still fails when e.g. switching
+    //       to a different workspace with a reparenting window manager.
+    if (window()->isMinimized()) {
+        makeCurrent();
+        paintGL();
+        context()->swapBuffers(context()->surface());
+        doneCurrent();
+    } else {
+        update();
+    }
+}
+
+void
+MpvWidget::onUpdate(void *ctx)
+{
+    QMetaObject::invokeMethod(static_cast<MpvWidget *>(ctx), "maybeUpdate");
+}
+
+bool
+MpvWidget::getElapsed(int *elapsed_sec)
+{
+    *elapsed_sec = m_position;
+    return true;
+}
+
+bool
+MpvWidget::getDuration(int *dur_sec)
+{
+    *dur_sec = m_duration;
+    return true;
+}
+
+bool
+MpvWidget::setPaused(int paused)
+{
+    const char *cmd[] = { "set", "pause", paused == 1 ? "yes" : "no", nullptr };
+    mpv_command_async(mpv, 0, cmd);
+    return true;
+}
+
+bool
+MpvWidget::setVolume([[maybe_unused]] int vol)
+{
+    return true;
+}
+
+bool
+MpvWidget::setPosition([[maybe_unused]] int sec)
+{
+    throw std::logic_error("lmpv_set_position");
+}
+
+bool
+MpvWidget::loadFile(const QString &filepath)
+{
+    const bool ret = false;
+    throw std::runtime_error("lmpv_load_file");
+    // const bool ret = !lmpv_load_file(mpv, filepath);
+
+    if (ret) {
+        m_state = NONSTOPPED;
+    }
+    return ret;
+}
+
+bool
+MpvWidget::togglePause()
+{
+    const char *cmd[] = { "cycle", "pause", nullptr };
+    mpv_command_async(mpv, 0, cmd);
+    return true;
+}
+
+bool
+MpvWidget::stop()
+{
+    m_state           = STOP;
+    const char *cmd[] = { "stop", nullptr };
+    mpv_command_async(mpv, 0, cmd);
+    emit titleChanged("[Lektord] Stopped");
+    return true;
+}
+
+#define MPV_SEND_COMMAND_ASYNC(...)          \
+    {                                        \
+        const char *cmd[] = { __VA_ARGS__ }; \
+        mpv_command_async(mpv, 0, cmd);      \
+        break;                               \
+    }
+void
+MpvWidget::keyPressEvent(QKeyEvent *event)
+{
+    if (m_state == STOP)
+        return QOpenGLWidget::keyPressEvent(event);
+
+    switch (event->modifiers()) {
+    /* SHIFTED */
+    case Qt::ShiftModifier:
+        switch (event->key()) {
+        case Qt::Key_J: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "sub", nullptr);
+        case Qt::Key_Period: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-step", nullptr);
+        case Qt::Key_Z: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-delay", "+0.1", nullptr);
+        case Qt::Key_G: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-scale", "+0.1", nullptr);
+        case Qt::Key_F: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-scale", "-0.1", nullptr);
+        }
+        break;
+
+    /* UN-SHIFTED */
+    default:
+        switch (event->key()) {
+        /* Playback */
+        case Qt::Key_Space:
+            throw std::logic_error("lmpv_toggle_pause");
+            // lmpv_toggle_pause(mpv);
+            break;
+        case Qt::Key_Return:
+        case Qt::Key_Left: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "-5", "relative", nullptr);
+        case Qt::Key_Right:
+            MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "+5", "relative", nullptr);
+        case Qt::Key_Down:
+            MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "-60", "relative", nullptr);
+        case Qt::Key_Up: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "seek", "+60", "relative", nullptr);
+        case Qt::Key_L: MPV_SEND_COMMAND_ASYNC("osd-msg", "ab-loop", nullptr);
+        case Qt::Key_O: MPV_SEND_COMMAND_ASYNC("osd-msg-bar", "show-progress", nullptr);
+        case Qt::Key_BracketLeft:
+            MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "1/1.1", nullptr);
+        case Qt::Key_BracketRight:
+            MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "1.1", nullptr);
+        case Qt::Key_BraceLeft:
+            MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "0.5", nullptr);
+        case Qt::Key_BraceRight:
+            MPV_SEND_COMMAND_ASYNC("osd-msg", "multiply", "speed", "2", nullptr);
+        case Qt::Key_Backspace: MPV_SEND_COMMAND_ASYNC("osd-msg", "set", "speed", "1.0", nullptr);
+        case Qt::Key_Semicolon: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-step", nullptr);
+        case Qt::Key_Comma: MPV_SEND_COMMAND_ASYNC("osd-msg", "frame-back-step", nullptr);
+
+        /* Track management */
+        case Qt::Key_NumberSign: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "audio", nullptr);
+        case Qt::Key_J: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "sub", "down", nullptr);
+        case Qt::Key_Underscore: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "video", nullptr);
+
+        /* Misc */
+        case Qt::Key_I: MPV_SEND_COMMAND_ASYNC("script-binding", "stats/display-stats", nullptr);
+        case Qt::Key_Delete:
+            MPV_SEND_COMMAND_ASYNC("script-message", "osc-visibility",
+                                   (m_oscVisible = !m_oscVisible) ? "always" : "never", nullptr);
+        case Qt::Key_Z: MPV_SEND_COMMAND_ASYNC("osd-msg", "add", "sub-delay", "-0.1", nullptr);
+        case Qt::Key_M: MPV_SEND_COMMAND_ASYNC("osd-msg", "cycle", "mute", nullptr);
+
+        default: break;
+        }
+        return QOpenGLWidget::keyPressEvent(event);
+    }
+}
+}
diff --git a/src/UI/DocumentViews/MpvWidget.hh b/src/UI/DocumentViews/MpvWidget.hh
new file mode 100644
index 0000000000000000000000000000000000000000..d392e8560b7e6f6e9cefb75732f05e8f0ac70762
--- /dev/null
+++ b/src/UI/DocumentViews/MpvWidget.hh
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <QtWidgets/QOpenGLWidget>
+#include <QString>
+#include <QtGui>
+
+#include <mpv/client.h>
+#include <mpv/render_gl.h>
+
+namespace Vivy
+{
+class MpvWidget final : public QOpenGLWidget {
+    Q_OBJECT
+
+public:
+    MpvWidget(QWidget *parent);
+    ~MpvWidget() override;
+
+    void command(const QVariant &params);
+    void setProperty(const QString &name, const QVariant &value);
+    QVariant getProperty(const QString &name) const;
+    QSize sizeHint() const override { return QSize(480, 270); }
+
+protected:
+    void initializeGL() override;
+    void paintGL() override;
+
+private slots:
+    void onMpvEvents();
+    void maybeUpdate();
+
+public:
+    void handleMpvEvent(mpv_event *event);
+    static void onUpdate(void *ctx);
+
+private:
+    enum { PLAY, PAUSE, STOP, NONSTOPPED } m_state = STOP;
+
+    mpv_handle *mpv;
+    mpv_render_context *mpv_gl;
+
+    int m_position;
+    int m_duration;
+    bool m_oscVisible = false;
+    bool m_inhib      = false;
+
+protected:
+    void keyPressEvent(QKeyEvent *event) override;
+
+public:
+    bool getElapsed(int *);
+    bool getDuration(int *);
+    bool setPaused(int);
+    bool setVolume(int);
+    bool setPosition(int);
+    bool loadFile(const QString &);
+    bool togglePause();
+    bool stop();
+
+signals:
+    void titleChanged(QString str);
+};
+}
diff --git a/src/UI/DocumentViews/QtHelper.hh b/src/UI/DocumentViews/QtHelper.hh
new file mode 100644
index 0000000000000000000000000000000000000000..4ef7a109fe8a208ba3d59c32d4124d3c91918af2
--- /dev/null
+++ b/src/UI/DocumentViews/QtHelper.hh
@@ -0,0 +1,318 @@
+#pragma once
+
+#include <cstring>
+
+#include <QVariant>
+#include <QString>
+#include <QList>
+#include <QHash>
+#include <QSharedPointer>
+#include <QMetaType>
+
+namespace mpv::qt
+{
+
+// Wrapper around mpv_handle. Does refcounting under the hood.
+class Handle {
+    struct container {
+        container(mpv_handle *h)
+            : mpv(h)
+        {
+        }
+        ~container() { mpv_terminate_destroy(mpv); }
+        mpv_handle *mpv;
+    };
+    QSharedPointer<container> sptr;
+
+public:
+    // Construct a new Handle from a raw mpv_handle with refcount 1. If the
+    // last Handle goes out of scope, the mpv_handle will be destroyed with
+    // mpv_terminate_destroy().
+    // Never destroy the mpv_handle manually when using this wrapper. You
+    // will create dangling pointers. Just let the wrapper take care of
+    // destroying the mpv_handle.
+    // Never create multiple wrappers from the same raw mpv_handle; copy the
+    // wrapper instead (that's what it's for).
+    static Handle FromRawHandle(mpv_handle *handle)
+    {
+        Handle h;
+        h.sptr = QSharedPointer<container>(new container(handle));
+        return h;
+    }
+
+    // Return the raw handle; for use with the libmpv C API.
+    operator mpv_handle *() const { return sptr ? (*sptr).mpv : nullptr; }
+};
+
+static inline QVariant
+node_to_variant(const mpv_node *node)
+{
+    switch (node->format) {
+    case MPV_FORMAT_STRING: return QVariant(QString::fromUtf8(node->u.string));
+    case MPV_FORMAT_FLAG: return QVariant(static_cast<bool>(node->u.flag));
+    case MPV_FORMAT_INT64: return QVariant(static_cast<qlonglong>(node->u.int64));
+    case MPV_FORMAT_DOUBLE: return QVariant(node->u.double_);
+
+    case MPV_FORMAT_NODE_ARRAY: {
+        mpv_node_list *list = node->u.list;
+        QVariantList qlist;
+        for (int n = 0; n < list->num; n++)
+            qlist.append(node_to_variant(&list->values[n]));
+        return QVariant(qlist);
+    }
+
+    case MPV_FORMAT_NODE_MAP: {
+        mpv_node_list *list = node->u.list;
+        QVariantMap qmap;
+        for (int n = 0; n < list->num; n++) {
+            qmap.insert(QString::fromUtf8(list->keys[n]), node_to_variant(&list->values[n]));
+        }
+        return QVariant(qmap);
+    }
+
+    case MPV_FORMAT_OSD_STRING:
+    case MPV_FORMAT_NONE:
+    case MPV_FORMAT_NODE:
+    case MPV_FORMAT_BYTE_ARRAY: return QVariant();
+    }
+}
+
+struct node_builder {
+    node_builder(const QVariant &v) { set(&node_, v); }
+    ~node_builder() { free_node(&node_); }
+    mpv_node *node() { return &node_; }
+
+private:
+    Q_DISABLE_COPY(node_builder)
+    mpv_node node_;
+
+    mpv_node_list *create_list(mpv_node *dst, bool is_map, int num)
+    {
+        dst->format         = is_map ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY;
+        mpv_node_list *list = new mpv_node_list();
+        dst->u.list         = list;
+        if (!list)
+            goto err;
+        list->values = new mpv_node[static_cast<size_t>(num)]();
+        if (!list->values)
+            goto err;
+        if (is_map) {
+            list->keys = new char *[static_cast<size_t>(num)]();
+            if (!list->keys)
+                goto err;
+        }
+        return list;
+    err:
+        free_node(dst);
+        return nullptr;
+    }
+
+    char *dup_qstring(const QString &s)
+    {
+        QByteArray b = s.toUtf8();
+        char *r      = new char[static_cast<size_t>(b.size() + 1)];
+        if (r)
+            std::memcpy(r, b.data(), static_cast<size_t>(b.size() + 1));
+        return r;
+    }
+
+    bool test_type(const QVariant &v, QMetaType::Type t)
+    {
+        // The Qt docs say: "Although this function is declared as returning
+        // "QVariant::Type(obsolete), the return value should be interpreted
+        // as QMetaType::Type."
+        // So a cast really seems to be needed to avoid warnings (urgh).
+        return static_cast<int>(v.type()) == static_cast<int>(t);
+    }
+
+    void set(mpv_node *dst, const QVariant &src)
+    {
+        if (test_type(src, QMetaType::QString)) {
+            dst->format   = MPV_FORMAT_STRING;
+            dst->u.string = dup_qstring(src.toString());
+            if (!dst->u.string)
+                goto fail;
+        } else if (test_type(src, QMetaType::Bool)) {
+            dst->format = MPV_FORMAT_FLAG;
+            dst->u.flag = src.toBool() ? 1 : 0;
+        } else if (test_type(src, QMetaType::Int) || test_type(src, QMetaType::LongLong) ||
+                   test_type(src, QMetaType::UInt) || test_type(src, QMetaType::ULongLong)) {
+            dst->format  = MPV_FORMAT_INT64;
+            dst->u.int64 = src.toLongLong();
+        } else if (test_type(src, QMetaType::Double)) {
+            dst->format    = MPV_FORMAT_DOUBLE;
+            dst->u.double_ = src.toDouble();
+        } else if (src.canConvert<QVariantList>()) {
+            QVariantList qlist  = src.toList();
+            mpv_node_list *list = create_list(dst, false, qlist.size());
+            if (!list)
+                goto fail;
+            list->num = qlist.size();
+            for (int n = 0; n < qlist.size(); n++)
+                set(&list->values[n], qlist[n]);
+        } else if (src.canConvert<QVariantMap>()) {
+            QVariantMap qmap    = src.toMap();
+            mpv_node_list *list = create_list(dst, true, qmap.size());
+            if (!list)
+                goto fail;
+            list->num = qmap.size();
+            for (int n = 0; n < qmap.size(); n++) {
+                list->keys[n] = dup_qstring(qmap.keys()[n]);
+                if (!list->keys[n]) {
+                    free_node(dst);
+                    goto fail;
+                }
+                set(&list->values[n], qmap.values()[n]);
+            }
+        } else {
+            goto fail;
+        }
+        return;
+    fail:
+        dst->format = MPV_FORMAT_NONE;
+    }
+
+    void free_node(mpv_node *dst)
+    {
+        mpv_node_list *list = nullptr;
+        switch (dst->format) {
+        case MPV_FORMAT_STRING: delete[] dst->u.string; break;
+
+        case MPV_FORMAT_NODE_ARRAY:
+        case MPV_FORMAT_NODE_MAP:
+            list = dst->u.list;
+            if (list) {
+                for (int n = 0; n < list->num; n++) {
+                    if (list->keys)
+                        delete[] list->keys[n];
+                    if (list->values)
+                        free_node(&list->values[n]);
+                }
+                delete[] list->keys;
+                delete[] list->values;
+            }
+            delete list;
+            break;
+
+        case MPV_FORMAT_OSD_STRING:
+        case MPV_FORMAT_NONE:
+        case MPV_FORMAT_NODE:
+        case MPV_FORMAT_FLAG:
+        case MPV_FORMAT_INT64:
+        case MPV_FORMAT_DOUBLE:
+        case MPV_FORMAT_BYTE_ARRAY: break;
+        }
+
+        dst->format = MPV_FORMAT_NONE;
+    }
+};
+
+/**
+ * RAII wrapper that calls mpv_free_node_contents() on the pointer.
+ */
+struct node_autofree {
+    mpv_node *ptr;
+    node_autofree(mpv_node *a_ptr)
+        : ptr(a_ptr)
+    {
+    }
+    ~node_autofree() { mpv_free_node_contents(ptr); }
+};
+
+/**
+ * This is used to return error codes wrapped in QVariant for functions which
+ * return QVariant.
+ *
+ * You can use get_error() or is_error() to extract the error status from a
+ * QVariant value.
+ */
+struct ErrorReturn {
+    /**
+     * enum mpv_error value (or a value outside of it if ABI was extended)
+     */
+    int error;
+
+    ErrorReturn()
+        : error(0)
+    {
+    }
+    explicit ErrorReturn(int err)
+        : error(err)
+    {
+    }
+};
+
+/**
+ * Return the mpv error code packed into a QVariant, or 0 (success) if it's not
+ * an error value.
+ *
+ * @return error code (<0) or success (>=0)
+ */
+static inline int
+get_error(const QVariant &v)
+{
+    if (!v.canConvert<ErrorReturn>())
+        return 0;
+    return v.value<ErrorReturn>().error;
+}
+
+/**
+ * Return whether the QVariant carries a mpv error code.
+ */
+static inline bool
+is_error(const QVariant &v)
+{
+    return get_error(v) < 0;
+}
+
+/**
+ * Return the given property as mpv_node converted to QVariant, or QVariant()
+ * on error.
+ *
+ * @param name the property name
+ * @return the property value, or an ErrorReturn with the error code
+ */
+static inline QVariant
+get_property(mpv_handle *ctx, const QString &name)
+{
+    mpv_node node;
+    int err = mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node);
+    if (err < 0)
+        return QVariant::fromValue(ErrorReturn(err));
+    node_autofree f(&node);
+    return node_to_variant(&node);
+}
+
+/**
+ * Set the given property as mpv_node converted from the QVariant argument.
+ *
+ * @return mpv error code (<0 on error, >= 0 on success)
+ */
+static inline int
+set_property(mpv_handle *ctx, const QString &name, const QVariant &v)
+{
+    node_builder node(v);
+    return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node());
+}
+
+/**
+ * mpv_command_node() equivalent.
+ *
+ * @param args command arguments, with args[0] being the command name as string
+ * @return the property value, or an ErrorReturn with the error code
+ */
+static inline QVariant
+command(mpv_handle *ctx, const QVariant &args)
+{
+    node_builder node(args);
+    mpv_node res;
+    int err = mpv_command_node(ctx, node.node(), &res);
+    if (err < 0)
+        return QVariant::fromValue(ErrorReturn(err));
+    node_autofree f(&res);
+    return node_to_variant(&res);
+}
+
+}
+
+Q_DECLARE_METATYPE(mpv::qt::ErrorReturn)
diff --git a/src/UI/DocumentViews/MpvContainer.cc b/src/UI/DocumentViews/old/MpvContainer.cc
similarity index 97%
rename from src/UI/DocumentViews/MpvContainer.cc
rename to src/UI/DocumentViews/old/MpvContainer.cc
index ec7c8d477ff85ca4f3981af5b13462a3b6c26163..b8a3fa9789acfd693a55c0eb299d7914915301f6 100644
--- a/src/UI/DocumentViews/MpvContainer.cc
+++ b/src/UI/DocumentViews/old/MpvContainer.cc
@@ -58,12 +58,14 @@ MpvContainer::initializeMpv()
 void
 MpvContainer::reCreateMpvContext()
 {
-    closeMpv();
-    mpv = mpv_create();
-    if (mpv == nullptr)
-        throw std::runtime_error("Failed to create the MPV context");
-    initializeMpv();
-    loadFile(previousLoadedFile);
+    if (!isMpvAlreadyInitialized) {
+        closeMpv();
+        mpv = mpv_create();
+        if (mpv == nullptr)
+            throw std::runtime_error("Failed to create the MPV context");
+        initializeMpv();
+        loadFile(previousLoadedFile);
+    }
 }
 
 void
diff --git a/src/UI/DocumentViews/MpvContainer.hh b/src/UI/DocumentViews/old/MpvContainer.hh
similarity index 100%
rename from src/UI/DocumentViews/MpvContainer.hh
rename to src/UI/DocumentViews/old/MpvContainer.hh
diff --git a/src/UI/DocumentViews/MpvControls.cc b/src/UI/DocumentViews/old/MpvControls.cc
similarity index 100%
rename from src/UI/DocumentViews/MpvControls.cc
rename to src/UI/DocumentViews/old/MpvControls.cc
diff --git a/src/UI/DocumentViews/MpvControls.hh b/src/UI/DocumentViews/old/MpvControls.hh
similarity index 100%
rename from src/UI/DocumentViews/MpvControls.hh
rename to src/UI/DocumentViews/old/MpvControls.hh
diff --git a/src/UI/VivyDocumentView.cc b/src/UI/VivyDocumentView.cc
index 6cc127fa6bdcde6dac57a37aa233bf3b8d97f49a..2b12fc262174b9ef7f1af45ad925f769ddb898a1 100644
--- a/src/UI/VivyDocumentView.cc
+++ b/src/UI/VivyDocumentView.cc
@@ -92,8 +92,8 @@ VivyDocumentView::loadVideoView() noexcept
     }
 
     Utils::deleteInternalWidget(videoView);
-    videoView->setWidget(new VideoView(videoView));
-    qobject_cast<VideoView *>(videoView->widget())
+    videoView->setWidget(new MpvWidget(videoView));
+    qobject_cast<MpvWidget *>(videoView->widget())
         ->loadFile(document->getVideoSubDocument()->getFilePath());
 }
 
diff --git a/src/UI/VivyDocumentView.hh b/src/UI/VivyDocumentView.hh
index 47ce593ab458a42d1d7e79c9282a683f1c704645..2a6edf5d02b89bedcfafee5b31211d2d1151f9ed 100644
--- a/src/UI/VivyDocumentView.hh
+++ b/src/UI/VivyDocumentView.hh
@@ -11,7 +11,7 @@
 #include "UI/PropertyModel.hh"
 #include "UI/UnclosableDockWidget.hh"
 #include "UI/AbstractDocumentView.hh"
-#include "UI/DocumentViews/VideoView.hh"
+#include "UI/DocumentViews/MpvWidget.hh"
 #include "UI/DocumentViews/AssLinesView.hh"
 #include "UI/DocumentViews/AssLinesModel.hh"