diff --git a/src/Lib/Video.cc b/src/Lib/Video.cc
new file mode 100644
index 0000000000000000000000000000000000000000..d6fe6e57eda1ae7d358d8d4be4a5ef2ccbd99fd5
--- /dev/null
+++ b/src/Lib/Video.cc
@@ -0,0 +1,50 @@
+#include "Video.hh"
+#include "Utils.hh"
+
+#include <QJsonObject>
+#include <QJsonArray>
+
+using namespace Vivy;
+
+VideoContext::VideoContext(const QString &path)
+    : filePath(path)
+{
+    if (!format)
+        throw std::runtime_error("out of memory, can't create allocate the AVFormatContext");
+
+    const std::string stdFilename = filePath.toStdString();
+    const char *filename          = stdFilename.c_str();
+    AVFormatContext *formatPtr    = format.get();
+
+    // Get info from video file
+    if (avformat_open_input(&formatPtr, filename, nullptr, nullptr) != 0) {
+        [[maybe_unused]] void *relatedOnFailure = format.release(); // freed by avformat_open_input
+        throw std::runtime_error("failed to open file");
+    }
+
+    if (avformat_find_stream_info(formatPtr, nullptr) < 0) {
+        throw std::runtime_error("failed to get video stream info");
+    }
+
+    // Populate all the stream indexes
+    for (uint i = 0; i < format->nb_streams; ++i) {
+        AVStream *itFormat        = format->streams[i];
+        AVCodecParameters *params = itFormat->codecpar;
+        AVCodec *streamCodec      = avcodec_find_decoder(params->codec_id);
+        if (streamCodec && streamCodec->type == AVMEDIA_TYPE_VIDEO) {
+            videoStreams.insert(i, std::make_shared<Stream>(streamCodec, formatPtr, itFormat, i));
+        }
+    }
+
+    // Get the default stream
+    defaultStreamIndex = av_find_best_stream(formatPtr, AVMEDIA_TYPE_VIDEO,
+                                             -1, // Let AV find one stream
+                                             -1, // We don't want related streams
+                                             nullptr, 0);
+    if (defaultStreamIndex < 0) {
+        qCritical() << "Could not find the best video stream";
+    }
+
+    qDebug() << "Opened video context for" << path << "with duration" << formatPtr->duration
+             << "and default stream index" << defaultStreamIndex;
+}
diff --git a/src/Lib/Video.hh b/src/Lib/Video.hh
new file mode 100644
index 0000000000000000000000000000000000000000..8dbb47580865610cb94b14c54c59ad1fdfaefe0d
--- /dev/null
+++ b/src/Lib/Video.hh
@@ -0,0 +1,88 @@
+#pragma once
+
+#ifndef __cplusplus
+#error "This is a C++ header"
+#endif
+
+extern "C" {
+#include <libavutil/opt.h>
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libswresample/swresample.h>
+#include <libavcodec/avfft.h>
+#include <memory.h>
+}
+
+#include "Utils.hh"
+#include <QtGlobal>
+#include <QMap>
+#include <QVector>
+#include <QString>
+
+namespace Vivy
+{
+class VideoContext;
+
+// Like an audio context, but for videos.
+class VideoContext final {
+    VIVY_UNMOVABLE_OBJECT(VideoContext)
+
+public:
+    // Hold all the data for a video stream. Should only be owned by the parent
+    // VideoContext instance.
+    class Stream final {
+        VIVY_UNMOVABLE_OBJECT(Stream)
+
+    public:
+        Stream(AVCodec *, AVFormatContext *, AVStream *, int index);
+        ~Stream() noexcept;
+
+        size_t getWidth() const noexcept;
+        size_t getHeight() const noexcept;
+        size_t getDuration() const noexcept;
+        size_t getFramesPerSecond() const noexcept;
+
+        QJsonObject getProperties() const noexcept;
+
+        static inline Utils::DeleterFunctionType<AVCodecContext> codecContexteleter =
+            std::bind_front(Utils::freePPtrIfNotNull<AVCodecContext>, avcodec_free_context);
+
+        using AVCodecContextPtr = std::unique_ptr<AVCodecContext, decltype(codecContexteleter)>;
+
+    private:
+        AVCodecID codecId{ AV_CODEC_ID_NONE };
+        AVCodec *codec{ nullptr };
+        AVCodecParameters *codecParams{ nullptr };
+        AVCodecContextPtr codecContext{ nullptr };
+
+        AVStream *videoStream{ nullptr };
+
+        int streamIndexInVideoContext;
+    };
+
+    using StreamPtr     = std::shared_ptr<Stream>;
+    using StreamWeakPtr = std::weak_ptr<Stream>;
+
+public:
+    VideoContext(const QString &path);
+
+    StreamWeakPtr getStream(int) const noexcept;
+    StreamWeakPtr getDefaultStream() const noexcept;
+
+    QString getElementName() const noexcept;
+    QJsonDocument getProperties() const noexcept;
+
+private:
+    static inline Utils::DeleterFunctionType<AVFormatContext> avFormatContextDeleter =
+        std::bind_front(Utils::freePtrIfNotNull<AVFormatContext>, avformat_free_context);
+    using AVFormatContextPtr = std::unique_ptr<AVFormatContext, decltype(avFormatContextDeleter)>;
+    AVFormatContextPtr format{ avformat_alloc_context(), avFormatContextDeleter };
+
+    const QString filePath;
+    QMap<uint, StreamPtr> videoStreams{};
+
+    int defaultStreamIndex{ -1 };
+
+    StreamPtr spareNullSreamPtr{ nullptr };
+};
+};