diff --git a/.ci/linux-appimage/docker.sh b/.ci/linux-appimage/docker.sh
index bb5488c59..fc866940e 100755
--- a/.ci/linux-appimage/docker.sh
+++ b/.ci/linux-appimage/docker.sh
@@ -3,7 +3,7 @@
 #Building Citra
 mkdir build
 cd build
-cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_FFMPEG_VIDEO_DUMPER=ON
+cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
 ninja
 
 ctest -VV -C Release
diff --git a/.ci/linux-fresh/docker.sh b/.ci/linux-fresh/docker.sh
index 29bf52cf3..fd145ed62 100755
--- a/.ci/linux-fresh/docker.sh
+++ b/.ci/linux-fresh/docker.sh
@@ -1,7 +1,7 @@
 #!/bin/bash -ex
 
 mkdir build && cd build
-cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_FFMPEG_VIDEO_DUMPER=ON
+cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=/usr/lib/ccache/gcc -DCMAKE_CXX_COMPILER=/usr/lib/ccache/g++ -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON
 ninja
 
 ctest -VV -C Release
diff --git a/.ci/linux-mingw/docker.sh b/.ci/linux-mingw/docker.sh
index c26bfa30c..df1a4c9bf 100755
--- a/.ci/linux-mingw/docker.sh
+++ b/.ci/linux-mingw/docker.sh
@@ -5,7 +5,7 @@ mkdir -p "$HOME/.ccache/"
 echo 'max_size = 3.0G' > "$HOME/.ccache/ccache.conf"
 
 mkdir build && cd build
-cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DCITRA_USE_CCACHE=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_MF=ON -DENABLE_FFMPEG_VIDEO_DUMPER=ON -DCMAKE_NO_SYSTEM_FROM_IMPORTED=TRUE -DCOMPILE_WITH_DWARF=OFF
+cmake .. -G Ninja -DCMAKE_TOOLCHAIN_FILE="$(pwd)/../CMakeModules/MinGWCross.cmake" -DCITRA_USE_CCACHE=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_QT_TRANSLATION=ON -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON -DUSE_DISCORD_PRESENCE=ON -DENABLE_MF=ON -DCMAKE_NO_SYSTEM_FROM_IMPORTED=TRUE -DCOMPILE_WITH_DWARF=OFF
 ninja
 
 echo "Tests skipped"
diff --git a/.ci/macos/build.sh b/.ci/macos/build.sh
index 94fd523af..9ef3f4973 100755
--- a/.ci/macos/build.sh
+++ b/.ci/macos/build.sh
@@ -22,7 +22,6 @@ cmake .. -DCMAKE_BUILD_TYPE=Release \
     -DCITRA_ENABLE_COMPATIBILITY_REPORTING=${ENABLE_COMPATIBILITY_REPORTING:-"OFF"} \
     -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
     -DUSE_DISCORD_PRESENCE=ON \
-    -DENABLE_FFMPEG_VIDEO_DUMPER=ON \
     -DENABLE_ASM=OFF \
     -GNinja
 ninja
diff --git a/.ci/windows-msvc/build.sh b/.ci/windows-msvc/build.sh
index 68c2da01d..a811171c6 100644
--- a/.ci/windows-msvc/build.sh
+++ b/.ci/windows-msvc/build.sh
@@ -11,7 +11,6 @@ cmake .. \
     -DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
     -DUSE_DISCORD_PRESENCE=ON \
     -DENABLE_MF=ON \
-    -DENABLE_FFMPEG_VIDEO_DUMPER=ON \
     -DOPENSSL_DLL_DIR="C:\Program Files\OpenSSL\bin"
 
 ninja
diff --git a/.gitmodules b/.gitmodules
index 6d0b09029..e5187598c 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -79,3 +79,6 @@
 [submodule "sirit"]
     path = externals/sirit
     url = https://github.com/yuzu-emu/sirit
+[submodule "library-headers"]
+    path = externals/library-headers
+    url = https://github.com/citra-emu/ext-library-headers.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 12eae712f..81b956070 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -48,15 +48,6 @@ option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
 
 CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
 
-option(ENABLE_FFMPEG_AUDIO_DECODER "Enable FFmpeg audio (AAC) decoder" OFF)
-option(ENABLE_FFMPEG_VIDEO_DUMPER "Enable FFmpeg video dumper" OFF)
-
-if (ENABLE_FFMPEG_AUDIO_DECODER OR ENABLE_FFMPEG_VIDEO_DUMPER)
-    set(ENABLE_FFMPEG ON)
-endif()
-
-CMAKE_DEPENDENT_OPTION(CITRA_USE_BUNDLED_FFMPEG "Download bundled FFmpeg binaries" ON "ENABLE_FFMPEG;MSVC OR APPLE" OFF)
-
 option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
 
 option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
@@ -68,7 +59,6 @@ CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ON "
 
 option(USE_SYSTEM_BOOST "Use the system Boost libs (instead of the bundled ones)" OFF)
 
-CMAKE_DEPENDENT_OPTION(ENABLE_FDK "Use FDK AAC decoder" OFF "NOT ENABLE_FFMPEG_AUDIO_DECODER;NOT ENABLE_MF" OFF)
 
 CMAKE_DEPENDENT_OPTION(CITRA_USE_BUNDLED_MOLTENVK "Download the bundled MoltenVK" ON "APPLE" OFF)
 
@@ -200,6 +190,8 @@ message(STATUS "Target architecture: ${ARCHITECTURE}")
 
 # boost asio's concept usage doesn't play nicely with some compilers yet.
 add_definitions(-DBOOST_ASIO_DISABLE_CONCEPTS)
+# boost can have issues compiling with C++17 and up on newer versions of Clang.
+add_definitions(-DBOOST_NO_CXX98_FUNCTION_BASE)
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
@@ -250,43 +242,6 @@ if (ENABLE_LIBUSB)
     endif()
 endif()
 
-if (ENABLE_FFMPEG)
-    if (CITRA_USE_BUNDLED_FFMPEG)
-        if ((MSVC_VERSION GREATER_EQUAL 1920 AND MSVC_VERSION LESS 1940) AND "x86_64" IN_LIST ARCHITECTURE)
-            set(FFmpeg_VER "ffmpeg-4.1-win64")
-        elseif (APPLE)
-            set(FFmpeg_VER "ffmpeg-6.0")
-        else()
-            message(FATAL_ERROR "No bundled FFmpeg binaries for your toolchain. Disable CITRA_USE_BUNDLED_FFMPEG and provide your own.")
-        endif()
-
-        if (DEFINED FFmpeg_VER)
-            download_bundled_external("ffmpeg/" ${FFmpeg_VER} FFmpeg_PREFIX)
-            set(FFMPEG_DIR "${FFmpeg_PREFIX}")
-        endif()
-    endif()
-
-    if (ENABLE_FFMPEG_VIDEO_DUMPER)
-        find_package(FFmpeg REQUIRED COMPONENTS avcodec avfilter avformat avutil swresample)
-    else()
-        find_package(FFmpeg REQUIRED COMPONENTS avcodec)
-    endif()
-    if ("${FFmpeg_avcodec_VERSION}" VERSION_LESS "58.4.100")
-        message(FATAL_ERROR "Found version for libavcodec is too low. The required version is at least 58.4.100 (included in FFmpeg 4.0 and later).")
-    endif()
-endif()
-
-if (ENABLE_FFMPEG_VIDEO_DUMPER)
-    add_definitions(-DENABLE_FFMPEG_VIDEO_DUMPER)
-endif()
-
-if (ENABLE_FDK)
-    find_library(FDK_AAC fdk-aac DOC "The path to fdk_aac library")
-    if(FDK_AAC STREQUAL "FDK_AAC-NOTFOUND")
-        message(FATAL_ERROR "fdk_aac library not found.")
-    endif()
-endif()
-
 # Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
 find_package(tsl-robin-map QUIET)
 
diff --git a/CMakeModules/CopyCitraFFmpegDeps.cmake b/CMakeModules/CopyCitraFFmpegDeps.cmake
deleted file mode 100644
index 532f478ce..000000000
--- a/CMakeModules/CopyCitraFFmpegDeps.cmake
+++ /dev/null
@@ -1,13 +0,0 @@
-function(copy_citra_FFmpeg_deps target_dir)
-    include(WindowsCopyFiles)
-    set(DLL_DEST "${CMAKE_BINARY_DIR}/bin/$<CONFIG>/")
-    windows_copy_files(${target_dir} ${FFMPEG_DIR}/bin ${DLL_DEST}
-        avcodec*.dll
-        avfilter*.dll
-        avformat*.dll
-        avutil*.dll
-        postproc*.dll
-        swresample*.dll
-        swscale*.dll
-    )
-endfunction(copy_citra_FFmpeg_deps)
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index e744dac8c..8130df3fe 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -117,6 +117,9 @@ endif()
 # Open Source Archives
 add_subdirectory(open_source_archives)
 
+# Dynamic library headers
+add_subdirectory(library-headers EXCLUDE_FROM_ALL)
+
 # SoundTouch
 set(INTEGER_SAMPLES ON CACHE BOOL "")
 set(SOUNDSTRETCH OFF CACHE BOOL "")
diff --git a/externals/cmake-modules/FindFFmpeg.cmake b/externals/cmake-modules/FindFFmpeg.cmake
deleted file mode 100644
index 5a1a6c4b8..000000000
--- a/externals/cmake-modules/FindFFmpeg.cmake
+++ /dev/null
@@ -1,192 +0,0 @@
-# FindFFmpeg
-# ----------
-#
-# Find the native FFmpeg includes and libraries
-#
-# This module defines the following variables:
-#
-#  FFmpeg_INCLUDE_<component>: where to find <component>.h
-#  FFmpeg_LIBRARY_<component>: where to find the <component> library
-#  FFmpeg_INCLUDES: aggregate all the include paths
-#  FFmpeg_LIBRARIES: aggregate all the paths to the libraries
-#  FFmpeg_FOUND: True if all components have been found
-#
-# This module defines the following targets, which are prefered over variables:
-#
-#  FFmpeg::<component>: Target to use <component> directly, with include path,
-#    library and dependencies set up. If you are using a static build, you are
-#    responsible for adding any external dependencies (such as zlib, bzlib...).
-#
-# <component> can be one of:
-#   avcodec
-#   avdevice
-#   avfilter
-#   avformat
-#   postproc
-#   swresample
-#   swscale
-#
-
-set(_FFmpeg_ALL_COMPONENTS
-  avcodec
-  avdevice
-  avfilter
-  avformat
-  avutil
-  postproc
-  swresample
-  swscale
-)
-
-set(_FFmpeg_DEPS_avcodec avutil)
-set(_FFmpeg_DEPS_avdevice avcodec avformat avutil)
-set(_FFmpeg_DEPS_avfilter avutil)
-set(_FFmpeg_DEPS_avformat avcodec avutil)
-set(_FFmpeg_DEPS_postproc avutil)
-set(_FFmpeg_DEPS_swresample avutil)
-set(_FFmpeg_DEPS_swscale avutil)
-
-function(find_ffmpeg LIBNAME)
-  if(DEFINED ENV{FFMPEG_DIR})
-    set(FFMPEG_DIR $ENV{FFMPEG_DIR})
-  endif()
-
-  if(FFMPEG_DIR)
-    list(APPEND INCLUDE_PATHS
-      ${FFMPEG_DIR}
-      ${FFMPEG_DIR}/ffmpeg
-      ${FFMPEG_DIR}/lib${LIBNAME}
-      ${FFMPEG_DIR}/include/lib${LIBNAME}
-      ${FFMPEG_DIR}/include/ffmpeg
-      ${FFMPEG_DIR}/include
-      NO_DEFAULT_PATH
-      NO_CMAKE_FIND_ROOT_PATH
-    )
-    list(APPEND LIB_PATHS
-      ${FFMPEG_DIR}
-      ${FFMPEG_DIR}/lib
-      ${FFMPEG_DIR}/lib${LIBNAME}
-      NO_DEFAULT_PATH
-      NO_CMAKE_FIND_ROOT_PATH
-    )
-  else()
-    list(APPEND INCLUDE_PATHS
-      /usr/local/include/ffmpeg
-      /usr/local/include/lib${LIBNAME}
-      /usr/include/ffmpeg
-      /usr/include/lib${LIBNAME}
-      /usr/include/ffmpeg/lib${LIBNAME}
-    )
-
-    list(APPEND LIB_PATHS
-      /usr/local/lib
-      /usr/lib
-    )
-  endif()
-
-  find_path(FFmpeg_INCLUDE_${LIBNAME} lib${LIBNAME}/${LIBNAME}.h
-    HINTS ${INCLUDE_PATHS}
-  )
-
-  find_library(FFmpeg_LIBRARY_${LIBNAME} ${LIBNAME}
-    HINTS ${LIB_PATHS}
-  )
-
-  if(NOT FFMPEG_DIR AND (NOT FFmpeg_LIBRARY_${LIBNAME} OR NOT FFmpeg_INCLUDE_${LIBNAME}))
-    # Didn't find it in the usual paths, try pkg-config
-    find_package(PkgConfig QUIET)
-    pkg_check_modules(FFmpeg_PKGCONFIG_${LIBNAME} QUIET lib${LIBNAME})
-
-    find_path(FFmpeg_INCLUDE_${LIBNAME} lib${LIBNAME}/${LIBNAME}.h
-      ${FFmpeg_PKGCONFIG_${LIBNAME}_INCLUDE_DIRS}
-    )
-
-    find_library(FFmpeg_LIBRARY_${LIBNAME} ${LIBNAME}
-      ${FFmpeg_PKGCONFIG_${LIBNAME}_LIBRARY_DIRS}
-    )
-  endif()
-
-  if(FFmpeg_INCLUDE_${LIBNAME} AND FFmpeg_LIBRARY_${LIBNAME})
-    set(FFmpeg_INCLUDE_${LIBNAME} "${FFmpeg_INCLUDE_${LIBNAME}}" PARENT_SCOPE)
-    set(FFmpeg_LIBRARY_${LIBNAME} "${FFmpeg_LIBRARY_${LIBNAME}}" PARENT_SCOPE)
-
-    # Extract FFmpeg version from version.h
-    foreach(v MAJOR MINOR MICRO)
-      set(FFmpeg_${LIBNAME}_VERSION_${v} 0)
-    endforeach()
-    string(TOUPPER ${LIBNAME} LIBNAME_UPPER)
-    file(STRINGS "${FFmpeg_INCLUDE_${LIBNAME}}/lib${LIBNAME}/version.h" _FFmpeg_VERSION_H_CONTENTS REGEX "#define LIB${LIBNAME_UPPER}_VERSION_(MAJOR|MINOR|MICRO) ")
-    if (EXISTS "${FFmpeg_INCLUDE_${LIBNAME}}/lib${LIBNAME}/version_major.h")
-      file(STRINGS "${FFmpeg_INCLUDE_${LIBNAME}}/lib${LIBNAME}/version_major.h" _FFmpeg_MAJOR_VERSION_H_CONTENTS REGEX "#define LIB${LIBNAME_UPPER}_VERSION_MAJOR ")
-      string(APPEND _FFmpeg_VERSION_H_CONTENTS "\n" ${_FFmpeg_MAJOR_VERSION_H_CONTENTS})
-    endif()
-    set(_FFmpeg_VERSION_REGEX "([0-9]+)")
-    foreach(v MAJOR MINOR MICRO)
-      if("${_FFmpeg_VERSION_H_CONTENTS}" MATCHES "#define LIB${LIBNAME_UPPER}_VERSION_${v}[\\t ]+${_FFmpeg_VERSION_REGEX}")
-        set(FFmpeg_${LIBNAME}_VERSION_${v} "${CMAKE_MATCH_1}")
-      endif()
-    endforeach()
-    set(FFmpeg_${LIBNAME}_VERSION "${FFmpeg_${LIBNAME}_VERSION_MAJOR}.${FFmpeg_${LIBNAME}_VERSION_MINOR}.${FFmpeg_${LIBNAME}_VERSION_MICRO}")
-    set(FFmpeg_${c}_VERSION "${FFmpeg_${LIBNAME}_VERSION}" PARENT_SCOPE)
-    unset(_FFmpeg_VERSION_REGEX)
-    unset(_FFmpeg_VERSION_H_CONTENTS)
-
-    set(FFmpeg_${c}_FOUND TRUE PARENT_SCOPE)
-    if(NOT FFmpeg_FIND_QUIETLY)
-      message("--  Found ${LIBNAME}: ${FFmpeg_INCLUDE_${LIBNAME}} ${FFmpeg_LIBRARY_${LIBNAME}} (version: ${FFmpeg_${LIBNAME}_VERSION})")
-    endif()
-  endif()
-endfunction()
-
-foreach(c ${_FFmpeg_ALL_COMPONENTS})
-  find_ffmpeg(${c})
-endforeach()
-
-foreach(c ${_FFmpeg_ALL_COMPONENTS})
-  if(FFmpeg_${c}_FOUND)
-    list(APPEND FFmpeg_INCLUDES ${FFmpeg_INCLUDE_${c}})
-    list(APPEND FFmpeg_LIBRARIES ${FFmpeg_LIBRARY_${c}})
-
-    add_library(FFmpeg::${c} IMPORTED UNKNOWN)
-    set_target_properties(FFmpeg::${c} PROPERTIES
-      IMPORTED_LOCATION ${FFmpeg_LIBRARY_${c}}
-      INTERFACE_INCLUDE_DIRECTORIES ${FFmpeg_INCLUDE_${c}}
-    )
-    if(APPLE)
-      set_target_properties(FFmpeg::${c} PROPERTIES
-        MACOSX_RPATH 1
-      )
-    endif()
-    if(_FFmpeg_DEPS_${c})
-      set(deps)
-      foreach(dep ${_FFmpeg_DEPS_${c}})
-        list(APPEND deps FFmpeg::${dep})
-      endforeach()
-
-      set_target_properties(FFmpeg::${c} PROPERTIES
-        INTERFACE_LINK_LIBRARIES "${deps}"
-      )
-      unset(deps)
-    endif()
-  endif()
-endforeach()
-
-if(FFmpeg_INCLUDES)
-  list(REMOVE_DUPLICATES FFmpeg_INCLUDES)
-endif()
-
-foreach(c ${FFmpeg_FIND_COMPONENTS})
-  list(APPEND _FFmpeg_REQUIRED_VARS FFmpeg_INCLUDE_${c} FFmpeg_LIBRARY_${c})
-endforeach()
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(FFmpeg
-  REQUIRED_VARS ${_FFmpeg_REQUIRED_VARS}
-  HANDLE_COMPONENTS
-)
-
-foreach(c ${_FFmpeg_ALL_COMPONENTS})
-  unset(_FFmpeg_DEPS_${c})
-endforeach()
-unset(_FFmpeg_ALL_COMPONENTS)
-unset(_FFmpeg_REQUIRED_VARS)
diff --git a/externals/library-headers b/externals/library-headers
new file mode 160000
index 000000000..071bc4282
--- /dev/null
+++ b/externals/library-headers
@@ -0,0 +1 @@
+Subproject commit 071bc4282ca29ec255ab2dae32c978481ca5dfea
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 32fb85a13..4385cdf5f 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -9,6 +9,10 @@ add_library(audio_core STATIC
     hle/common.h
     hle/decoder.cpp
     hle/decoder.h
+    hle/fdk_decoder.cpp
+    hle/fdk_decoder.h
+    hle/ffmpeg_decoder.cpp
+    hle/ffmpeg_decoder.h
     hle/filter.cpp
     hle/filter.h
     hle/hle.cpp
@@ -67,26 +71,6 @@ elseif(ENABLE_AUDIOTOOLBOX)
     find_library(AUDIOTOOLBOX AudioToolbox)
     target_link_libraries(audio_core PRIVATE ${AUDIOTOOLBOX})
     target_compile_definitions(audio_core PUBLIC HAVE_AUDIOTOOLBOX)
-elseif(ENABLE_FFMPEG_AUDIO_DECODER)
-    target_sources(audio_core PRIVATE
-        hle/ffmpeg_decoder.cpp
-        hle/ffmpeg_decoder.h
-        hle/ffmpeg_dl.cpp
-        hle/ffmpeg_dl.h
-    )
-    if(UNIX)
-        target_link_libraries(audio_core PRIVATE FFmpeg::avcodec)
-    else()
-        target_include_directories(audio_core PRIVATE ${FFMPEG_DIR}/include)
-    endif()
-    target_compile_definitions(audio_core PUBLIC HAVE_FFMPEG)
-elseif(ENABLE_FDK)
-    target_sources(audio_core PRIVATE
-        hle/fdk_decoder.cpp
-        hle/fdk_decoder.h
-    )
-    target_link_libraries(audio_core PRIVATE ${FDK_AAC})
-    target_compile_definitions(audio_core PUBLIC HAVE_FDK)
 endif()
 
 if(ANDROID)
diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp
index fee8caed9..4948eb958 100644
--- a/src/audio_core/dsp_interface.cpp
+++ b/src/audio_core/dsp_interface.cpp
@@ -47,8 +47,9 @@ void DspInterface::OutputFrame(StereoFrame16 frame) {
 
     fifo.Push(frame.data(), frame.size());
 
-    if (Core::System::GetInstance().VideoDumper().IsDumping()) {
-        Core::System::GetInstance().VideoDumper().AddAudioFrame(std::move(frame));
+    auto video_dumper = Core::System::GetInstance().GetVideoDumper();
+    if (video_dumper && video_dumper->IsDumping()) {
+        video_dumper->AddAudioFrame(std::move(frame));
     }
 }
 
@@ -58,8 +59,9 @@ void DspInterface::OutputSample(std::array<s16, 2> sample) {
 
     fifo.Push(&sample, 1);
 
-    if (Core::System::GetInstance().VideoDumper().IsDumping()) {
-        Core::System::GetInstance().VideoDumper().AddAudioSample(std::move(sample));
+    auto video_dumper = Core::System::GetInstance().GetVideoDumper();
+    if (video_dumper && video_dumper->IsDumping()) {
+        video_dumper->AddAudioSample(std::move(sample));
     }
 }
 
diff --git a/src/audio_core/hle/fdk_decoder.cpp b/src/audio_core/hle/fdk_decoder.cpp
index 6a29d623d..0de539085 100644
--- a/src/audio_core/hle/fdk_decoder.cpp
+++ b/src/audio_core/hle/fdk_decoder.cpp
@@ -2,8 +2,10 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
-#include <fdk-aac/aacdecoder_lib.h>
 #include "audio_core/hle/fdk_decoder.h"
+#include "common/dynamic_library/fdk-aac.h"
+
+using namespace DynamicLibrary;
 
 namespace AudioCore::HLE {
 
@@ -29,13 +31,17 @@ private:
 };
 
 FDKDecoder::Impl::Impl(Memory::MemorySystem& memory) : memory(memory) {
+    if (!FdkAac::LoadFdkAac()) {
+        return;
+    }
+
     // allocate an array of LIB_INFO structures
     // if we don't pre-fill the whole segment with zeros, when we call `aacDecoder_GetLibInfo`
     // it will segfault, upon investigation, there is some code in fdk_aac depends on your initial
     // values in this array
     LIB_INFO decoder_info[FDK_MODULE_LAST] = {};
     // get library information and fill the struct
-    if (aacDecoder_GetLibInfo(decoder_info) != 0) {
+    if (FdkAac::aacDecoder_GetLibInfo(decoder_info) != 0) {
         LOG_ERROR(Audio_DSP, "Failed to retrieve fdk_aac library information!");
         return;
     }
@@ -44,14 +50,14 @@ FDKDecoder::Impl::Impl(Memory::MemorySystem& memory) : memory(memory) {
              decoder_info[0].build_date);
 
     // choose the input format when initializing: 1 layer of ADTS
-    decoder = aacDecoder_Open(TRANSPORT_TYPE::TT_MP4_ADTS, 1);
+    decoder = FdkAac::aacDecoder_Open(TRANSPORT_TYPE::TT_MP4_ADTS, 1);
     // set maximum output channel to two (stereo)
     // if the input samples have more channels, fdk_aac will perform a downmix
-    AAC_DECODER_ERROR ret = aacDecoder_SetParam(decoder, AAC_PCM_MAX_OUTPUT_CHANNELS, 2);
+    AAC_DECODER_ERROR ret = FdkAac::aacDecoder_SetParam(decoder, AAC_PCM_MAX_OUTPUT_CHANNELS, 2);
     if (ret != AAC_DEC_OK) {
         // unable to set this parameter reflects the decoder implementation might be broken
         // we'd better shuts down everything
-        aacDecoder_Close(decoder);
+        FdkAac::aacDecoder_Close(decoder);
         decoder = nullptr;
         LOG_ERROR(Audio_DSP, "Unable to set downmix parameter: {}", ret);
         return;
@@ -73,8 +79,9 @@ std::optional<BinaryMessage> FDKDecoder::Impl::Initalize(const BinaryMessage& re
 }
 
 FDKDecoder::Impl::~Impl() {
-    if (decoder)
-        aacDecoder_Close(decoder);
+    if (decoder) {
+        FdkAac::aacDecoder_Close(decoder);
+    }
 }
 
 void FDKDecoder::Impl::Clear() {
@@ -84,9 +91,10 @@ void FDKDecoder::Impl::Clear() {
     // FLUSH - flush internal buffer
     // INTR - treat the current internal buffer as discontinuous
     // CONCEAL - try to interpolate and smooth out the samples
-    if (decoder)
-        aacDecoder_DecodeFrame(decoder, decoder_output, 8192,
-                               AACDEC_FLUSH & AACDEC_INTR & AACDEC_CONCEAL);
+    if (decoder) {
+        FdkAac::aacDecoder_DecodeFrame(decoder, decoder_output, 8192,
+                                       AACDEC_FLUSH & AACDEC_INTR & AACDEC_CONCEAL);
+    }
 }
 
 std::optional<BinaryMessage> FDKDecoder::Impl::ProcessRequest(const BinaryMessage& request) {
@@ -140,7 +148,7 @@ std::optional<BinaryMessage> FDKDecoder::Impl::Decode(const BinaryMessage& reque
 
     std::array<std::vector<s16>, 2> out_streams;
 
-    std::size_t data_size = request.decode_aac_request.size;
+    u32 data_size = request.decode_aac_request.size;
 
     // decoding loops
     AAC_DECODER_ERROR result = AAC_DEC_OK;
@@ -156,18 +164,18 @@ std::optional<BinaryMessage> FDKDecoder::Impl::Decode(const BinaryMessage& reque
     while (buffer_remaining) {
         // queue the input buffer, fdk_aac will automatically slice out the buffer it needs
         // from the input buffer
-        result = aacDecoder_Fill(decoder, &data, &input_size, &buffer_remaining);
+        result = FdkAac::aacDecoder_Fill(decoder, &data, &input_size, &buffer_remaining);
         if (result != AAC_DEC_OK) {
             // there are some issues when queuing the input buffer
             LOG_ERROR(Audio_DSP, "Failed to enqueue the input samples");
             return std::nullopt;
         }
         // get output from decoder
-        result = aacDecoder_DecodeFrame(decoder, decoder_output,
-                                        sizeof(decoder_output) / sizeof(s16), 0);
+        result = FdkAac::aacDecoder_DecodeFrame(decoder, decoder_output,
+                                                sizeof(decoder_output) / sizeof(s16), 0);
         if (result == AAC_DEC_OK) {
             // get the stream information
-            stream_info = aacDecoder_GetStreamInfo(decoder);
+            stream_info = FdkAac::aacDecoder_GetStreamInfo(decoder);
             // fill the stream information for binary response
             response.decode_aac_response.sample_rate = GetSampleRateEnum(stream_info->sampleRate);
             response.decode_aac_response.num_channels = stream_info->numChannels;
diff --git a/src/audio_core/hle/ffmpeg_decoder.cpp b/src/audio_core/hle/ffmpeg_decoder.cpp
index c58bc0c3e..d291343ca 100644
--- a/src/audio_core/hle/ffmpeg_decoder.cpp
+++ b/src/audio_core/hle/ffmpeg_decoder.cpp
@@ -3,7 +3,9 @@
 // Refer to the license.txt file included.
 
 #include "audio_core/hle/ffmpeg_decoder.h"
-#include "audio_core/hle/ffmpeg_dl.h"
+#include "common/dynamic_library/ffmpeg.h"
+
+using namespace DynamicLibrary;
 
 namespace AudioCore::HLE {
 
@@ -25,25 +27,25 @@ private:
 
     struct AVPacketDeleter {
         void operator()(AVPacket* packet) const {
-            av_packet_free_dl(&packet);
+            FFmpeg::av_packet_free(&packet);
         }
     };
 
     struct AVCodecContextDeleter {
         void operator()(AVCodecContext* context) const {
-            avcodec_free_context_dl(&context);
+            FFmpeg::avcodec_free_context(&context);
         }
     };
 
     struct AVCodecParserContextDeleter {
         void operator()(AVCodecParserContext* parser) const {
-            av_parser_close_dl(parser);
+            FFmpeg::av_parser_close(parser);
         }
     };
 
     struct AVFrameDeleter {
         void operator()(AVFrame* frame) const {
-            av_frame_free_dl(&frame);
+            FFmpeg::av_frame_free(&frame);
         }
     };
 
@@ -60,7 +62,7 @@ private:
 };
 
 FFMPEGDecoder::Impl::Impl(Memory::MemorySystem& memory) : memory(memory) {
-    have_ffmpeg_dl = InitFFmpegDL();
+    have_ffmpeg_dl = FFmpeg::LoadFFmpeg();
 }
 
 FFMPEGDecoder::Impl::~Impl() = default;
@@ -102,27 +104,27 @@ std::optional<BinaryMessage> FFMPEGDecoder::Impl::Initalize(const BinaryMessage&
         return response;
     }
 
-    av_packet.reset(av_packet_alloc_dl());
+    av_packet.reset(FFmpeg::av_packet_alloc());
 
-    codec = avcodec_find_decoder_dl(AV_CODEC_ID_AAC);
+    codec = FFmpeg::avcodec_find_decoder(AV_CODEC_ID_AAC);
     if (!codec) {
         LOG_ERROR(Audio_DSP, "Codec not found\n");
         return response;
     }
 
-    parser.reset(av_parser_init_dl(codec->id));
+    parser.reset(FFmpeg::av_parser_init(codec->id));
     if (!parser) {
         LOG_ERROR(Audio_DSP, "Parser not found\n");
         return response;
     }
 
-    av_context.reset(avcodec_alloc_context3_dl(codec));
+    av_context.reset(FFmpeg::avcodec_alloc_context3(codec));
     if (!av_context) {
         LOG_ERROR(Audio_DSP, "Could not allocate audio codec context\n");
         return response;
     }
 
-    if (avcodec_open2_dl(av_context.get(), codec, nullptr) < 0) {
+    if (FFmpeg::avcodec_open2(av_context.get(), codec, nullptr) < 0) {
         LOG_ERROR(Audio_DSP, "Could not open codec\n");
         return response;
     }
@@ -170,16 +172,16 @@ std::optional<BinaryMessage> FFMPEGDecoder::Impl::Decode(const BinaryMessage& re
     std::size_t data_size = request.decode_aac_request.size;
     while (data_size > 0) {
         if (!decoded_frame) {
-            decoded_frame.reset(av_frame_alloc_dl());
+            decoded_frame.reset(FFmpeg::av_frame_alloc());
             if (!decoded_frame) {
                 LOG_ERROR(Audio_DSP, "Could not allocate audio frame");
                 return {};
             }
         }
 
-        int ret =
-            av_parser_parse2_dl(parser.get(), av_context.get(), &av_packet->data, &av_packet->size,
-                                data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
+        int ret = FFmpeg::av_parser_parse2(parser.get(), av_context.get(), &av_packet->data,
+                                           &av_packet->size, data, static_cast<int>(data_size),
+                                           AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
         if (ret < 0) {
             LOG_ERROR(Audio_DSP, "Error while parsing");
             return {};
@@ -187,7 +189,7 @@ std::optional<BinaryMessage> FFMPEGDecoder::Impl::Decode(const BinaryMessage& re
         data += ret;
         data_size -= ret;
 
-        ret = avcodec_send_packet_dl(av_context.get(), av_packet.get());
+        ret = FFmpeg::avcodec_send_packet(av_context.get(), av_packet.get());
         if (ret < 0) {
             LOG_ERROR(Audio_DSP, "Error submitting the packet to the decoder");
             return {};
@@ -195,33 +197,39 @@ std::optional<BinaryMessage> FFMPEGDecoder::Impl::Decode(const BinaryMessage& re
 
         if (av_packet->size) {
             while (ret >= 0) {
-                ret = avcodec_receive_frame_dl(av_context.get(), decoded_frame.get());
+                ret = FFmpeg::avcodec_receive_frame(av_context.get(), decoded_frame.get());
                 if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
                     break;
                 else if (ret < 0) {
                     LOG_ERROR(Audio_DSP, "Error during decoding");
                     return {};
                 }
-                int bytes_per_sample = av_get_bytes_per_sample_dl(av_context->sample_fmt);
+                int bytes_per_sample = FFmpeg::av_get_bytes_per_sample(av_context->sample_fmt);
                 if (bytes_per_sample < 0) {
                     LOG_ERROR(Audio_DSP, "Failed to calculate data size");
                     return {};
                 }
 
-                ASSERT(decoded_frame->channels <= out_streams.size());
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100)
+                auto num_channels = static_cast<u32>(decoded_frame->ch_layout.nb_channels);
+#else
+                auto num_channels = static_cast<u32>(decoded_frame->channels);
+#endif
+
+                ASSERT(num_channels <= out_streams.size());
 
                 std::size_t size = bytes_per_sample * (decoded_frame->nb_samples);
 
                 response.decode_aac_response.sample_rate =
                     GetSampleRateEnum(decoded_frame->sample_rate);
-                response.decode_aac_response.num_channels = decoded_frame->channels;
+                response.decode_aac_response.num_channels = num_channels;
                 response.decode_aac_response.num_samples += decoded_frame->nb_samples;
 
                 // FFmpeg converts to 32 signed floating point PCM, we need s16 PCM so we need to
                 // convert it
                 f32 val_float;
                 for (std::size_t current_pos(0); current_pos < size;) {
-                    for (std::size_t channel(0); channel < decoded_frame->channels; channel++) {
+                    for (std::size_t channel(0); channel < num_channels; channel++) {
                         std::memcpy(&val_float, decoded_frame->data[channel] + current_pos,
                                     sizeof(val_float));
                         val_float = std::clamp(val_float, -1.0f, 1.0f);
diff --git a/src/audio_core/hle/ffmpeg_dl.cpp b/src/audio_core/hle/ffmpeg_dl.cpp
deleted file mode 100644
index 073d0aedd..000000000
--- a/src/audio_core/hle/ffmpeg_dl.cpp
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright 2018 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#ifdef _WIN32
-
-#include <memory>
-#include "audio_core/hle/ffmpeg_dl.h"
-#include "common/file_util.h"
-#include "common/logging/log.h"
-#include "common/string_util.h"
-
-namespace {
-
-struct LibraryDeleter {
-    using pointer = HMODULE;
-    void operator()(HMODULE h) const {
-        if (h != nullptr)
-            FreeLibrary(h);
-    }
-};
-
-std::unique_ptr<HMODULE, LibraryDeleter> dll_util{nullptr};
-std::unique_ptr<HMODULE, LibraryDeleter> dll_codec{nullptr};
-
-} // namespace
-
-FuncDL<int(AVSampleFormat)> av_get_bytes_per_sample_dl;
-FuncDL<AVFrame*(void)> av_frame_alloc_dl;
-FuncDL<void(AVFrame**)> av_frame_free_dl;
-FuncDL<AVCodecContext*(const AVCodec*)> avcodec_alloc_context3_dl;
-FuncDL<void(AVCodecContext**)> avcodec_free_context_dl;
-FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)> avcodec_open2_dl;
-FuncDL<AVPacket*(void)> av_packet_alloc_dl;
-FuncDL<void(AVPacket**)> av_packet_free_dl;
-FuncDL<AVCodec*(AVCodecID)> avcodec_find_decoder_dl;
-FuncDL<int(AVCodecContext*, const AVPacket*)> avcodec_send_packet_dl;
-FuncDL<int(AVCodecContext*, AVFrame*)> avcodec_receive_frame_dl;
-FuncDL<AVCodecParserContext*(int)> av_parser_init_dl;
-FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int, int64_t,
-           int64_t, int64_t)>
-    av_parser_parse2_dl;
-FuncDL<void(AVCodecParserContext*)> av_parser_close_dl;
-
-bool InitFFmpegDL() {
-    std::string dll_path = FileUtil::GetUserPath(FileUtil::UserPath::DLLDir);
-    FileUtil::CreateDir(dll_path);
-    std::wstring w_dll_path = Common::UTF8ToUTF16W(dll_path);
-    SetDllDirectoryW(w_dll_path.c_str());
-
-    dll_util.reset(LoadLibrary("avutil-56.dll"));
-    if (!dll_util) {
-        DWORD error_message_id = GetLastError();
-        LPSTR message_buffer = nullptr;
-        size_t size =
-            FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
-                               FORMAT_MESSAGE_IGNORE_INSERTS,
-                           nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
-                           reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
-
-        std::string message(message_buffer, size);
-
-        LocalFree(message_buffer);
-        LOG_ERROR(Audio_DSP, "Could not load avutil-56.dll: {}", message);
-        return false;
-    }
-
-    dll_codec.reset(LoadLibrary("avcodec-58.dll"));
-    if (!dll_codec) {
-        DWORD error_message_id = GetLastError();
-        LPSTR message_buffer = nullptr;
-        size_t size =
-            FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
-                               FORMAT_MESSAGE_IGNORE_INSERTS,
-                           nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
-                           reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
-
-        std::string message(message_buffer, size);
-
-        LocalFree(message_buffer);
-        LOG_ERROR(Audio_DSP, "Could not load avcodec-58.dll: {}", message);
-        return false;
-    }
-    av_get_bytes_per_sample_dl =
-        FuncDL<int(AVSampleFormat)>(dll_util.get(), "av_get_bytes_per_sample");
-    if (!av_get_bytes_per_sample_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_get_bytes_per_sample");
-        return false;
-    }
-
-    av_frame_alloc_dl = FuncDL<AVFrame*()>(dll_util.get(), "av_frame_alloc");
-    if (!av_frame_alloc_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_frame_alloc");
-        return false;
-    }
-
-    av_frame_free_dl = FuncDL<void(AVFrame**)>(dll_util.get(), "av_frame_free");
-    if (!av_frame_free_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_frame_free");
-        return false;
-    }
-
-    avcodec_alloc_context3_dl =
-        FuncDL<AVCodecContext*(const AVCodec*)>(dll_codec.get(), "avcodec_alloc_context3");
-    if (!avcodec_alloc_context3_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_alloc_context3");
-        return false;
-    }
-
-    avcodec_free_context_dl =
-        FuncDL<void(AVCodecContext**)>(dll_codec.get(), "avcodec_free_context");
-    if (!av_get_bytes_per_sample_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_free_context");
-        return false;
-    }
-
-    avcodec_open2_dl = FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)>(
-        dll_codec.get(), "avcodec_open2");
-    if (!avcodec_open2_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_open2");
-        return false;
-    }
-    av_packet_alloc_dl = FuncDL<AVPacket*(void)>(dll_codec.get(), "av_packet_alloc");
-    if (!av_packet_alloc_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_packet_alloc");
-        return false;
-    }
-
-    av_packet_free_dl = FuncDL<void(AVPacket**)>(dll_codec.get(), "av_packet_free");
-    if (!av_packet_free_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_packet_free");
-        return false;
-    }
-
-    avcodec_find_decoder_dl = FuncDL<AVCodec*(AVCodecID)>(dll_codec.get(), "avcodec_find_decoder");
-    if (!avcodec_find_decoder_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_find_decoder");
-        return false;
-    }
-
-    avcodec_send_packet_dl =
-        FuncDL<int(AVCodecContext*, const AVPacket*)>(dll_codec.get(), "avcodec_send_packet");
-    if (!avcodec_send_packet_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_send_packet");
-        return false;
-    }
-
-    avcodec_receive_frame_dl =
-        FuncDL<int(AVCodecContext*, AVFrame*)>(dll_codec.get(), "avcodec_receive_frame");
-    if (!avcodec_receive_frame_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function avcodec_receive_frame");
-        return false;
-    }
-
-    av_parser_init_dl = FuncDL<AVCodecParserContext*(int)>(dll_codec.get(), "av_parser_init");
-    if (!av_parser_init_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_parser_init");
-        return false;
-    }
-
-    av_parser_parse2_dl =
-        FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int,
-                   int64_t, int64_t, int64_t)>(dll_codec.get(), "av_parser_parse2");
-    if (!av_parser_parse2_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_parser_parse2");
-        return false;
-    }
-
-    av_parser_close_dl = FuncDL<void(AVCodecParserContext*)>(dll_codec.get(), "av_parser_close");
-    if (!av_parser_close_dl) {
-        LOG_ERROR(Audio_DSP, "Can not load function av_parser_close");
-        return false;
-    }
-
-    return true;
-}
-
-#endif // _Win32
diff --git a/src/audio_core/hle/ffmpeg_dl.h b/src/audio_core/hle/ffmpeg_dl.h
deleted file mode 100644
index 6ca2066fc..000000000
--- a/src/audio_core/hle/ffmpeg_dl.h
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2018 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-#ifdef _WIN32
-#include <windows.h>
-#endif // _WIN32
-
-extern "C" {
-#include <libavcodec/avcodec.h>
-}
-
-#ifdef _WIN32
-
-template <typename T>
-struct FuncDL {
-    FuncDL() = default;
-    FuncDL(HMODULE dll, const char* name) {
-        if (dll) {
-            ptr_function = reinterpret_cast<T*>(GetProcAddress(dll, name));
-        }
-    }
-
-    operator T*() const {
-        return ptr_function;
-    }
-
-    explicit operator bool() const {
-        return ptr_function != nullptr;
-    }
-
-    T* ptr_function = nullptr;
-};
-
-extern FuncDL<int(AVSampleFormat)> av_get_bytes_per_sample_dl;
-extern FuncDL<AVFrame*(void)> av_frame_alloc_dl;
-extern FuncDL<void(AVFrame**)> av_frame_free_dl;
-extern FuncDL<AVCodecContext*(const AVCodec*)> avcodec_alloc_context3_dl;
-extern FuncDL<void(AVCodecContext**)> avcodec_free_context_dl;
-extern FuncDL<int(AVCodecContext*, const AVCodec*, AVDictionary**)> avcodec_open2_dl;
-extern FuncDL<AVPacket*(void)> av_packet_alloc_dl;
-extern FuncDL<void(AVPacket**)> av_packet_free_dl;
-extern FuncDL<AVCodec*(AVCodecID)> avcodec_find_decoder_dl;
-extern FuncDL<int(AVCodecContext*, const AVPacket*)> avcodec_send_packet_dl;
-extern FuncDL<int(AVCodecContext*, AVFrame*)> avcodec_receive_frame_dl;
-extern FuncDL<AVCodecParserContext*(int)> av_parser_init_dl;
-extern FuncDL<int(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*, const uint8_t*, int,
-                  int64_t, int64_t, int64_t)>
-    av_parser_parse2_dl;
-extern FuncDL<void(AVCodecParserContext*)> av_parser_close_dl;
-
-bool InitFFmpegDL();
-
-#else // _Win32
-
-// No dynamic loading for Unix and Apple
-
-const auto av_get_bytes_per_sample_dl = &av_get_bytes_per_sample;
-const auto av_frame_alloc_dl = &av_frame_alloc;
-const auto av_frame_free_dl = &av_frame_free;
-const auto avcodec_alloc_context3_dl = &avcodec_alloc_context3;
-const auto avcodec_free_context_dl = &avcodec_free_context;
-const auto avcodec_open2_dl = &avcodec_open2;
-const auto av_packet_alloc_dl = &av_packet_alloc;
-const auto av_packet_free_dl = &av_packet_free;
-const auto avcodec_find_decoder_dl = &avcodec_find_decoder;
-const auto avcodec_send_packet_dl = &avcodec_send_packet;
-const auto avcodec_receive_frame_dl = &avcodec_receive_frame;
-const auto av_parser_init_dl = &av_parser_init;
-const auto av_parser_parse2_dl = &av_parser_parse2;
-const auto av_parser_close_dl = &av_parser_close;
-
-bool InitFFmpegDL() {
-    return true;
-}
-
-#endif // _Win32
diff --git a/src/audio_core/hle/hle.cpp b/src/audio_core/hle/hle.cpp
index d7e70f328..de42c13bc 100644
--- a/src/audio_core/hle/hle.cpp
+++ b/src/audio_core/hle/hle.cpp
@@ -12,15 +12,13 @@
 #include "audio_core/hle/wmf_decoder.h"
 #elif HAVE_AUDIOTOOLBOX
 #include "audio_core/hle/audiotoolbox_decoder.h"
-#elif HAVE_FFMPEG
-#include "audio_core/hle/ffmpeg_decoder.h"
 #elif ANDROID
 #include "audio_core/hle/mediandk_decoder.h"
-#elif HAVE_FDK
-#include "audio_core/hle/fdk_decoder.h"
 #endif
 #include "audio_core/hle/common.h"
 #include "audio_core/hle/decoder.h"
+#include "audio_core/hle/fdk_decoder.h"
+#include "audio_core/hle/ffmpeg_decoder.h"
 #include "audio_core/hle/hle.h"
 #include "audio_core/hle/mixers.h"
 #include "audio_core/hle/shared_memory.h"
@@ -120,6 +118,31 @@ private:
     friend class boost::serialization::access;
 };
 
+static std::vector<std::function<std::unique_ptr<HLE::DecoderBase>(Memory::MemorySystem&)>>
+    decoder_backends = {
+#if defined(HAVE_MF)
+        [](Memory::MemorySystem& memory) -> std::unique_ptr<HLE::DecoderBase> {
+            return std::make_unique<HLE::WMFDecoder>(memory);
+        },
+#endif
+#if defined(HAVE_AUDIOTOOLBOX)
+        [](Memory::MemorySystem& memory) -> std::unique_ptr<HLE::DecoderBase> {
+            return std::make_unique<HLE::AudioToolboxDecoder>(memory);
+        },
+#endif
+#if ANDROID
+        [](Memory::MemorySystem& memory) -> std::unique_ptr<HLE::DecoderBase> {
+            return std::make_unique<HLE::MediaNDKDecoder>(memory);
+        },
+#endif
+        [](Memory::MemorySystem& memory) -> std::unique_ptr<HLE::DecoderBase> {
+            return std::make_unique<HLE::FDKDecoder>(memory);
+        },
+        [](Memory::MemorySystem& memory) -> std::unique_ptr<HLE::DecoderBase> {
+            return std::make_unique<HLE::FFMPEGDecoder>(memory);
+        },
+};
+
 DspHle::Impl::Impl(DspHle& parent_, Memory::MemorySystem& memory, Core::Timing& timing)
     : parent(parent_), core_timing(timing) {
     dsp_memory.raw_memory.fill(0);
@@ -128,28 +151,14 @@ DspHle::Impl::Impl(DspHle& parent_, Memory::MemorySystem& memory, Core::Timing&
         source.SetMemory(memory);
     }
 
-#if defined(HAVE_MF) && defined(HAVE_FFMPEG)
-    decoder = std::make_unique<HLE::WMFDecoder>(memory);
-    if (!decoder->IsValid()) {
-        LOG_WARNING(Audio_DSP, "Unable to load MediaFoundation. Attempting to load FFMPEG instead");
-        decoder = std::make_unique<HLE::FFMPEGDecoder>(memory);
+    for (auto& factory : decoder_backends) {
+        decoder = factory(memory);
+        if (decoder && decoder->IsValid()) {
+            break;
+        }
     }
-#elif defined(HAVE_MF)
-    decoder = std::make_unique<HLE::WMFDecoder>(memory);
-#elif defined(HAVE_AUDIOTOOLBOX)
-    decoder = std::make_unique<HLE::AudioToolboxDecoder>(memory);
-#elif defined(HAVE_FFMPEG)
-    decoder = std::make_unique<HLE::FFMPEGDecoder>(memory);
-#elif ANDROID
-    decoder = std::make_unique<HLE::MediaNDKDecoder>(memory);
-#elif defined(HAVE_FDK)
-    decoder = std::make_unique<HLE::FDKDecoder>(memory);
-#else
-    LOG_WARNING(Audio_DSP, "No decoder found, this could lead to missing audio");
-    decoder = std::make_unique<HLE::NullDecoder>();
-#endif // HAVE_MF
 
-    if (!decoder->IsValid()) {
+    if (!decoder || !decoder->IsValid()) {
         LOG_WARNING(Audio_DSP,
                     "Unable to load any decoders, this could cause missing audio in some games");
         decoder = std::make_unique<HLE::NullDecoder>();
diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index b2b447a03..7445c1d07 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -27,6 +27,7 @@
 #include "common/string_util.h"
 #include "core/core.h"
 #include "core/dumping/backend.h"
+#include "core/dumping/ffmpeg_backend.h"
 #include "core/file_sys/cia_container.h"
 #include "core/frontend/applets/default_applets.h"
 #include "core/frontend/framebuffer_layout.h"
@@ -445,10 +446,13 @@ int main(int argc, char** argv) {
     if (!movie_record.empty()) {
         Core::Movie::GetInstance().StartRecording(movie_record, movie_record_author);
     }
-    if (!dump_video.empty()) {
+    if (!dump_video.empty() && DynamicLibrary::FFmpeg::LoadFFmpeg()) {
         Layout::FramebufferLayout layout{Layout::FrameLayoutFromResolutionScale(
             VideoCore::g_renderer->GetResolutionScaleFactor())};
-        system.VideoDumper().StartDumping(dump_video, layout);
+        auto dumper = std::make_shared<VideoDumper::FFmpegBackend>();
+        if (dumper->StartDumping(dump_video, layout)) {
+            Core::System::GetInstance().RegisterVideoDumper(dumper);
+        }
     }
 
     std::thread main_render_thread([&emu_window] { emu_window->Present(); });
@@ -491,8 +495,10 @@ int main(int argc, char** argv) {
     secondary_render_thread.join();
 
     Core::Movie::GetInstance().Shutdown();
-    if (system.VideoDumper().IsDumping()) {
-        system.VideoDumper().StopDumping();
+
+    auto video_dumper = system.GetVideoDumper();
+    if (video_dumper && video_dumper->IsDumping()) {
+        video_dumper->StopDumping();
     }
 
     Network::Shutdown();
diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index 18802c957..04d113abb 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -120,6 +120,15 @@ add_executable(citra-qt
     debugger/wait_tree.cpp
     debugger/wait_tree.h
     discord.h
+    dumping/dumping_dialog.cpp
+    dumping/dumping_dialog.h
+    dumping/dumping_dialog.ui
+    dumping/option_set_dialog.cpp
+    dumping/option_set_dialog.h
+    dumping/option_set_dialog.ui
+    dumping/options_dialog.cpp
+    dumping/options_dialog.h
+    dumping/options_dialog.ui
     game_list.cpp
     game_list.h
     game_list_p.h
@@ -178,20 +187,6 @@ add_executable(citra-qt
     util/util.h
 )
 
-if (ENABLE_FFMPEG_VIDEO_DUMPER)
-    target_sources(citra-qt PRIVATE
-        dumping/dumping_dialog.cpp
-        dumping/dumping_dialog.h
-        dumping/dumping_dialog.ui
-        dumping/option_set_dialog.cpp
-        dumping/option_set_dialog.h
-        dumping/option_set_dialog.ui
-        dumping/options_dialog.cpp
-        dumping/options_dialog.h
-        dumping/options_dialog.ui
-    )
-endif()
-
 file(GLOB COMPAT_LIST
      ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc
      ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json)
@@ -377,11 +372,6 @@ if (MSVC)
         include(CopyCitraOpensslDeps)
         copy_citra_openssl_deps(citra-qt)
     endif()
-
-    if (ENABLE_FFMPEG)
-        include(CopyCitraFFmpegDeps)
-        copy_citra_FFmpeg_deps(citra-qt)
-    endif()
 endif()
 
 if (NOT APPLE)
diff --git a/src/citra_qt/dumping/option_set_dialog.cpp b/src/citra_qt/dumping/option_set_dialog.cpp
index 84fd619f9..f6d6e4af7 100644
--- a/src/citra_qt/dumping/option_set_dialog.cpp
+++ b/src/citra_qt/dumping/option_set_dialog.cpp
@@ -10,10 +10,6 @@
 #include "common/string_util.h"
 #include "ui_option_set_dialog.h"
 
-extern "C" {
-#include <libavutil/pixdesc.h>
-}
-
 static const std::unordered_map<AVOptionType, const char*> TypeNameMap{{
     {AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")},
     {AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")},
@@ -56,21 +52,14 @@ std::vector<std::pair<QString, QString>> GetPresetValues(const VideoDumper::Opti
     }
     case AV_OPT_TYPE_PIXEL_FMT: {
         std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
-        // List all pixel formats
-        const AVPixFmtDescriptor* current = nullptr;
-        while ((current = av_pix_fmt_desc_next(current))) {
-            out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name));
+        for (const auto& name : VideoDumper::GetPixelFormats()) {
+            out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name));
         }
         return out;
     }
     case AV_OPT_TYPE_SAMPLE_FMT: {
         std::vector<std::pair<QString, QString>> out{{QObject::tr("none"), QStringLiteral("none")}};
-        // List all sample formats
-        int current = 0;
-        while (true) {
-            const char* name = av_get_sample_fmt_name(static_cast<AVSampleFormat>(current));
-            if (name == nullptr)
-                break;
+        for (const auto& name : VideoDumper::GetSampleFormats()) {
             out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name));
         }
         return out;
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 5b218c6d4..ad53376ad 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -49,6 +49,7 @@
 #include "citra_qt/debugger/registers.h"
 #include "citra_qt/debugger/wait_tree.h"
 #include "citra_qt/discord.h"
+#include "citra_qt/dumping/dumping_dialog.h"
 #include "citra_qt/game_list.h"
 #include "citra_qt/hotkeys.h"
 #include "citra_qt/loading_screen.h"
@@ -102,10 +103,6 @@
 #include "citra_qt/discord_impl.h"
 #endif
 
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-#include "citra_qt/dumping/dumping_dialog.h"
-#endif
-
 #ifdef QT_STATICPLUGIN
 Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
 #endif
@@ -834,18 +831,7 @@ void GMainWindow::ConnectMenuEvents() {
         }
     });
     connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot);
-
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-    connect_menu(ui->action_Dump_Video, [this] {
-        if (ui->action_Dump_Video->isChecked()) {
-            OnStartVideoDumping();
-        } else {
-            OnStopVideoDumping();
-        }
-    });
-#else
-    ui->action_Dump_Video->setEnabled(false);
-#endif
+    connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo);
 
     // Help
     connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder);
@@ -1203,15 +1189,7 @@ void GMainWindow::BootGame(const QString& filename) {
     }
 
     if (video_dumping_on_start) {
-        Layout::FramebufferLayout layout{Layout::FrameLayoutFromResolutionScale(
-            VideoCore::g_renderer->GetResolutionScaleFactor())};
-        if (!system.VideoDumper().StartDumping(video_dumping_path.toStdString(), layout)) {
-
-            QMessageBox::critical(
-                this, tr("Citra"),
-                tr("Could not start video dumping.<br>Refer to the log for details."));
-            ui->action_Dump_Video->setChecked(false);
-        }
+        StartVideoDumping(video_dumping_path);
         video_dumping_on_start = false;
         video_dumping_path.clear();
     }
@@ -1279,13 +1257,12 @@ void GMainWindow::ShutdownGame() {
         HideFullscreen();
     }
 
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-    if (system.VideoDumper().IsDumping()) {
+    auto video_dumper = system.GetVideoDumper();
+    if (video_dumper && video_dumper->IsDumping()) {
         game_shutdown_delayed = true;
         OnStopVideoDumping();
         return;
     }
-#endif
 
     AllowOSSleep();
 
@@ -2215,7 +2192,97 @@ void GMainWindow::OnCaptureScreenshot() {
     OnStartGame();
 }
 
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
+void GMainWindow::OnDumpVideo() {
+    if (DynamicLibrary::FFmpeg::LoadFFmpeg()) {
+        if (ui->action_Dump_Video->isChecked()) {
+            OnStartVideoDumping();
+        } else {
+            OnStopVideoDumping();
+        }
+    } else {
+        ui->action_Dump_Video->setChecked(false);
+
+        QMessageBox message_box;
+        message_box.setWindowTitle(tr("Could not load video dumper"));
+        message_box.setText(
+            tr("FFmpeg could not be loaded. Make sure you have a compatible version installed."
+#ifdef _WIN32
+               "\n\nTo install FFmpeg to Citra, press Open and select your FFmpeg directory."
+#endif
+               "\n\nTo view a guide on how to install FFmpeg, press Help."));
+        message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help
+#ifdef _WIN32
+                                       | QMessageBox::Open
+#endif
+        );
+        auto result = message_box.exec();
+        if (result == QMessageBox::Help) {
+            QDesktopServices::openUrl(QUrl(QStringLiteral(
+                "https://citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/")));
+#ifdef _WIN32
+        } else if (result == QMessageBox::Open) {
+            OnOpenFFmpeg();
+#endif
+        }
+    }
+}
+
+#ifdef _WIN32
+void GMainWindow::OnOpenFFmpeg() {
+    auto filename =
+        QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString();
+    if (filename.empty()) {
+        return;
+    }
+    // Check for a bin directory if they chose the FFmpeg root directory.
+    auto bin_dir = filename + DIR_SEP + "bin";
+    if (!FileUtil::Exists(bin_dir)) {
+        // Otherwise, assume the user directly selected the directory containing the DLLs.
+        bin_dir = filename;
+    }
+
+    static const std::array library_names = {
+        DynamicLibrary::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR),
+        DynamicLibrary::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR),
+        DynamicLibrary::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR),
+        DynamicLibrary::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR),
+        DynamicLibrary::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR),
+    };
+
+    for (auto& library_name : library_names) {
+        if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) {
+            QMessageBox::critical(this, tr("Citra"),
+                                  tr("The provided FFmpeg directory is missing %1. Please make "
+                                     "sure the correct directory was selected.")
+                                      .arg(QString::fromStdString(library_name)));
+            return;
+        }
+    }
+
+    std::atomic<bool> success(true);
+    auto process_file = [&success](u64* num_entries_out, const std::string& directory,
+                                   const std::string& virtual_name) -> bool {
+        auto file_path = directory + DIR_SEP + virtual_name;
+        if (file_path.ends_with(".dll")) {
+            auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name;
+            if (!FileUtil::Copy(file_path, destination_path)) {
+                success.store(false);
+                return false;
+            }
+        }
+        return true;
+    };
+    FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file);
+
+    if (success.load()) {
+        QMessageBox::information(this, tr("Citra"), tr("FFmpeg has been sucessfully installed."));
+    } else {
+        QMessageBox::critical(this, tr("Citra"),
+                              tr("Installation of FFmpeg failed. Check the log file for details."));
+    }
+}
+#endif
+
 void GMainWindow::OnStartVideoDumping() {
     DumpingDialog dialog(this);
     if (dialog.exec() != QDialog::DialogCode::Accepted) {
@@ -2224,20 +2291,28 @@ void GMainWindow::OnStartVideoDumping() {
     }
     const auto path = dialog.GetFilePath();
     if (emulation_running) {
-        Layout::FramebufferLayout layout{Layout::FrameLayoutFromResolutionScale(
-            VideoCore::g_renderer->GetResolutionScaleFactor())};
-        if (!system.VideoDumper().StartDumping(path.toStdString(), layout)) {
-            QMessageBox::critical(
-                this, tr("Citra"),
-                tr("Could not start video dumping.<br>Refer to the log for details."));
-            ui->action_Dump_Video->setChecked(false);
-        }
+        StartVideoDumping(path);
     } else {
         video_dumping_on_start = true;
         video_dumping_path = path;
     }
 }
 
+void GMainWindow::StartVideoDumping(const QString& path) {
+    Layout::FramebufferLayout layout{
+        Layout::FrameLayoutFromResolutionScale(VideoCore::g_renderer->GetResolutionScaleFactor())};
+
+    auto dumper = std::make_shared<VideoDumper::FFmpegBackend>();
+    if (dumper->StartDumping(path.toStdString(), layout)) {
+        system.RegisterVideoDumper(dumper);
+    } else {
+        QMessageBox::critical(
+            this, tr("Citra"),
+            tr("Could not start video dumping.<br>Refer to the log for details."));
+        ui->action_Dump_Video->setChecked(false);
+    }
+}
+
 void GMainWindow::OnStopVideoDumping() {
     ui->action_Dump_Video->setChecked(false);
 
@@ -2245,14 +2320,15 @@ void GMainWindow::OnStopVideoDumping() {
         video_dumping_on_start = false;
         video_dumping_path.clear();
     } else {
-        const bool was_dumping = system.VideoDumper().IsDumping();
-        if (!was_dumping)
+        auto dumper = system.GetVideoDumper();
+        if (!dumper || !dumper->IsDumping()) {
             return;
+        }
 
         game_paused_for_dumping = emu_thread->IsRunning();
         OnPauseGame();
 
-        auto future = QtConcurrent::run([this] { system.VideoDumper().StopDumping(); });
+        auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); });
         auto* future_watcher = new QFutureWatcher<void>(this);
         connect(future_watcher, &QFutureWatcher<void>::finished, this, [this] {
             if (game_shutdown_delayed) {
@@ -2266,7 +2342,6 @@ void GMainWindow::OnStopVideoDumping() {
         future_watcher->setFuture(future);
     }
 }
-#endif
 
 void GMainWindow::UpdateStatusBar() {
     if (!emu_thread) [[unlikely]] {
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 03b9f54a7..d3c4d03dc 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -242,10 +242,13 @@ private slots:
     void OnCloseMovie();
     void OnSaveMovie();
     void OnCaptureScreenshot();
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-    void OnStartVideoDumping();
-    void OnStopVideoDumping();
+    void OnDumpVideo();
+#ifdef _WIN32
+    void OnOpenFFmpeg();
 #endif
+    void OnStartVideoDumping();
+    void StartVideoDumping(const QString& path);
+    void OnStopVideoDumping();
     void OnCoreError(Core::System::ResultStatus, std::string);
     /// Called whenever a user selects Help->About Citra
     void OnMenuAboutCitra();
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 9efb1eac1..7c3e3f841 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -65,6 +65,12 @@ add_library(citra_common STATIC
     common_precompiled_headers.h
     common_types.h
     construct.h
+    dynamic_library/dynamic_library.cpp
+    dynamic_library/dynamic_library.h
+    dynamic_library/fdk-aac.cpp
+    dynamic_library/fdk-aac.h
+    dynamic_library/ffmpeg.cpp
+    dynamic_library/ffmpeg.h
     error.cpp
     error.h
     file_util.cpp
@@ -153,7 +159,7 @@ endif()
 
 create_target_directory_groups(citra_common)
 
-target_link_libraries(citra_common PUBLIC fmt::fmt microprofile Boost::boost Boost::serialization Boost::iostreams)
+target_link_libraries(citra_common PUBLIC fmt::fmt library-headers microprofile Boost::boost Boost::serialization Boost::iostreams)
 target_link_libraries(citra_common PRIVATE libzstd_static)
 set_target_properties(citra_common PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO})
 
diff --git a/src/common/dynamic_library/dynamic_library.cpp b/src/common/dynamic_library/dynamic_library.cpp
new file mode 100644
index 000000000..b90cb1424
--- /dev/null
+++ b/src/common/dynamic_library/dynamic_library.cpp
@@ -0,0 +1,87 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <fmt/format.h>
+#if defined(_WIN32)
+#include <windows.h>
+#else
+#include <dlfcn.h>
+#endif
+#include "dynamic_library.h"
+
+namespace DynamicLibrary {
+
+DynamicLibrary::DynamicLibrary(std::string_view name, int major, int minor) {
+    auto full_name = GetLibraryName(name, major, minor);
+#if defined(_WIN32)
+    handle = reinterpret_cast<void*>(LoadLibraryA(full_name.c_str()));
+    if (!handle) {
+        DWORD error_message_id = GetLastError();
+        LPSTR message_buffer = nullptr;
+        size_t size =
+            FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
+                               FORMAT_MESSAGE_IGNORE_INSERTS,
+                           nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+                           reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
+        std::string message(message_buffer, size);
+        load_error = message;
+    }
+#else
+    handle = dlopen(full_name.c_str(), RTLD_LAZY);
+    if (!handle) {
+        load_error = dlerror();
+    }
+#endif // defined(_WIN32)
+}
+
+DynamicLibrary::~DynamicLibrary() {
+    if (handle) {
+#if defined(_WIN32)
+        FreeLibrary(reinterpret_cast<HMODULE>(handle));
+#else
+        dlclose(handle);
+#endif // defined(_WIN32)
+        handle = nullptr;
+    }
+}
+
+void* DynamicLibrary::GetRawSymbol(std::string_view name) {
+#if defined(_WIN32)
+    return reinterpret_cast<void*>(GetProcAddress(reinterpret_cast<HMODULE>(handle), name.data()));
+#else
+    return dlsym(handle, name.data());
+#endif // defined(_WIN32)
+}
+
+std::string DynamicLibrary::GetLibraryName(std::string_view name, int major, int minor) {
+#if defined(_WIN32)
+    if (major >= 0 && minor >= 0) {
+        return fmt::format("{}-{}-{}.dll", name, major, minor);
+    } else if (major >= 0) {
+        return fmt::format("{}-{}.dll", name, major);
+    } else {
+        return fmt::format("{}.dll", name);
+    }
+#elif defined(__APPLE__)
+    auto prefix = name.starts_with("lib") ? "" : "lib";
+    if (major >= 0 && minor >= 0) {
+        return fmt::format("{}{}.{}.{}.dylib", prefix, name, major, minor);
+    } else if (major >= 0) {
+        return fmt::format("{}{}.{}.dylib", prefix, name, major);
+    } else {
+        return fmt::format("{}{}.dylib", prefix, name);
+    }
+#else
+    auto prefix = name.starts_with("lib") ? "" : "lib";
+    if (major >= 0 && minor >= 0) {
+        return fmt::format("{}{}.so.{}.{}", prefix, name, major, minor);
+    } else if (major >= 0) {
+        return fmt::format("{}{}.so.{}", prefix, name, major);
+    } else {
+        return fmt::format("{}{}.so", prefix, name);
+    }
+#endif
+}
+
+} // namespace DynamicLibrary
diff --git a/src/common/dynamic_library/dynamic_library.h b/src/common/dynamic_library/dynamic_library.h
new file mode 100644
index 000000000..9846c8326
--- /dev/null
+++ b/src/common/dynamic_library/dynamic_library.h
@@ -0,0 +1,39 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <string>
+#include "common/common_types.h"
+
+namespace DynamicLibrary {
+
+class DynamicLibrary {
+public:
+    explicit DynamicLibrary(std::string_view name, int major = -1, int minor = -1);
+    ~DynamicLibrary();
+
+    bool IsLoaded() {
+        return handle != nullptr;
+    }
+
+    std::string_view GetLoadError() {
+        return load_error;
+    }
+
+    template <typename T>
+    T GetSymbol(std::string_view name) {
+        return reinterpret_cast<T>(GetRawSymbol(name));
+    }
+
+    static std::string GetLibraryName(std::string_view name, int major = -1, int minor = -1);
+
+private:
+    void* GetRawSymbol(std::string_view name);
+
+    void* handle;
+    std::string load_error;
+};
+
+} // namespace DynamicLibrary
diff --git a/src/common/dynamic_library/fdk-aac.cpp b/src/common/dynamic_library/fdk-aac.cpp
new file mode 100644
index 000000000..d4b0bcd62
--- /dev/null
+++ b/src/common/dynamic_library/fdk-aac.cpp
@@ -0,0 +1,54 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/dynamic_library/fdk-aac.h"
+#include "common/logging/log.h"
+
+namespace DynamicLibrary::FdkAac {
+
+aacDecoder_GetLibInfo_func aacDecoder_GetLibInfo;
+aacDecoder_Open_func aacDecoder_Open;
+aacDecoder_Close_func aacDecoder_Close;
+aacDecoder_SetParam_func aacDecoder_SetParam;
+aacDecoder_GetStreamInfo_func aacDecoder_GetStreamInfo;
+aacDecoder_DecodeFrame_func aacDecoder_DecodeFrame;
+aacDecoder_Fill_func aacDecoder_Fill;
+
+static std::unique_ptr<DynamicLibrary> fdk_aac;
+
+#define LOAD_SYMBOL(library, name)                                                                 \
+    any_failed = any_failed || (name = library->GetSymbol<name##_func>(#name)) == nullptr
+
+bool LoadFdkAac() {
+    if (fdk_aac) {
+        return true;
+    }
+
+    fdk_aac = std::make_unique<DynamicLibrary>("fdk-aac", 2);
+    if (!fdk_aac->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libfdk-aac: {}", fdk_aac->GetLoadError());
+        fdk_aac.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+    LOAD_SYMBOL(fdk_aac, aacDecoder_GetLibInfo);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_Open);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_Close);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_SetParam);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_GetStreamInfo);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_DecodeFrame);
+    LOAD_SYMBOL(fdk_aac, aacDecoder_Fill);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libfdk-aac.");
+        fdk_aac.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libfdk-aac.");
+    return true;
+}
+
+} // namespace DynamicLibrary::FdkAac
diff --git a/src/common/dynamic_library/fdk-aac.h b/src/common/dynamic_library/fdk-aac.h
new file mode 100644
index 000000000..4c1dca4b8
--- /dev/null
+++ b/src/common/dynamic_library/fdk-aac.h
@@ -0,0 +1,37 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+extern "C" {
+#include <fdk-aac/aacdecoder_lib.h>
+}
+
+#include "common/common_types.h"
+#include "common/dynamic_library/dynamic_library.h"
+
+namespace DynamicLibrary::FdkAac {
+
+typedef INT (*aacDecoder_GetLibInfo_func)(LIB_INFO* info);
+typedef HANDLE_AACDECODER (*aacDecoder_Open_func)(TRANSPORT_TYPE transportFmt, UINT nrOfLayers);
+typedef void (*aacDecoder_Close_func)(HANDLE_AACDECODER self);
+typedef AAC_DECODER_ERROR (*aacDecoder_SetParam_func)(const HANDLE_AACDECODER self,
+                                                      const AACDEC_PARAM param, const INT value);
+typedef CStreamInfo* (*aacDecoder_GetStreamInfo_func)(HANDLE_AACDECODER self);
+typedef AAC_DECODER_ERROR (*aacDecoder_DecodeFrame_func)(HANDLE_AACDECODER self, INT_PCM* pTimeData,
+                                                         const INT timeDataSize, const UINT flags);
+typedef AAC_DECODER_ERROR (*aacDecoder_Fill_func)(HANDLE_AACDECODER self, UCHAR* pBuffer[],
+                                                  const UINT bufferSize[], UINT* bytesValid);
+
+extern aacDecoder_GetLibInfo_func aacDecoder_GetLibInfo;
+extern aacDecoder_Open_func aacDecoder_Open;
+extern aacDecoder_Close_func aacDecoder_Close;
+extern aacDecoder_SetParam_func aacDecoder_SetParam;
+extern aacDecoder_GetStreamInfo_func aacDecoder_GetStreamInfo;
+extern aacDecoder_DecodeFrame_func aacDecoder_DecodeFrame;
+extern aacDecoder_Fill_func aacDecoder_Fill;
+
+bool LoadFdkAac();
+
+} // namespace DynamicLibrary::FdkAac
diff --git a/src/common/dynamic_library/ffmpeg.cpp b/src/common/dynamic_library/ffmpeg.cpp
new file mode 100644
index 000000000..0a1f0a280
--- /dev/null
+++ b/src/common/dynamic_library/ffmpeg.cpp
@@ -0,0 +1,390 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/dynamic_library/ffmpeg.h"
+#include "common/logging/log.h"
+
+namespace DynamicLibrary::FFmpeg {
+
+// avutil
+av_buffer_ref_func av_buffer_ref;
+av_buffer_unref_func av_buffer_unref;
+av_d2q_func av_d2q;
+av_dict_count_func av_dict_count;
+av_dict_get_func av_dict_get;
+av_dict_get_string_func av_dict_get_string;
+av_dict_set_func av_dict_set;
+av_frame_alloc_func av_frame_alloc;
+av_frame_free_func av_frame_free;
+av_frame_unref_func av_frame_unref;
+av_freep_func av_freep;
+av_get_bytes_per_sample_func av_get_bytes_per_sample;
+av_get_pix_fmt_func av_get_pix_fmt;
+av_get_pix_fmt_name_func av_get_pix_fmt_name;
+av_get_sample_fmt_name_func av_get_sample_fmt_name;
+av_hwdevice_ctx_create_func av_hwdevice_ctx_create;
+av_hwdevice_get_hwframe_constraints_func av_hwdevice_get_hwframe_constraints;
+av_hwframe_constraints_free_func av_hwframe_constraints_free;
+av_hwframe_ctx_alloc_func av_hwframe_ctx_alloc;
+av_hwframe_ctx_init_func av_hwframe_ctx_init;
+av_hwframe_get_buffer_func av_hwframe_get_buffer;
+av_hwframe_transfer_data_func av_hwframe_transfer_data;
+av_int_list_length_for_size_func av_int_list_length_for_size;
+#if LIBAVCODEC_VERSION_MAJOR >= 59
+av_opt_child_class_iterate_func av_opt_child_class_iterate;
+#else
+av_opt_child_class_next_func av_opt_child_class_next;
+#endif
+av_opt_next_func av_opt_next;
+av_opt_set_bin_func av_opt_set_bin;
+av_pix_fmt_desc_get_func av_pix_fmt_desc_get;
+av_pix_fmt_desc_next_func av_pix_fmt_desc_next;
+av_sample_fmt_is_planar_func av_sample_fmt_is_planar;
+av_samples_alloc_array_and_samples_func av_samples_alloc_array_and_samples;
+av_strdup_func av_strdup;
+avutil_version_func avutil_version;
+
+// avcodec
+av_codec_is_encoder_func av_codec_is_encoder;
+av_codec_iterate_func av_codec_iterate;
+av_init_packet_func av_init_packet;
+av_packet_alloc_func av_packet_alloc;
+av_packet_free_func av_packet_free;
+av_packet_rescale_ts_func av_packet_rescale_ts;
+av_parser_close_func av_parser_close;
+av_parser_init_func av_parser_init;
+av_parser_parse2_func av_parser_parse2;
+avcodec_alloc_context3_func avcodec_alloc_context3;
+avcodec_descriptor_next_func avcodec_descriptor_next;
+avcodec_find_decoder_func avcodec_find_decoder;
+avcodec_find_encoder_by_name_func avcodec_find_encoder_by_name;
+avcodec_free_context_func avcodec_free_context;
+avcodec_get_class_func avcodec_get_class;
+avcodec_get_hw_config_func avcodec_get_hw_config;
+avcodec_open2_func avcodec_open2;
+avcodec_parameters_from_context_func avcodec_parameters_from_context;
+avcodec_receive_frame_func avcodec_receive_frame;
+avcodec_receive_packet_func avcodec_receive_packet;
+avcodec_send_frame_func avcodec_send_frame;
+avcodec_send_packet_func avcodec_send_packet;
+avcodec_version_func avcodec_version;
+
+// avfilter
+av_buffersink_get_frame_func av_buffersink_get_frame;
+av_buffersrc_add_frame_func av_buffersrc_add_frame;
+avfilter_get_by_name_func avfilter_get_by_name;
+avfilter_graph_alloc_func avfilter_graph_alloc;
+avfilter_graph_config_func avfilter_graph_config;
+avfilter_graph_create_filter_func avfilter_graph_create_filter;
+avfilter_graph_free_func avfilter_graph_free;
+avfilter_graph_parse_ptr_func avfilter_graph_parse_ptr;
+avfilter_inout_alloc_func avfilter_inout_alloc;
+avfilter_inout_free_func avfilter_inout_free;
+avfilter_version_func avfilter_version;
+
+// avformat
+av_guess_format_func av_guess_format;
+av_interleaved_write_frame_func av_interleaved_write_frame;
+av_muxer_iterate_func av_muxer_iterate;
+av_write_trailer_func av_write_trailer;
+avformat_alloc_output_context2_func avformat_alloc_output_context2;
+avformat_free_context_func avformat_free_context;
+avformat_get_class_func avformat_get_class;
+avformat_network_init_func avformat_network_init;
+avformat_new_stream_func avformat_new_stream;
+avformat_query_codec_func avformat_query_codec;
+avformat_write_header_func avformat_write_header;
+avformat_version_func avformat_version;
+avio_closep_func avio_closep;
+avio_open_func avio_open;
+
+// swresample
+#if LIBSWRESAMPLE_VERSION_INT >= AV_VERSION_INT(4, 5, 100)
+swr_alloc_set_opts2_func swr_alloc_set_opts2;
+#else
+swr_alloc_set_opts_func swr_alloc_set_opts;
+#endif
+swr_convert_func swr_convert;
+swr_free_func swr_free;
+swr_init_func swr_init;
+swresample_version_func swresample_version;
+
+static std::unique_ptr<DynamicLibrary> avutil;
+static std::unique_ptr<DynamicLibrary> avcodec;
+static std::unique_ptr<DynamicLibrary> avfilter;
+static std::unique_ptr<DynamicLibrary> avformat;
+static std::unique_ptr<DynamicLibrary> swresample;
+
+#define LOAD_SYMBOL(library, name)                                                                 \
+    any_failed = any_failed || (name = library->GetSymbol<name##_func>(#name)) == nullptr
+
+static bool LoadAVUtil() {
+    if (avutil) {
+        return true;
+    }
+
+    avutil = std::make_unique<DynamicLibrary>("avutil", LIBAVUTIL_VERSION_MAJOR);
+    if (!avutil->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libavutil: {}", avutil->GetLoadError());
+        avutil.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+
+    LOAD_SYMBOL(avutil, avutil_version);
+
+    auto major_version = AV_VERSION_MAJOR(avutil_version());
+    if (major_version != LIBAVUTIL_VERSION_MAJOR) {
+        LOG_WARNING(Common, "libavutil version {} does not match supported version {}.",
+                    major_version, LIBAVUTIL_VERSION_MAJOR);
+        avutil.reset();
+        return false;
+    }
+
+    LOAD_SYMBOL(avutil, av_buffer_ref);
+    LOAD_SYMBOL(avutil, av_buffer_unref);
+    LOAD_SYMBOL(avutil, av_d2q);
+    LOAD_SYMBOL(avutil, av_dict_count);
+    LOAD_SYMBOL(avutil, av_dict_get);
+    LOAD_SYMBOL(avutil, av_dict_get_string);
+    LOAD_SYMBOL(avutil, av_dict_set);
+    LOAD_SYMBOL(avutil, av_frame_alloc);
+    LOAD_SYMBOL(avutil, av_frame_free);
+    LOAD_SYMBOL(avutil, av_frame_unref);
+    LOAD_SYMBOL(avutil, av_freep);
+    LOAD_SYMBOL(avutil, av_get_bytes_per_sample);
+    LOAD_SYMBOL(avutil, av_get_pix_fmt);
+    LOAD_SYMBOL(avutil, av_get_pix_fmt_name);
+    LOAD_SYMBOL(avutil, av_get_sample_fmt_name);
+    LOAD_SYMBOL(avutil, av_hwdevice_ctx_create);
+    LOAD_SYMBOL(avutil, av_hwdevice_get_hwframe_constraints);
+    LOAD_SYMBOL(avutil, av_hwframe_constraints_free);
+    LOAD_SYMBOL(avutil, av_hwframe_ctx_alloc);
+    LOAD_SYMBOL(avutil, av_hwframe_ctx_init);
+    LOAD_SYMBOL(avutil, av_hwframe_get_buffer);
+    LOAD_SYMBOL(avutil, av_hwframe_transfer_data);
+    LOAD_SYMBOL(avutil, av_int_list_length_for_size);
+#if LIBAVCODEC_VERSION_MAJOR >= 59
+    LOAD_SYMBOL(avutil, av_opt_child_class_iterate);
+#else
+    LOAD_SYMBOL(avutil, av_opt_child_class_next);
+#endif
+    LOAD_SYMBOL(avutil, av_opt_next);
+    LOAD_SYMBOL(avutil, av_opt_set_bin);
+    LOAD_SYMBOL(avutil, av_pix_fmt_desc_get);
+    LOAD_SYMBOL(avutil, av_pix_fmt_desc_next);
+    LOAD_SYMBOL(avutil, av_sample_fmt_is_planar);
+    LOAD_SYMBOL(avutil, av_samples_alloc_array_and_samples);
+    LOAD_SYMBOL(avutil, av_strdup);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libavutil.");
+        avutil.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libavutil.");
+    return true;
+}
+
+static bool LoadAVCodec() {
+    if (avcodec) {
+        return true;
+    }
+
+    avcodec = std::make_unique<DynamicLibrary>("avcodec", LIBAVCODEC_VERSION_MAJOR);
+    if (!avcodec->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libavcodec: {}", avcodec->GetLoadError());
+        avcodec.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+
+    LOAD_SYMBOL(avcodec, avcodec_version);
+
+    auto major_version = AV_VERSION_MAJOR(avcodec_version());
+    if (major_version != LIBAVCODEC_VERSION_MAJOR) {
+        LOG_WARNING(Common, "libavcodec version {} does not match supported version {}.",
+                    major_version, LIBAVCODEC_VERSION_MAJOR);
+        avcodec.reset();
+        return false;
+    }
+
+    LOAD_SYMBOL(avcodec, av_codec_is_encoder);
+    LOAD_SYMBOL(avcodec, av_codec_iterate);
+    LOAD_SYMBOL(avcodec, av_init_packet);
+    LOAD_SYMBOL(avcodec, av_packet_alloc);
+    LOAD_SYMBOL(avcodec, av_packet_free);
+    LOAD_SYMBOL(avcodec, av_packet_rescale_ts);
+    LOAD_SYMBOL(avcodec, av_parser_close);
+    LOAD_SYMBOL(avcodec, av_parser_init);
+    LOAD_SYMBOL(avcodec, av_parser_parse2);
+    LOAD_SYMBOL(avcodec, avcodec_alloc_context3);
+    LOAD_SYMBOL(avcodec, avcodec_descriptor_next);
+    LOAD_SYMBOL(avcodec, avcodec_find_decoder);
+    LOAD_SYMBOL(avcodec, avcodec_find_encoder_by_name);
+    LOAD_SYMBOL(avcodec, avcodec_free_context);
+    LOAD_SYMBOL(avcodec, avcodec_get_class);
+    LOAD_SYMBOL(avcodec, avcodec_get_hw_config);
+    LOAD_SYMBOL(avcodec, avcodec_open2);
+    LOAD_SYMBOL(avcodec, avcodec_parameters_from_context);
+    LOAD_SYMBOL(avcodec, avcodec_receive_frame);
+    LOAD_SYMBOL(avcodec, avcodec_receive_packet);
+    LOAD_SYMBOL(avcodec, avcodec_send_frame);
+    LOAD_SYMBOL(avcodec, avcodec_send_packet);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libavcodec.");
+        avcodec.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libavcodec.");
+    return true;
+}
+
+static bool LoadAVFilter() {
+    if (avfilter) {
+        return true;
+    }
+
+    avfilter = std::make_unique<DynamicLibrary>("avfilter", LIBAVFILTER_VERSION_MAJOR);
+    if (!avfilter->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libavfilter: {}", avfilter->GetLoadError());
+        avfilter.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+
+    LOAD_SYMBOL(avfilter, avfilter_version);
+
+    auto major_version = AV_VERSION_MAJOR(avfilter_version());
+    if (major_version != LIBAVFILTER_VERSION_MAJOR) {
+        LOG_WARNING(Common, "libavfilter version {} does not match supported version {}.",
+                    major_version, LIBAVFILTER_VERSION_MAJOR);
+        avfilter.reset();
+        return false;
+    }
+
+    LOAD_SYMBOL(avfilter, av_buffersink_get_frame);
+    LOAD_SYMBOL(avfilter, av_buffersrc_add_frame);
+    LOAD_SYMBOL(avfilter, avfilter_get_by_name);
+    LOAD_SYMBOL(avfilter, avfilter_graph_alloc);
+    LOAD_SYMBOL(avfilter, avfilter_graph_config);
+    LOAD_SYMBOL(avfilter, avfilter_graph_create_filter);
+    LOAD_SYMBOL(avfilter, avfilter_graph_free);
+    LOAD_SYMBOL(avfilter, avfilter_graph_parse_ptr);
+    LOAD_SYMBOL(avfilter, avfilter_inout_alloc);
+    LOAD_SYMBOL(avfilter, avfilter_inout_free);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libavfilter.");
+        avfilter.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libavfilter.");
+    return true;
+}
+
+static bool LoadAVFormat() {
+    if (avformat) {
+        return true;
+    }
+
+    avformat = std::make_unique<DynamicLibrary>("avformat", LIBAVFORMAT_VERSION_MAJOR);
+    if (!avformat->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libavformat: {}", avformat->GetLoadError());
+        avformat.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+
+    LOAD_SYMBOL(avformat, avformat_version);
+
+    auto major_version = AV_VERSION_MAJOR(avformat_version());
+    if (major_version != LIBAVFORMAT_VERSION_MAJOR) {
+        LOG_WARNING(Common, "libavformat version {} does not match supported version {}.",
+                    major_version, LIBAVFORMAT_VERSION_MAJOR);
+        avformat.reset();
+        return false;
+    }
+
+    LOAD_SYMBOL(avformat, av_guess_format);
+    LOAD_SYMBOL(avformat, av_interleaved_write_frame);
+    LOAD_SYMBOL(avformat, av_muxer_iterate);
+    LOAD_SYMBOL(avformat, av_write_trailer);
+    LOAD_SYMBOL(avformat, avformat_alloc_output_context2);
+    LOAD_SYMBOL(avformat, avformat_free_context);
+    LOAD_SYMBOL(avformat, avformat_get_class);
+    LOAD_SYMBOL(avformat, avformat_network_init);
+    LOAD_SYMBOL(avformat, avformat_new_stream);
+    LOAD_SYMBOL(avformat, avformat_query_codec);
+    LOAD_SYMBOL(avformat, avformat_write_header);
+    LOAD_SYMBOL(avformat, avio_closep);
+    LOAD_SYMBOL(avformat, avio_open);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libavformat.");
+        avformat.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libavformat.");
+    return true;
+}
+
+static bool LoadSWResample() {
+    if (swresample) {
+        return true;
+    }
+
+    swresample = std::make_unique<DynamicLibrary>("swresample", LIBSWRESAMPLE_VERSION_MAJOR);
+    if (!swresample->IsLoaded()) {
+        LOG_WARNING(Common, "Could not dynamically load libswresample: {}",
+                    swresample->GetLoadError());
+        swresample.reset();
+        return false;
+    }
+
+    auto any_failed = false;
+
+    LOAD_SYMBOL(swresample, swresample_version);
+
+    auto major_version = AV_VERSION_MAJOR(swresample_version());
+    if (major_version != LIBSWRESAMPLE_VERSION_MAJOR) {
+        LOG_WARNING(Common, "libswresample version {} does not match supported version {}.",
+                    major_version, LIBSWRESAMPLE_VERSION_MAJOR);
+        swresample.reset();
+        return false;
+    }
+
+#if LIBSWRESAMPLE_VERSION_INT >= AV_VERSION_INT(4, 5, 100)
+    LOAD_SYMBOL(swresample, swr_alloc_set_opts2);
+#else
+    LOAD_SYMBOL(swresample, swr_alloc_set_opts);
+#endif
+    LOAD_SYMBOL(swresample, swr_convert);
+    LOAD_SYMBOL(swresample, swr_free);
+    LOAD_SYMBOL(swresample, swr_init);
+
+    if (any_failed) {
+        LOG_WARNING(Common, "Could not find all required functions in libswresample.");
+        swresample.reset();
+        return false;
+    }
+
+    LOG_INFO(Common, "Successfully loaded libswresample.");
+    return true;
+}
+
+bool LoadFFmpeg() {
+    return LoadAVUtil() && LoadAVCodec() && LoadAVFilter() && LoadAVFormat() && LoadSWResample();
+}
+
+} // namespace DynamicLibrary::FFmpeg
diff --git a/src/common/dynamic_library/ffmpeg.h b/src/common/dynamic_library/ffmpeg.h
new file mode 100644
index 000000000..023847179
--- /dev/null
+++ b/src/common/dynamic_library/ffmpeg.h
@@ -0,0 +1,236 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavfilter/avfilter.h>
+#include <libavformat/avformat.h>
+#include <libavutil/avutil.h>
+#include <libavutil/ffversion.h>
+#include <libavutil/opt.h>
+#include <libavutil/pixdesc.h>
+#include <libswresample/swresample.h>
+}
+
+#include "common/common_types.h"
+#include "common/dynamic_library/dynamic_library.h"
+
+namespace DynamicLibrary::FFmpeg {
+
+// avutil
+typedef AVBufferRef* (*av_buffer_ref_func)(const AVBufferRef*);
+typedef void (*av_buffer_unref_func)(AVBufferRef**);
+typedef AVRational (*av_d2q_func)(double d, int max);
+typedef int (*av_dict_count_func)(const AVDictionary*);
+typedef AVDictionaryEntry* (*av_dict_get_func)(const AVDictionary*, const char*,
+                                               const AVDictionaryEntry*, int);
+typedef int (*av_dict_get_string_func)(const AVDictionary*, char**, const char, const char);
+typedef int (*av_dict_set_func)(AVDictionary**, const char*, const char*, int);
+typedef AVFrame* (*av_frame_alloc_func)();
+typedef void (*av_frame_free_func)(AVFrame**);
+typedef void (*av_frame_unref_func)(AVFrame*);
+typedef void (*av_freep_func)(void*);
+typedef int (*av_get_bytes_per_sample_func)(AVSampleFormat);
+typedef AVPixelFormat (*av_get_pix_fmt_func)(const char*);
+typedef const char* (*av_get_pix_fmt_name_func)(AVPixelFormat);
+typedef const char* (*av_get_sample_fmt_name_func)(AVSampleFormat);
+typedef int (*av_hwdevice_ctx_create_func)(AVBufferRef**, AVHWDeviceType, const char*,
+                                           AVDictionary*, int);
+typedef AVHWFramesConstraints* (*av_hwdevice_get_hwframe_constraints_func)(AVBufferRef*,
+                                                                           const void*);
+typedef void (*av_hwframe_constraints_free_func)(AVHWFramesConstraints**);
+typedef AVBufferRef* (*av_hwframe_ctx_alloc_func)(AVBufferRef*);
+typedef int (*av_hwframe_ctx_init_func)(AVBufferRef*);
+typedef int (*av_hwframe_get_buffer_func)(AVBufferRef*, AVFrame*, int);
+typedef int (*av_hwframe_transfer_data_func)(AVFrame*, const AVFrame*, int);
+typedef unsigned (*av_int_list_length_for_size_func)(unsigned, const void*, uint64_t);
+#if LIBAVCODEC_VERSION_MAJOR >= 59
+typedef const AVClass* (*av_opt_child_class_iterate_func)(const AVClass*, void**);
+#else
+typedef const AVClass* (*av_opt_child_class_next_func)(const AVClass*, const AVClass*);
+#endif
+typedef const AVOption* (*av_opt_next_func)(const void*, const AVOption*);
+typedef int (*av_opt_set_bin_func)(void*, const char*, const uint8_t*, int, int);
+typedef const AVPixFmtDescriptor* (*av_pix_fmt_desc_get_func)(AVPixelFormat);
+typedef const AVPixFmtDescriptor* (*av_pix_fmt_desc_next_func)(const AVPixFmtDescriptor*);
+typedef int (*av_sample_fmt_is_planar_func)(AVSampleFormat);
+typedef int (*av_samples_alloc_array_and_samples_func)(uint8_t***, int*, int, int, AVSampleFormat,
+                                                       int);
+typedef char* (*av_strdup_func)(const char*);
+typedef unsigned (*avutil_version_func)();
+
+extern av_buffer_ref_func av_buffer_ref;
+extern av_buffer_unref_func av_buffer_unref;
+extern av_d2q_func av_d2q;
+extern av_dict_count_func av_dict_count;
+extern av_dict_get_func av_dict_get;
+extern av_dict_get_string_func av_dict_get_string;
+extern av_dict_set_func av_dict_set;
+extern av_frame_alloc_func av_frame_alloc;
+extern av_frame_free_func av_frame_free;
+extern av_frame_unref_func av_frame_unref;
+extern av_freep_func av_freep;
+extern av_get_bytes_per_sample_func av_get_bytes_per_sample;
+extern av_get_pix_fmt_func av_get_pix_fmt;
+extern av_get_pix_fmt_name_func av_get_pix_fmt_name;
+extern av_get_sample_fmt_name_func av_get_sample_fmt_name;
+extern av_hwdevice_ctx_create_func av_hwdevice_ctx_create;
+extern av_hwdevice_get_hwframe_constraints_func av_hwdevice_get_hwframe_constraints;
+extern av_hwframe_constraints_free_func av_hwframe_constraints_free;
+extern av_hwframe_ctx_alloc_func av_hwframe_ctx_alloc;
+extern av_hwframe_ctx_init_func av_hwframe_ctx_init;
+extern av_hwframe_get_buffer_func av_hwframe_get_buffer;
+extern av_hwframe_transfer_data_func av_hwframe_transfer_data;
+extern av_int_list_length_for_size_func av_int_list_length_for_size;
+#if LIBAVCODEC_VERSION_MAJOR >= 59
+extern av_opt_child_class_iterate_func av_opt_child_class_iterate;
+#else
+extern av_opt_child_class_next_func av_opt_child_class_next;
+#endif
+extern av_opt_next_func av_opt_next;
+extern av_opt_set_bin_func av_opt_set_bin;
+extern av_pix_fmt_desc_get_func av_pix_fmt_desc_get;
+extern av_pix_fmt_desc_next_func av_pix_fmt_desc_next;
+extern av_sample_fmt_is_planar_func av_sample_fmt_is_planar;
+extern av_samples_alloc_array_and_samples_func av_samples_alloc_array_and_samples;
+extern av_strdup_func av_strdup;
+extern avutil_version_func avutil_version;
+
+// avcodec
+typedef int (*av_codec_is_encoder_func)(const AVCodec*);
+typedef const AVCodec* (*av_codec_iterate_func)(void**);
+typedef void (*av_init_packet_func)(AVPacket*);
+typedef AVPacket* (*av_packet_alloc_func)();
+typedef void (*av_packet_free_func)(AVPacket**);
+typedef void (*av_packet_rescale_ts_func)(AVPacket*, AVRational, AVRational);
+typedef void (*av_parser_close_func)(AVCodecParserContext*);
+typedef AVCodecParserContext* (*av_parser_init_func)(int);
+typedef int (*av_parser_parse2_func)(AVCodecParserContext*, AVCodecContext*, uint8_t**, int*,
+                                     const uint8_t*, int, int64_t, int64_t, int64_t);
+typedef AVCodecContext* (*avcodec_alloc_context3_func)(const AVCodec*);
+typedef const AVCodecDescriptor* (*avcodec_descriptor_next_func)(const AVCodecDescriptor*);
+typedef AVCodec* (*avcodec_find_decoder_func)(AVCodecID);
+typedef const AVCodec* (*avcodec_find_encoder_by_name_func)(const char*);
+typedef void (*avcodec_free_context_func)(AVCodecContext**);
+typedef const AVClass* (*avcodec_get_class_func)();
+typedef const AVCodecHWConfig* (*avcodec_get_hw_config_func)(const AVCodec*, int);
+typedef int (*avcodec_open2_func)(AVCodecContext*, const AVCodec*, AVDictionary**);
+typedef int (*avcodec_parameters_from_context_func)(AVCodecParameters* par, const AVCodecContext*);
+typedef int (*avcodec_receive_frame_func)(AVCodecContext*, AVFrame*);
+typedef int (*avcodec_receive_packet_func)(AVCodecContext*, AVPacket*);
+typedef int (*avcodec_send_frame_func)(AVCodecContext*, const AVFrame*);
+typedef int (*avcodec_send_packet_func)(AVCodecContext*, const AVPacket*);
+typedef unsigned (*avcodec_version_func)();
+
+extern av_codec_is_encoder_func av_codec_is_encoder;
+extern av_codec_iterate_func av_codec_iterate;
+extern av_init_packet_func av_init_packet;
+extern av_packet_alloc_func av_packet_alloc;
+extern av_packet_free_func av_packet_free;
+extern av_packet_rescale_ts_func av_packet_rescale_ts;
+extern av_parser_close_func av_parser_close;
+extern av_parser_init_func av_parser_init;
+extern av_parser_parse2_func av_parser_parse2;
+extern avcodec_alloc_context3_func avcodec_alloc_context3;
+extern avcodec_descriptor_next_func avcodec_descriptor_next;
+extern avcodec_find_decoder_func avcodec_find_decoder;
+extern avcodec_find_encoder_by_name_func avcodec_find_encoder_by_name;
+extern avcodec_free_context_func avcodec_free_context;
+extern avcodec_get_class_func avcodec_get_class;
+extern avcodec_get_hw_config_func avcodec_get_hw_config;
+extern avcodec_open2_func avcodec_open2;
+extern avcodec_parameters_from_context_func avcodec_parameters_from_context;
+extern avcodec_receive_frame_func avcodec_receive_frame;
+extern avcodec_receive_packet_func avcodec_receive_packet;
+extern avcodec_send_frame_func avcodec_send_frame;
+extern avcodec_send_packet_func avcodec_send_packet;
+extern avcodec_version_func avcodec_version;
+
+// avfilter
+typedef int (*av_buffersink_get_frame_func)(AVFilterContext*, AVFrame*);
+typedef int (*av_buffersrc_add_frame_func)(AVFilterContext*, AVFrame*);
+typedef const AVFilter* (*avfilter_get_by_name_func)(const char*);
+typedef AVFilterGraph* (*avfilter_graph_alloc_func)();
+typedef int (*avfilter_graph_config_func)(AVFilterGraph*, void*);
+typedef int (*avfilter_graph_create_filter_func)(AVFilterContext**, const AVFilter*, const char*,
+                                                 const char*, void*, AVFilterGraph*);
+typedef void (*avfilter_graph_free_func)(AVFilterGraph** graph);
+typedef int (*avfilter_graph_parse_ptr_func)(AVFilterGraph*, const char*, AVFilterInOut**,
+                                             AVFilterInOut**, void*);
+typedef AVFilterInOut* (*avfilter_inout_alloc_func)();
+typedef void (*avfilter_inout_free_func)(AVFilterInOut**);
+typedef unsigned (*avfilter_version_func)();
+
+extern av_buffersink_get_frame_func av_buffersink_get_frame;
+extern av_buffersrc_add_frame_func av_buffersrc_add_frame;
+extern avfilter_get_by_name_func avfilter_get_by_name;
+extern avfilter_graph_alloc_func avfilter_graph_alloc;
+extern avfilter_graph_config_func avfilter_graph_config;
+extern avfilter_graph_create_filter_func avfilter_graph_create_filter;
+extern avfilter_graph_free_func avfilter_graph_free;
+extern avfilter_graph_parse_ptr_func avfilter_graph_parse_ptr;
+extern avfilter_inout_alloc_func avfilter_inout_alloc;
+extern avfilter_inout_free_func avfilter_inout_free;
+extern avfilter_version_func avfilter_version;
+
+// avformat
+typedef const AVOutputFormat* (*av_guess_format_func)(const char*, const char*, const char*);
+typedef int (*av_interleaved_write_frame_func)(AVFormatContext*, AVPacket*);
+typedef const AVOutputFormat* (*av_muxer_iterate_func)(void**);
+typedef int (*av_write_trailer_func)(AVFormatContext*);
+typedef int (*avformat_alloc_output_context2_func)(AVFormatContext**, const AVOutputFormat*,
+                                                   const char*, const char*);
+typedef void (*avformat_free_context_func)(AVFormatContext*);
+typedef const AVClass* (*avformat_get_class_func)();
+typedef int (*avformat_network_init_func)();
+typedef AVStream* (*avformat_new_stream_func)(AVFormatContext*, const AVCodec*);
+typedef int (*avformat_query_codec_func)(const AVOutputFormat*, AVCodecID, int);
+typedef int (*avformat_write_header_func)(AVFormatContext*, AVDictionary**);
+typedef unsigned (*avformat_version_func)();
+typedef int (*avio_closep_func)(AVIOContext**);
+typedef int (*avio_open_func)(AVIOContext**, const char*, int);
+
+extern av_guess_format_func av_guess_format;
+extern av_interleaved_write_frame_func av_interleaved_write_frame;
+extern av_muxer_iterate_func av_muxer_iterate;
+extern av_write_trailer_func av_write_trailer;
+extern avformat_alloc_output_context2_func avformat_alloc_output_context2;
+extern avformat_free_context_func avformat_free_context;
+extern avformat_get_class_func avformat_get_class;
+extern avformat_network_init_func avformat_network_init;
+extern avformat_new_stream_func avformat_new_stream;
+extern avformat_query_codec_func avformat_query_codec;
+extern avformat_write_header_func avformat_write_header;
+extern avformat_version_func avformat_version;
+extern avio_closep_func avio_closep;
+extern avio_open_func avio_open;
+
+// swresample
+#if LIBSWRESAMPLE_VERSION_INT >= AV_VERSION_INT(4, 5, 100)
+typedef SwrContext* (*swr_alloc_set_opts2_func)(SwrContext**, AVChannelLayout*, AVSampleFormat, int,
+                                                AVChannelLayout*, AVSampleFormat, int, int, void*);
+#else
+typedef SwrContext* (*swr_alloc_set_opts_func)(SwrContext*, int64_t, AVSampleFormat, int, int64_t,
+                                               AVSampleFormat, int, int, void*);
+#endif
+typedef int (*swr_convert_func)(SwrContext*, uint8_t**, int, const uint8_t**, int);
+typedef void (*swr_free_func)(SwrContext**);
+typedef int (*swr_init_func)(SwrContext*);
+typedef unsigned (*swresample_version_func)();
+
+#if LIBSWRESAMPLE_VERSION_INT >= AV_VERSION_INT(4, 5, 100)
+extern swr_alloc_set_opts2_func swr_alloc_set_opts2;
+#else
+extern swr_alloc_set_opts_func swr_alloc_set_opts;
+#endif
+extern swr_convert_func swr_convert;
+extern swr_free_func swr_free;
+extern swr_init_func swr_init;
+extern swresample_version_func swresample_version;
+
+bool LoadFFmpeg();
+
+} // namespace DynamicLibrary::FFmpeg
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 1ee217a6b..b8a9d778e 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -38,6 +38,8 @@ add_library(citra_core STATIC
     core_timing.h
     dumping/backend.cpp
     dumping/backend.h
+    dumping/ffmpeg_backend.cpp
+    dumping/ffmpeg_backend.h
     file_sys/archive_backend.cpp
     file_sys/archive_backend.h
     file_sys/archive_extsavedata.cpp
@@ -468,13 +470,6 @@ add_library(citra_core STATIC
     tracer/recorder.h
 )
 
-if (ENABLE_FFMPEG_VIDEO_DUMPER)
-    target_sources(citra_core PRIVATE
-        dumping/ffmpeg_backend.cpp
-        dumping/ffmpeg_backend.h
-    )
-endif()
-
 create_target_directory_groups(citra_core)
 
 target_link_libraries(citra_core PUBLIC citra_common PRIVATE audio_core network video_core)
@@ -504,10 +499,6 @@ if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
     target_link_libraries(citra_core PRIVATE dynarmic)
 endif()
 
-if (ENABLE_FFMPEG_VIDEO_DUMPER)
-    target_link_libraries(citra_core PUBLIC FFmpeg::avcodec FFmpeg::avfilter FFmpeg::avformat FFmpeg::swresample FFmpeg::avutil)
-endif()
-
 if (CITRA_USE_PRECOMPILED_HEADERS)
     target_precompile_headers(citra_core PRIVATE precompiled_headers.h)
 endif()
diff --git a/src/core/core.cpp b/src/core/core.cpp
index e1710b4b7..a4ad8a817 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -11,6 +11,7 @@
 #include "audio_core/lle/lle.h"
 #include "common/arch.h"
 #include "common/logging/log.h"
+#include "common/settings.h"
 #include "common/texture.h"
 #include "core/arm/arm_interface.h"
 #include "core/arm/exclusive_monitor.h"
@@ -22,10 +23,7 @@
 #include "core/core.h"
 #include "core/core_timing.h"
 #include "core/dumping/backend.h"
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
 #include "core/dumping/ffmpeg_backend.h"
-#endif
-#include "common/settings.h"
 #include "core/frontend/image_interface.h"
 #include "core/gdbstub/gdbstub.h"
 #include "core/global.h"
@@ -423,12 +421,6 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window,
     Service::Init(*this);
     GDBStub::DeferStart();
 
-#ifdef ENABLE_FFMPEG_VIDEO_DUMPER
-    video_dumper = std::make_unique<VideoDumper::FFmpegBackend>();
-#else
-    video_dumper = std::make_unique<VideoDumper::NullBackend>();
-#endif
-
     if (!registered_image_interface) {
         registered_image_interface = std::make_shared<Frontend::ImageInterface>();
     }
@@ -500,12 +492,8 @@ const Cheats::CheatEngine& System::CheatEngine() const {
     return *cheat_engine;
 }
 
-VideoDumper::Backend& System::VideoDumper() {
-    return *video_dumper;
-}
-
-const VideoDumper::Backend& System::VideoDumper() const {
-    return *video_dumper;
+void System::RegisterVideoDumper(std::shared_ptr<VideoDumper::Backend> dumper) {
+    video_dumper = std::move(dumper);
 }
 
 VideoCore::CustomTexManager& System::CustomTexManager() {
diff --git a/src/core/core.h b/src/core/core.h
index d4cab87e3..b29fc20e2 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -258,11 +258,13 @@ public:
     /// Gets a const reference to the custom texture cache system
     [[nodiscard]] const VideoCore::CustomTexManager& CustomTexManager() const;
 
-    /// Gets a reference to the video dumper backend
-    [[nodiscard]] VideoDumper::Backend& VideoDumper();
+    /// Video Dumper interface
 
-    /// Gets a const reference to the video dumper backend
-    [[nodiscard]] const VideoDumper::Backend& VideoDumper() const;
+    void RegisterVideoDumper(std::shared_ptr<VideoDumper::Backend> video_dumper);
+
+    [[nodiscard]] std::shared_ptr<VideoDumper::Backend> GetVideoDumper() const {
+        return video_dumper;
+    }
 
     std::unique_ptr<PerfStats> perf_stats;
     FrameLimiter frame_limiter;
@@ -370,7 +372,7 @@ private:
     std::unique_ptr<Cheats::CheatEngine> cheat_engine;
 
     /// Video dumper backend
-    std::unique_ptr<VideoDumper::Backend> video_dumper;
+    std::shared_ptr<VideoDumper::Backend> video_dumper;
 
     /// Custom texture cache system
     std::unique_ptr<VideoCore::CustomTexManager> custom_tex_manager;
diff --git a/src/core/dumping/ffmpeg_backend.cpp b/src/core/dumping/ffmpeg_backend.cpp
index 0981a2715..c0c8a4f46 100644
--- a/src/core/dumping/ffmpeg_backend.cpp
+++ b/src/core/dumping/ffmpeg_backend.cpp
@@ -15,24 +15,17 @@
 #include "video_core/renderer_base.h"
 #include "video_core/video_core.h"
 
-extern "C" {
-#include <libavfilter/buffersink.h>
-#include <libavfilter/buffersrc.h>
-#include <libavutil/hwcontext.h>
-#include <libavutil/pixdesc.h>
-}
+using namespace DynamicLibrary;
 
 namespace VideoDumper {
 
 void InitializeFFmpegLibraries() {
     static bool initialized = false;
-
-    if (initialized)
+    if (initialized) {
         return;
-#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100)
-    av_register_all();
-#endif
-    avformat_network_init();
+    }
+
+    FFmpeg::avformat_network_init();
     initialized = true;
 }
 
@@ -40,7 +33,7 @@ AVDictionary* ToAVDictionary(const std::string& serialized) {
     Common::ParamPackage param_package{serialized};
     AVDictionary* result = nullptr;
     for (const auto& [key, value] : param_package) {
-        av_dict_set(&result, key.c_str(), value.c_str(), 0);
+        FFmpeg::av_dict_set(&result, key.c_str(), value.c_str(), 0);
     }
     return result;
 }
@@ -67,29 +60,29 @@ void FFmpegStream::Flush() {
 }
 
 void FFmpegStream::WritePacket(AVPacket& packet) {
-    av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base);
+    FFmpeg::av_packet_rescale_ts(&packet, codec_context->time_base, stream->time_base);
     packet.stream_index = stream->index;
     {
         std::lock_guard lock{*format_context_mutex};
-        av_interleaved_write_frame(format_context, &packet);
+        FFmpeg::av_interleaved_write_frame(format_context, &packet);
     }
 }
 
 void FFmpegStream::SendFrame(AVFrame* frame) {
     // Initialize packet
     AVPacket packet;
-    av_init_packet(&packet);
+    FFmpeg::av_init_packet(&packet);
     packet.data = nullptr;
     packet.size = 0;
 
     // Encode frame
-    if (avcodec_send_frame(codec_context.get(), frame) < 0) {
+    if (FFmpeg::avcodec_send_frame(codec_context.get(), frame) < 0) {
         LOG_ERROR(Render, "Frame dropped: could not send frame");
         return;
     }
     int error = 1;
     while (error >= 0) {
-        error = avcodec_receive_packet(codec_context.get(), &packet);
+        error = FFmpeg::avcodec_receive_packet(codec_context.get(), &packet);
         if (error == AVERROR(EAGAIN) || error == AVERROR_EOF)
             return;
         if (error < 0) {
@@ -111,7 +104,7 @@ FFmpegVideoStream::~FFmpegVideoStream() {
 static AVPixelFormat GetPixelFormat(AVCodecContext* avctx, const AVPixelFormat* fmt) {
     // Choose a software pixel format if any, prefering those in the front of the list
     for (int i = 0; fmt[i] != AV_PIX_FMT_NONE; i++) {
-        const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(fmt[i]);
+        const AVPixFmtDescriptor* desc = FFmpeg::av_pix_fmt_desc_get(fmt[i]);
         if (!(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
             return fmt[i];
         }
@@ -123,7 +116,7 @@ static AVPixelFormat GetPixelFormat(AVCodecContext* avctx, const AVPixelFormat*
     for (int i = 0; fmt[i] != AV_PIX_FMT_NONE; i++) {
         const AVCodecHWConfig* config;
         for (int j = 0;; j++) {
-            config = avcodec_get_hw_config(avctx->codec, j);
+            config = FFmpeg::avcodec_get_hw_config(avctx->codec, j);
             if (!config || config->pix_fmt == fmt[i]) {
                 break;
             }
@@ -144,18 +137,19 @@ static AVPixelFormat GetPixelFormat(AVCodecContext* avctx, const AVPixelFormat*
 }
 
 bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout& layout_) {
-
     InitializeFFmpegLibraries();
 
-    if (!FFmpegStream::Init(muxer))
+    if (!FFmpegStream::Init(muxer)) {
         return false;
+    }
 
     layout = layout_;
     frame_count = 0;
 
     // Initialize video codec
-    const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
-    codec_context.reset(avcodec_alloc_context3(codec));
+    const AVCodec* codec =
+        FFmpeg::avcodec_find_encoder_by_name(Settings::values.video_encoder.c_str());
+    codec_context.reset(FFmpeg::avcodec_alloc_context3(codec));
     if (!codec || !codec_context) {
         LOG_ERROR(Render, "Could not find video encoder or allocate video codec context");
         return false;
@@ -173,9 +167,9 @@ bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout
 
     // Get pixel format for codec
     auto options = ToAVDictionary(Settings::values.video_encoder_options);
-    auto pixel_format_opt = av_dict_get(options, "pixel_format", nullptr, 0);
+    auto pixel_format_opt = FFmpeg::av_dict_get(options, "pixel_format", nullptr, 0);
     if (pixel_format_opt) {
-        sw_pixel_format = av_get_pix_fmt(pixel_format_opt->value);
+        sw_pixel_format = FFmpeg::av_get_pix_fmt(pixel_format_opt->value);
     } else if (codec->pix_fmts) {
         sw_pixel_format = GetPixelFormat(codec_context.get(), codec->pix_fmts);
     } else {
@@ -192,23 +186,25 @@ bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout
         codec_context->pix_fmt = sw_pixel_format;
     }
 
-    if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
+    if (format_context->oformat->flags & AVFMT_GLOBALHEADER) {
         codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+    }
 
-    if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
+    if (FFmpeg::avcodec_open2(codec_context.get(), codec, &options) < 0) {
         LOG_ERROR(Render, "Could not open video codec");
         return false;
     }
 
-    if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
+    if (FFmpeg::av_dict_count(options) != 0) { // Successfully set options are removed from the dict
         char* buf = nullptr;
-        av_dict_get_string(options, &buf, ':', ';');
+        FFmpeg::av_dict_get_string(options, &buf, ':', ';');
         LOG_WARNING(Render, "Video encoder options not found: {}", buf);
     }
 
     // Create video stream
-    stream = avformat_new_stream(format_context, codec);
-    if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
+    stream = FFmpeg::avformat_new_stream(format_context, codec);
+    if (!stream ||
+        FFmpeg::avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
         LOG_ERROR(Render, "Could not create video stream");
         return false;
     }
@@ -216,12 +212,12 @@ bool FFmpegVideoStream::Init(FFmpegMuxer& muxer, const Layout::FramebufferLayout
     stream->time_base = codec_context->time_base;
 
     // Allocate frames
-    current_frame.reset(av_frame_alloc());
-    filtered_frame.reset(av_frame_alloc());
+    current_frame.reset(FFmpeg::av_frame_alloc());
+    filtered_frame.reset(FFmpeg::av_frame_alloc());
 
     if (requires_hw_frames) {
-        hw_frame.reset(av_frame_alloc());
-        if (av_hwframe_get_buffer(codec_context->hw_frames_ctx, hw_frame.get(), 0) < 0) {
+        hw_frame.reset(FFmpeg::av_frame_alloc());
+        if (FFmpeg::av_hwframe_get_buffer(codec_context->hw_frames_ctx, hw_frame.get(), 0) < 0) {
             LOG_ERROR(Render, "Could not allocate buffer for HW frame");
             return false;
         }
@@ -255,12 +251,12 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) {
     current_frame->pts = frame_count++;
 
     // Filter the frame
-    if (av_buffersrc_add_frame(source_context, current_frame.get()) < 0) {
+    if (FFmpeg::av_buffersrc_add_frame(source_context, current_frame.get()) < 0) {
         LOG_ERROR(Render, "Video frame dropped: Could not add frame to filter graph");
         return;
     }
     while (true) {
-        const int error = av_buffersink_get_frame(sink_context, filtered_frame.get());
+        const int error = FFmpeg::av_buffersink_get_frame(sink_context, filtered_frame.get());
         if (error == AVERROR(EAGAIN) || error == AVERROR_EOF) {
             return;
         }
@@ -269,7 +265,7 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) {
             return;
         } else {
             if (requires_hw_frames) {
-                if (av_hwframe_transfer_data(hw_frame.get(), filtered_frame.get(), 0) < 0) {
+                if (FFmpeg::av_hwframe_transfer_data(hw_frame.get(), filtered_frame.get(), 0) < 0) {
                     LOG_ERROR(Render, "Video frame dropped: Could not upload to HW frame");
                     return;
                 }
@@ -278,7 +274,7 @@ void FFmpegVideoStream::ProcessFrame(VideoFrame& frame) {
                 SendFrame(filtered_frame.get());
             }
 
-            av_frame_unref(filtered_frame.get());
+            FFmpeg::av_frame_unref(filtered_frame.get());
         }
     }
 }
@@ -287,7 +283,7 @@ bool FFmpegVideoStream::InitHWContext(const AVCodec* codec) {
     for (std::size_t i = 0; codec->pix_fmts[i] != AV_PIX_FMT_NONE; ++i) {
         const AVCodecHWConfig* config;
         for (int j = 0;; ++j) {
-            config = avcodec_get_hw_config(codec, j);
+            config = FFmpeg::avcodec_get_hw_config(codec, j);
             if (!config || config->pix_fmt == codec->pix_fmts[i]) {
                 break;
             }
@@ -306,22 +302,22 @@ bool FFmpegVideoStream::InitHWContext(const AVCodec* codec) {
 
         // Create HW device context
         AVBufferRef* hw_device_context;
-        SCOPE_EXIT({ av_buffer_unref(&hw_device_context); });
+        SCOPE_EXIT({ FFmpeg::av_buffer_unref(&hw_device_context); });
 
         // TODO: Provide the argument here somehow.
         // This is necessary for some devices like CUDA where you must supply the GPU name.
         // This is not necessary for VAAPI, etc.
-        if (av_hwdevice_ctx_create(&hw_device_context, config->device_type, nullptr, nullptr, 0) <
-            0) {
+        if (FFmpeg::av_hwdevice_ctx_create(&hw_device_context, config->device_type, nullptr,
+                                           nullptr, 0) < 0) {
             LOG_ERROR(Render, "Failed to create HW device context");
             continue;
         }
-        codec_context->hw_device_ctx = av_buffer_ref(hw_device_context);
+        codec_context->hw_device_ctx = FFmpeg::av_buffer_ref(hw_device_context);
 
         // Get the SW format
         AVHWFramesConstraints* constraints =
-            av_hwdevice_get_hwframe_constraints(hw_device_context, nullptr);
-        SCOPE_EXIT({ av_hwframe_constraints_free(&constraints); });
+            FFmpeg::av_hwdevice_get_hwframe_constraints(hw_device_context, nullptr);
+        SCOPE_EXIT({ FFmpeg::av_hwframe_constraints_free(&constraints); });
 
         if (constraints) {
             sw_pixel_format = constraints->valid_sw_formats ? constraints->valid_sw_formats[0]
@@ -341,9 +337,9 @@ bool FFmpegVideoStream::InitHWContext(const AVCodec* codec) {
 
         // Create HW frames context
         AVBufferRef* hw_frames_context_ref;
-        SCOPE_EXIT({ av_buffer_unref(&hw_frames_context_ref); });
+        SCOPE_EXIT({ FFmpeg::av_buffer_unref(&hw_frames_context_ref); });
 
-        if (!(hw_frames_context_ref = av_hwframe_ctx_alloc(hw_device_context))) {
+        if (!(hw_frames_context_ref = FFmpeg::av_hwframe_ctx_alloc(hw_device_context))) {
             LOG_ERROR(Render, "Failed to create HW frames context");
             continue;
         }
@@ -356,12 +352,12 @@ bool FFmpegVideoStream::InitHWContext(const AVCodec* codec) {
         hw_frames_context->height = codec_context->height;
         hw_frames_context->initial_pool_size = 20; // value from FFmpeg's example
 
-        if (av_hwframe_ctx_init(hw_frames_context_ref) < 0) {
+        if (FFmpeg::av_hwframe_ctx_init(hw_frames_context_ref) < 0) {
             LOG_ERROR(Render, "Failed to initialize HW frames context");
             continue;
         }
 
-        codec_context->hw_frames_ctx = av_buffer_ref(hw_frames_context_ref);
+        codec_context->hw_frames_ctx = FFmpeg::av_buffer_ref(hw_frames_context_ref);
         return true;
     }
 
@@ -370,10 +366,10 @@ bool FFmpegVideoStream::InitHWContext(const AVCodec* codec) {
 }
 
 bool FFmpegVideoStream::InitFilters() {
-    filter_graph.reset(avfilter_graph_alloc());
+    filter_graph.reset(FFmpeg::avfilter_graph_alloc());
 
-    const AVFilter* source = avfilter_get_by_name("buffer");
-    const AVFilter* sink = avfilter_get_by_name("buffersink");
+    const AVFilter* source = FFmpeg::avfilter_get_by_name("buffer");
+    const AVFilter* sink = FFmpeg::avfilter_get_by_name("buffersink");
     if (!source || !sink) {
         LOG_ERROR(Render, "Could not find buffer source or sink");
         return false;
@@ -385,18 +381,23 @@ bool FFmpegVideoStream::InitFilters() {
     const std::string in_args =
         fmt::format("video_size={}x{}:pix_fmt={}:time_base={}/{}:pixel_aspect=1", layout.width,
                     layout.height, pixel_format, src_time_base.num, src_time_base.den);
-    if (avfilter_graph_create_filter(&source_context, source, "in", in_args.c_str(), nullptr,
-                                     filter_graph.get()) < 0) {
+    if (FFmpeg::avfilter_graph_create_filter(&source_context, source, "in", in_args.c_str(),
+                                             nullptr, filter_graph.get()) < 0) {
         LOG_ERROR(Render, "Could not create buffer source");
         return false;
     }
 
     // Configure buffer sink
-    if (avfilter_graph_create_filter(&sink_context, sink, "out", nullptr, nullptr,
-                                     filter_graph.get()) < 0) {
+    if (FFmpeg::avfilter_graph_create_filter(&sink_context, sink, "out", nullptr, nullptr,
+                                             filter_graph.get()) < 0) {
         LOG_ERROR(Render, "Could not create buffer sink");
         return false;
     }
+
+    // Point av_opt_set_int_list to correct functions.
+#define av_int_list_length_for_size FFmpeg::av_int_list_length_for_size
+#define av_opt_set_bin FFmpeg::av_opt_set_bin
+
     const AVPixelFormat pix_fmts[] = {sw_pixel_format, AV_PIX_FMT_NONE};
     if (av_opt_set_int_list(sink_context, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE,
                             AV_OPT_SEARCH_CHILDREN) < 0) {
@@ -406,30 +407,30 @@ bool FFmpegVideoStream::InitFilters() {
 
     // Initialize filter graph
     // `outputs` as in outputs of the 'previous' graphs
-    AVFilterInOut* outputs = avfilter_inout_alloc();
-    outputs->name = av_strdup("in");
+    AVFilterInOut* outputs = FFmpeg::avfilter_inout_alloc();
+    outputs->name = FFmpeg::av_strdup("in");
     outputs->filter_ctx = source_context;
     outputs->pad_idx = 0;
     outputs->next = nullptr;
 
     // `inputs` as in inputs to the 'next' graphs
-    AVFilterInOut* inputs = avfilter_inout_alloc();
-    inputs->name = av_strdup("out");
+    AVFilterInOut* inputs = FFmpeg::avfilter_inout_alloc();
+    inputs->name = FFmpeg::av_strdup("out");
     inputs->filter_ctx = sink_context;
     inputs->pad_idx = 0;
     inputs->next = nullptr;
 
     SCOPE_EXIT({
-        avfilter_inout_free(&outputs);
-        avfilter_inout_free(&inputs);
+        FFmpeg::avfilter_inout_free(&outputs);
+        FFmpeg::avfilter_inout_free(&inputs);
     });
 
-    if (avfilter_graph_parse_ptr(filter_graph.get(), filter_graph_desc.data(), &inputs, &outputs,
-                                 nullptr) < 0) {
+    if (FFmpeg::avfilter_graph_parse_ptr(filter_graph.get(), filter_graph_desc.data(), &inputs,
+                                         &outputs, nullptr) < 0) {
         LOG_ERROR(Render, "Could not parse or create filter graph");
         return false;
     }
-    if (avfilter_graph_config(filter_graph.get(), nullptr) < 0) {
+    if (FFmpeg::avfilter_graph_config(filter_graph.get(), nullptr) < 0) {
         LOG_ERROR(Render, "Could not configure filter graph");
         return false;
     }
@@ -444,14 +445,16 @@ FFmpegAudioStream::~FFmpegAudioStream() {
 bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) {
     InitializeFFmpegLibraries();
 
-    if (!FFmpegStream::Init(muxer))
+    if (!FFmpegStream::Init(muxer)) {
         return false;
+    }
 
     frame_count = 0;
 
     // Initialize audio codec
-    const AVCodec* codec = avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
-    codec_context.reset(avcodec_alloc_context3(codec));
+    const AVCodec* codec =
+        FFmpeg::avcodec_find_encoder_by_name(Settings::values.audio_encoder.c_str());
+    codec_context.reset(FFmpeg::avcodec_alloc_context3(codec));
     if (!codec || !codec_context) {
         LOG_ERROR(Render, "Could not find audio encoder or allocate audio codec context");
         return false;
@@ -482,20 +485,25 @@ bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) {
     }
     codec_context->time_base.num = 1;
     codec_context->time_base.den = codec_context->sample_rate;
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100)
+    codec_context->ch_layout = AV_CHANNEL_LAYOUT_STEREO;
+#else
     codec_context->channel_layout = AV_CH_LAYOUT_STEREO;
     codec_context->channels = 2;
-    if (format_context->oformat->flags & AVFMT_GLOBALHEADER)
+#endif
+    if (format_context->oformat->flags & AVFMT_GLOBALHEADER) {
         codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+    }
 
     AVDictionary* options = ToAVDictionary(Settings::values.audio_encoder_options);
-    if (avcodec_open2(codec_context.get(), codec, &options) < 0) {
+    if (FFmpeg::avcodec_open2(codec_context.get(), codec, &options) < 0) {
         LOG_ERROR(Render, "Could not open audio codec");
         return false;
     }
 
-    if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
+    if (FFmpeg::av_dict_count(options) != 0) { // Successfully set options are removed from the dict
         char* buf = nullptr;
-        av_dict_get_string(options, &buf, ':', ';');
+        FFmpeg::av_dict_get_string(options, &buf, ':', ';');
         LOG_WARNING(Render, "Audio encoder options not found: {}", buf);
     }
 
@@ -506,39 +514,49 @@ bool FFmpegAudioStream::Init(FFmpegMuxer& muxer) {
     }
 
     // Create audio stream
-    stream = avformat_new_stream(format_context, codec);
-    if (!stream || avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
+    stream = FFmpeg::avformat_new_stream(format_context, codec);
+    if (!stream ||
+        FFmpeg::avcodec_parameters_from_context(stream->codecpar, codec_context.get()) < 0) {
 
         LOG_ERROR(Render, "Could not create audio stream");
         return false;
     }
 
     // Allocate frame
-    audio_frame.reset(av_frame_alloc());
+    audio_frame.reset(FFmpeg::av_frame_alloc());
     audio_frame->format = codec_context->sample_fmt;
-    audio_frame->channel_layout = codec_context->channel_layout;
-    audio_frame->channels = codec_context->channels;
     audio_frame->sample_rate = codec_context->sample_rate;
 
-    // Allocate SWR context
-    auto* context =
-        swr_alloc_set_opts(nullptr, codec_context->channel_layout, codec_context->sample_fmt,
-                           codec_context->sample_rate, codec_context->channel_layout,
-                           AV_SAMPLE_FMT_S16P, AudioCore::native_sample_rate, 0, nullptr);
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100)
+    auto num_channels = codec_context->ch_layout.nb_channels;
+    audio_frame->ch_layout = codec_context->ch_layout;
+    SwrContext* context = nullptr;
+    FFmpeg::swr_alloc_set_opts2(&context, &codec_context->ch_layout, codec_context->sample_fmt,
+                                codec_context->sample_rate, &codec_context->ch_layout,
+                                AV_SAMPLE_FMT_S16P, AudioCore::native_sample_rate, 0, nullptr);
+#else
+    auto num_channels = codec_context->channels;
+    audio_frame->channel_layout = codec_context->channel_layout;
+    audio_frame->channels = num_channels;
+    auto* context = FFmpeg::swr_alloc_set_opts(
+        nullptr, codec_context->channel_layout, codec_context->sample_fmt,
+        codec_context->sample_rate, codec_context->channel_layout, AV_SAMPLE_FMT_S16P,
+        AudioCore::native_sample_rate, 0, nullptr);
+#endif
+
     if (!context) {
         LOG_ERROR(Render, "Could not create SWR context");
         return false;
     }
     swr_context.reset(context);
-    if (swr_init(swr_context.get()) < 0) {
+    if (FFmpeg::swr_init(swr_context.get()) < 0) {
         LOG_ERROR(Render, "Could not init SWR context");
         return false;
     }
 
     // Allocate resampled data
-    int error =
-        av_samples_alloc_array_and_samples(&resampled_data, nullptr, codec_context->channels,
-                                           frame_size, codec_context->sample_fmt, 0);
+    int error = FFmpeg::av_samples_alloc_array_and_samples(
+        &resampled_data, nullptr, num_channels, frame_size, codec_context->sample_fmt, 0);
     if (error < 0) {
         LOG_ERROR(Render, "Could not allocate samples storage");
         return false;
@@ -554,9 +572,9 @@ void FFmpegAudioStream::Free() {
     swr_context.reset();
     // Free resampled data
     if (resampled_data) {
-        av_freep(&resampled_data[0]);
+        FFmpeg::av_freep(&resampled_data[0]);
     }
-    av_freep(&resampled_data);
+    FFmpeg::av_freep(&resampled_data);
 }
 
 void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
@@ -564,20 +582,21 @@ void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
     ASSERT_MSG(channel0.size() == channel1.size(),
                "Frames of the two channels must have the same number of samples");
 
-    const auto sample_size = av_get_bytes_per_sample(codec_context->sample_fmt);
+    const auto sample_size = FFmpeg::av_get_bytes_per_sample(codec_context->sample_fmt);
     std::array<const u8*, 2> src_data = {reinterpret_cast<const u8*>(channel0.data()),
                                          reinterpret_cast<const u8*>(channel1.data())};
 
     std::array<u8*, 2> dst_data;
-    if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
+    if (FFmpeg::av_sample_fmt_is_planar(codec_context->sample_fmt)) {
         dst_data = {resampled_data[0] + sample_size * offset,
                     resampled_data[1] + sample_size * offset};
     } else {
         dst_data = {resampled_data[0] + sample_size * offset * 2}; // 2 channels
     }
 
-    auto resampled_count = swr_convert(swr_context.get(), dst_data.data(), frame_size - offset,
-                                       src_data.data(), static_cast<int>(channel0.size()));
+    auto resampled_count =
+        FFmpeg::swr_convert(swr_context.get(), dst_data.data(), frame_size - offset,
+                            src_data.data(), static_cast<int>(channel0.size()));
     if (resampled_count < 0) {
         LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
         return;
@@ -592,7 +611,7 @@ void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
         // Prepare frame
         audio_frame->nb_samples = frame_size;
         audio_frame->data[0] = resampled_data[0];
-        if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
+        if (FFmpeg::av_sample_fmt_is_planar(codec_context->sample_fmt)) {
             audio_frame->data[1] = resampled_data[1];
         }
         audio_frame->pts = frame_count * frame_size;
@@ -601,7 +620,8 @@ void FFmpegAudioStream::ProcessFrame(const VariableAudioFrame& channel0,
         SendFrame(audio_frame.get());
 
         // swr_convert buffers input internally. Try to get more resampled data
-        resampled_count = swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0);
+        resampled_count =
+            FFmpeg::swr_convert(swr_context.get(), resampled_data, frame_size, nullptr, 0);
         if (resampled_count < 0) {
             LOG_ERROR(Render, "Audio frame dropped: Could not resample data");
             return;
@@ -617,7 +637,7 @@ void FFmpegAudioStream::Flush() {
     // Send the last samples
     audio_frame->nb_samples = offset;
     audio_frame->data[0] = resampled_data[0];
-    if (av_sample_fmt_is_planar(codec_context->sample_fmt)) {
+    if (FFmpeg::av_sample_fmt_is_planar(codec_context->sample_fmt)) {
         audio_frame->data[1] = resampled_data[1];
     }
     audio_frame->pts = frame_count * frame_size;
@@ -632,7 +652,6 @@ FFmpegMuxer::~FFmpegMuxer() {
 }
 
 bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout& layout) {
-
     InitializeFFmpegLibraries();
 
     if (!FileUtil::CreateFullPath(path)) {
@@ -641,7 +660,7 @@ bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout&
 
     // Get output format
     const auto format = Settings::values.output_format;
-    auto* output_format = av_guess_format(format.c_str(), path.c_str(), nullptr);
+    auto* output_format = FFmpeg::av_guess_format(format.c_str(), path.c_str(), nullptr);
     if (!output_format) {
         LOG_ERROR(Render, "Could not get format {}", format);
         return false;
@@ -649,9 +668,8 @@ bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout&
 
     // Initialize format context
     auto* format_context_raw = format_context.get();
-    if (avformat_alloc_output_context2(&format_context_raw, output_format, nullptr, path.c_str()) <
-        0) {
-
+    if (FFmpeg::avformat_alloc_output_context2(&format_context_raw, output_format, nullptr,
+                                               path.c_str()) < 0) {
         LOG_ERROR(Render, "Could not allocate output context");
         return false;
     }
@@ -664,15 +682,15 @@ bool FFmpegMuxer::Init(const std::string& path, const Layout::FramebufferLayout&
 
     AVDictionary* options = ToAVDictionary(Settings::values.format_options);
     // Open video file
-    if (avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
-        avformat_write_header(format_context.get(), &options)) {
+    if (FFmpeg::avio_open(&format_context->pb, path.c_str(), AVIO_FLAG_WRITE) < 0 ||
+        FFmpeg::avformat_write_header(format_context.get(), &options)) {
 
         LOG_ERROR(Render, "Could not open {}", path);
         return false;
     }
-    if (av_dict_count(options) != 0) { // Successfully set options are removed from the dict
+    if (FFmpeg::av_dict_count(options) != 0) { // Successfully set options are removed from the dict
         char* buf = nullptr;
-        av_dict_get_string(options, &buf, ':', ';');
+        FFmpeg::av_dict_get_string(options, &buf, ':', ';');
         LOG_WARNING(Render, "Format options not found: {}", buf);
     }
 
@@ -705,8 +723,8 @@ void FFmpegMuxer::FlushAudio() {
 
 void FFmpegMuxer::WriteTrailer() {
     std::lock_guard lock{format_context_mutex};
-    av_interleaved_write_frame(format_context.get(), nullptr);
-    av_write_trailer(format_context.get());
+    FFmpeg::av_interleaved_write_frame(format_context.get(), nullptr);
+    FFmpeg::av_write_trailer(format_context.get());
 }
 
 FFmpegBackend::FFmpegBackend() = default;
@@ -722,7 +740,6 @@ FFmpegBackend::~FFmpegBackend() {
 }
 
 bool FFmpegBackend::StartDumping(const std::string& path, const Layout::FramebufferLayout& layout) {
-
     InitializeFFmpegLibraries();
 
     if (!ffmpeg.Init(path, layout)) {
@@ -732,8 +749,9 @@ bool FFmpegBackend::StartDumping(const std::string& path, const Layout::Framebuf
 
     video_layout = layout;
 
-    if (video_processing_thread.joinable())
+    if (video_processing_thread.joinable()) {
         video_processing_thread.join();
+    }
     video_processing_thread = std::thread([&] {
         event1.Set();
         while (true) {
@@ -756,8 +774,9 @@ bool FFmpegBackend::StartDumping(const std::string& path, const Layout::Framebuf
         EndDumping();
     });
 
-    if (audio_processing_thread.joinable())
+    if (audio_processing_thread.joinable()) {
         audio_processing_thread.join();
+    }
     audio_processing_thread = std::thread([&] {
         VariableAudioFrame channel0, channel1;
         while (true) {
@@ -912,16 +931,17 @@ std::string FormatDefaultValue(const AVOption* option,
         return fmt::format("{}", option->default_val.dbl);
     }
     case AV_OPT_TYPE_RATIONAL: {
-        const auto q = av_d2q(option->default_val.dbl, std::numeric_limits<int>::max());
+        const auto q = FFmpeg::av_d2q(option->default_val.dbl, std::numeric_limits<int>::max());
         return fmt::format("{}/{}", q.num, q.den);
     }
     case AV_OPT_TYPE_PIXEL_FMT: {
-        const char* name = av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64));
+        const char* name =
+            FFmpeg::av_get_pix_fmt_name(static_cast<AVPixelFormat>(option->default_val.i64));
         return ToStdString(name, "none");
     }
     case AV_OPT_TYPE_SAMPLE_FMT: {
         const char* name =
-            av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64));
+            FFmpeg::av_get_sample_fmt_name(static_cast<AVSampleFormat>(option->default_val.i64));
         return ToStdString(name, "none");
     }
     case AV_OPT_TYPE_COLOR:
@@ -947,7 +967,7 @@ void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class)
     const AVOption* current = nullptr;
     std::unordered_map<std::string, std::vector<OptionInfo::NamedConstant>> named_constants_map;
     // First iteration: find and place all named constants
-    while ((current = av_opt_next(&av_class, current))) {
+    while ((current = FFmpeg::av_opt_next(&av_class, current))) {
         if (current->type != AV_OPT_TYPE_CONST || !current->unit) {
             continue;
         }
@@ -956,7 +976,7 @@ void GetOptionListSingle(std::vector<OptionInfo>& out, const AVClass* av_class)
     }
     // Second iteration: find all options
     current = nullptr;
-    while ((current = av_opt_next(&av_class, current))) {
+    while ((current = FFmpeg::av_opt_next(&av_class, current))) {
         // Currently we cannot handle binary options
         if (current->type == AV_OPT_TYPE_CONST || current->type == AV_OPT_TYPE_BINARY) {
             continue;
@@ -985,9 +1005,9 @@ void GetOptionList(std::vector<OptionInfo>& out, const AVClass* av_class, bool s
     const AVClass* child_class = nullptr;
 #if LIBAVCODEC_VERSION_MAJOR >= 59
     void* iter = nullptr;
-    while ((child_class = av_opt_child_class_iterate(av_class, &iter))) {
+    while ((child_class = FFmpeg::av_opt_child_class_iterate(av_class, &iter))) {
 #else
-    while ((child_class = av_opt_child_class_next(av_class, child_class))) {
+    while ((child_class = FFmpeg::av_opt_child_class_next(av_class, child_class))) {
 #endif
         GetOptionListSingle(out, child_class);
     }
@@ -1005,13 +1025,9 @@ std::vector<EncoderInfo> ListEncoders(AVMediaType type) {
     std::vector<EncoderInfo> out;
 
     const AVCodec* current = nullptr;
-#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100)
-    while ((current = av_codec_next(current))) {
-#else
     void* data = nullptr; // For libavcodec to save the iteration state
-    while ((current = av_codec_iterate(&data))) {
-#endif
-        if (!av_codec_is_encoder(current) || current->type != type) {
+    while ((current = FFmpeg::av_codec_iterate(&data))) {
+        if (!FFmpeg::av_codec_is_encoder(current) || current->type != type) {
             continue;
         }
         out.push_back({current->name, ToStdString(current->long_name), current->id,
@@ -1021,7 +1037,7 @@ std::vector<EncoderInfo> ListEncoders(AVMediaType type) {
 }
 
 std::vector<OptionInfo> GetEncoderGenericOptions() {
-    return GetOptionList(avcodec_get_class(), false);
+    return GetOptionList(FFmpeg::avcodec_get_class(), false);
 }
 
 std::vector<FormatInfo> ListFormats() {
@@ -1030,20 +1046,16 @@ std::vector<FormatInfo> ListFormats() {
     std::vector<FormatInfo> out;
 
     const AVOutputFormat* current = nullptr;
-#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(58, 9, 100)
-    while ((current = av_oformat_next(current))) {
-#else
     void* data = nullptr; // For libavformat to save the iteration state
-    while ((current = av_muxer_iterate(&data))) {
-#endif
+    while ((current = FFmpeg::av_muxer_iterate(&data))) {
         const auto extensions = Common::SplitString(ToStdString(current->extensions), ',');
 
         std::set<AVCodecID> supported_video_codecs;
         std::set<AVCodecID> supported_audio_codecs;
         // Go through all codecs
         const AVCodecDescriptor* codec = nullptr;
-        while ((codec = avcodec_descriptor_next(codec))) {
-            if (avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) {
+        while ((codec = FFmpeg::avcodec_descriptor_next(codec))) {
+            if (FFmpeg::avformat_query_codec(current, codec->id, FF_COMPLIANCE_NORMAL) == 1) {
                 if (codec->type == AVMEDIA_TYPE_VIDEO) {
                     supported_video_codecs.emplace(codec->id);
                 } else if (codec->type == AVMEDIA_TYPE_AUDIO) {
@@ -1064,7 +1076,24 @@ std::vector<FormatInfo> ListFormats() {
 }
 
 std::vector<OptionInfo> GetFormatGenericOptions() {
-    return GetOptionList(avformat_get_class(), false);
+    return GetOptionList(FFmpeg::avformat_get_class(), false);
+}
+
+std::vector<std::string> GetPixelFormats() {
+    std::vector<std::string> out;
+    const AVPixFmtDescriptor* current = nullptr;
+    while ((current = FFmpeg::av_pix_fmt_desc_next(current))) {
+        out.emplace_back(current->name);
+    }
+    return out;
+}
+
+std::vector<std::string> GetSampleFormats() {
+    std::vector<std::string> out;
+    for (int current = AV_SAMPLE_FMT_U8; current < AV_SAMPLE_FMT_NB; current++) {
+        out.emplace_back(FFmpeg::av_get_sample_fmt_name(static_cast<AVSampleFormat>(current)));
+    }
+    return out;
 }
 
 } // namespace VideoDumper
diff --git a/src/core/dumping/ffmpeg_backend.h b/src/core/dumping/ffmpeg_backend.h
index f7ba53ebb..115731f67 100644
--- a/src/core/dumping/ffmpeg_backend.h
+++ b/src/core/dumping/ffmpeg_backend.h
@@ -13,24 +13,15 @@
 #include <thread>
 #include <vector>
 #include "common/common_types.h"
+#include "common/dynamic_library/ffmpeg.h"
 #include "common/thread.h"
 #include "common/threadsafe_queue.h"
 #include "core/dumping/backend.h"
 
-extern "C" {
-#include <libavcodec/avcodec.h>
-#include <libavfilter/avfilter.h>
-#include <libavformat/avformat.h>
-#include <libavutil/opt.h>
-#include <libswresample/swresample.h>
-}
-
 namespace VideoDumper {
 
 using VariableAudioFrame = std::vector<s16>;
 
-void InitFFmpegLibraries();
-
 class FFmpegMuxer;
 
 /**
@@ -51,13 +42,13 @@ protected:
 
     struct AVCodecContextDeleter {
         void operator()(AVCodecContext* codec_context) const {
-            avcodec_free_context(&codec_context);
+            DynamicLibrary::FFmpeg::avcodec_free_context(&codec_context);
         }
     };
 
     struct AVFrameDeleter {
         void operator()(AVFrame* frame) const {
-            av_frame_free(&frame);
+            DynamicLibrary::FFmpeg::av_frame_free(&frame);
         }
     };
 
@@ -103,7 +94,7 @@ private:
     // Filter related
     struct AVFilterGraphDeleter {
         void operator()(AVFilterGraph* filter_graph) const {
-            avfilter_graph_free(&filter_graph);
+            DynamicLibrary::FFmpeg::avfilter_graph_free(&filter_graph);
         }
     };
     std::unique_ptr<AVFilterGraph, AVFilterGraphDeleter> filter_graph{};
@@ -132,7 +123,7 @@ public:
 private:
     struct SwrContextDeleter {
         void operator()(SwrContext* swr_context) const {
-            swr_free(&swr_context);
+            DynamicLibrary::FFmpeg::swr_free(&swr_context);
         }
     };
 
@@ -165,8 +156,8 @@ public:
 private:
     struct AVFormatContextDeleter {
         void operator()(AVFormatContext* format_context) const {
-            avio_closep(&format_context->pb);
-            avformat_free_context(format_context);
+            DynamicLibrary::FFmpeg::avio_closep(&format_context->pb);
+            DynamicLibrary::FFmpeg::avformat_free_context(format_context);
         }
     };
 
@@ -253,5 +244,7 @@ std::vector<EncoderInfo> ListEncoders(AVMediaType type);
 std::vector<OptionInfo> GetEncoderGenericOptions();
 std::vector<FormatInfo> ListFormats();
 std::vector<OptionInfo> GetFormatGenericOptions();
+std::vector<std::string> GetPixelFormats();
+std::vector<std::string> GetSampleFormats();
 
 } // namespace VideoDumper
diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp
index 3122de2f7..1d4ac8c32 100644
--- a/src/video_core/renderer_opengl/frame_dumper_opengl.cpp
+++ b/src/video_core/renderer_opengl/frame_dumper_opengl.cpp
@@ -3,32 +3,37 @@
 // Refer to the license.txt file included.
 
 #include <glad/glad.h>
+
+#include <utility>
 #include "core/frontend/emu_window.h"
 #include "video_core/renderer_opengl/frame_dumper_opengl.h"
 #include "video_core/renderer_opengl/renderer_opengl.h"
 
 namespace OpenGL {
 
-FrameDumperOpenGL::FrameDumperOpenGL(VideoDumper::Backend& video_dumper_,
-                                     Frontend::EmuWindow& emu_window)
-    : video_dumper(video_dumper_), context(emu_window.CreateSharedContext()) {}
+FrameDumperOpenGL::FrameDumperOpenGL(Core::System& system_, Frontend::EmuWindow& emu_window)
+    : system(system_), context(emu_window.CreateSharedContext()) {}
 
 FrameDumperOpenGL::~FrameDumperOpenGL() {
-    if (present_thread.joinable())
+    if (present_thread.joinable()) {
         present_thread.join();
+    }
 }
 
 bool FrameDumperOpenGL::IsDumping() const {
-    return video_dumper.IsDumping();
+    auto video_dumper = system.GetVideoDumper();
+    return video_dumper && video_dumper->IsDumping();
 }
 
 Layout::FramebufferLayout FrameDumperOpenGL::GetLayout() const {
-    return video_dumper.GetLayout();
+    auto video_dumper = system.GetVideoDumper();
+    return video_dumper ? video_dumper->GetLayout() : Layout::FramebufferLayout{};
 }
 
 void FrameDumperOpenGL::StartDumping() {
-    if (present_thread.joinable())
+    if (present_thread.joinable()) {
         present_thread.join();
+    }
 
     present_thread = std::thread(&FrameDumperOpenGL::PresentLoop, this);
 }
@@ -62,13 +67,17 @@ void FrameDumperOpenGL::PresentLoop() {
         frame->present_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
         glFlush();
 
-        // Bind the previous PBO and read the pixels
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle);
-        GLubyte* pixels = static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
-        VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
-        video_dumper.AddVideoFrame(std::move(frame_data));
-        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
-        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+        auto video_dumper = system.GetVideoDumper();
+        if (video_dumper) {
+            // Bind the previous PBO and read the pixels
+            glBindBuffer(GL_PIXEL_PACK_BUFFER, pbos[next_pbo].handle);
+            GLubyte* pixels =
+                static_cast<GLubyte*>(glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY));
+            VideoDumper::VideoFrame frame_data{layout.width, layout.height, pixels};
+            video_dumper->AddVideoFrame(std::move(frame_data));
+            glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
+            glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+        }
 
         current_pbo = (current_pbo + 1) % 2;
         next_pbo = (current_pbo + 1) % 2;
diff --git a/src/video_core/renderer_opengl/frame_dumper_opengl.h b/src/video_core/renderer_opengl/frame_dumper_opengl.h
index da6d96053..bcd498094 100644
--- a/src/video_core/renderer_opengl/frame_dumper_opengl.h
+++ b/src/video_core/renderer_opengl/frame_dumper_opengl.h
@@ -7,6 +7,7 @@
 #include <atomic>
 #include <memory>
 #include <thread>
+#include "core/core.h"
 #include "core/dumping/backend.h"
 #include "core/frontend/framebuffer_layout.h"
 #include "video_core/renderer_opengl/gl_resource_manager.h"
@@ -28,7 +29,7 @@ class RendererOpenGL;
  */
 class FrameDumperOpenGL {
 public:
-    explicit FrameDumperOpenGL(VideoDumper::Backend& video_dumper, Frontend::EmuWindow& emu_window);
+    explicit FrameDumperOpenGL(Core::System& system, Frontend::EmuWindow& emu_window);
     ~FrameDumperOpenGL();
 
     bool IsDumping() const;
@@ -43,7 +44,7 @@ private:
     void CleanupOpenGLObjects();
     void PresentLoop();
 
-    VideoDumper::Backend& video_dumper;
+    Core::System& system;
     std::unique_ptr<Frontend::GraphicsContext> context;
     std::thread present_thread;
     std::atomic_bool stop_requested{false};
diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp
index 89ac5fc49..bc897d73f 100644
--- a/src/video_core/renderer_opengl/renderer_opengl.cpp
+++ b/src/video_core/renderer_opengl/renderer_opengl.cpp
@@ -306,7 +306,7 @@ static std::array<GLfloat, 3 * 2> MakeOrthographicMatrix(const float width, cons
 RendererOpenGL::RendererOpenGL(Core::System& system, Frontend::EmuWindow& window,
                                Frontend::EmuWindow* secondary_window)
     : VideoCore::RendererBase{system, window, secondary_window}, driver{system.TelemetrySession()},
-      frame_dumper{system.VideoDumper(), window} {
+      frame_dumper{system, window} {
     const bool has_debug_tool = driver.HasDebugTool();
     window.mailbox = std::make_unique<OGLTextureMailbox>(has_debug_tool);
     if (secondary_window) {