diff --git a/Makefile b/Makefile index 1bacf2f..bbe5a19 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ FFMPEG_CONFIG := --enable-asm \ --disable-muxers \ --target-os=horizon --enable-cross-compile \ --cross-prefix=aarch64-none-elf- --arch=aarch64 --cpu=cortex-a57 --enable-neon \ + --enable-mbedtls --enable-version3 \ --enable-pic --disable-autodetect --disable-runtime-cpudetect --disable-debug MPV_CONFIG := --enable-libmpv-static --disable-libmpv-shared --disable-manpage-build \ @@ -37,7 +38,7 @@ BUILD := build ROMFS := $(BUILD)/romfs INSTALL := $(TOPDIR)/$(BUILD)/install PACKAGES := mpv libavcodec libavformat libavfilter libavutil libswscale libswresample \ - uam libsmb2 libnfs libssh2 freetype2 libarchive + uam libsmb2 libnfs libssh2 libcurl freetype2 libarchive LIBDIRS := $(INSTALL) DEFINES := __SWITCH__ _GNU_SOURCE _POSIX_VERSION=200809L timegm=mktime \ diff --git a/README.md b/README.md index d50e0ab..88c6114 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A hardware-accelerated media player for the Nintendo Switch, built on mpv and FF - Direct rendering (faster software decoding) - Custom post-processing shaders - Custom audio backend for mpv using native Nintendo APIs, supporting layouts up to 5.1 surround -- Network playback through Samba, NFS or SFTP +- Network playback through HTTP/S, Samba, NFS or SFTP - External drive support using [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs) - Rich and responsive user interface, even under load @@ -47,7 +47,7 @@ GIMP_VERSION=3 ./build-docker.sh ### Manual - Set up a [devkitpro](https://devkitpro.org/wiki/devkitPro_pacman) environment for Switch homebrew development. -- Install the following packages: `switch-bzip2`, `switch-dav1d`, `switch-freetype`, `switch-glm`, `switch-harfbuzz`, `switch-libarchive`, `switch-libass`, `switch-libfribidi`, `switch-libjpeg-turbo`, `switch-libpng`, `switch-libwebp`, `switch-libssh2`, `switch-mbedtls`, `switch-ntfs-3g` and `switch-lwext4`. In addition, the following build dependencies are required: `switch-pkg-config`, `dkp-meson-scripts`, `dkp-toolchain-vars`, and [GIMP](https://www.gimp.org/) (2 or 3). +- Install the following packages: `switch-bzip2`, `switch-dav1d`, `switch-freetype`, `switch-glm`, `switch-harfbuzz`, `switch-libarchive`, `switch-libass`, `switch-libfribidi`, `switch-libjpeg-turbo`, `switch-libpng`, `switch-libwebp`, `switch-curl`, `switch-libssh2`, `switch-mbedtls`, `switch-ntfs-3g` and `switch-lwext4`. In addition, the following build dependencies are required: `switch-pkg-config`, `dkp-meson-scripts`, `dkp-toolchain-vars`, and [GIMP](https://www.gimp.org/) (2 or 3). - Compile and install a GPL build of [libusbhsfs](https://github.com/DarkMatterCore/libusbhsfs). - Compile and install [libsmb2](misc/libsmb2/) and [libnfs](misc/libnfs/). - Configure, compile and install FFmpeg: `make configure-ffmpeg && make build-ffmpeg -j$(nproc)`. diff --git a/src/context.cpp b/src/context.cpp index 4f0bbe8..49f7193 100644 --- a/src/context.cpp +++ b/src/context.cpp @@ -25,6 +25,7 @@ #include "fs/fs_smb.hpp" #include "fs/fs_nfs.hpp" #include "fs/fs_sftp.hpp" +#include "fs/fs_http.hpp" #include "context.hpp" @@ -70,7 +71,9 @@ int Context::read_from_file() { if (n == "protocol") info->protocol = (v == "smb" ? fs::NetworkFilesystem::Protocol::Smb : - (v == "nfs" ? fs::NetworkFilesystem::Protocol::Nfs : fs::NetworkFilesystem::Protocol::Sftp)); + (v == "nfs" ? fs::NetworkFilesystem::Protocol::Nfs : + (v == "http" ? fs::NetworkFilesystem::Protocol::Http : + (v == "https" ? fs::NetworkFilesystem::Protocol::Https : fs::NetworkFilesystem::Protocol::Sftp)))); else if (n == "connect") info->want_connect = v != "no"; else if (n == "share") @@ -152,10 +155,16 @@ int Context::register_network_fs(NetworkFsInfo &info) { case fs::NetworkFilesystem::Protocol::Sftp: fs = std::make_shared(*this, info.fs_name, info.mountpoint); break; + case fs::NetworkFilesystem::Protocol::Http: + case fs::NetworkFilesystem::Protocol::Https: + fs = std::make_shared(*this, info.fs_name, info.mountpoint); + break; default: return -1; } + fs->protocol = info.protocol; + if (auto rc = fs->initialize(); rc) return rc; diff --git a/src/fs/fs_common.hpp b/src/fs/fs_common.hpp index f9f9103..d3d93af 100644 --- a/src/fs/fs_common.hpp +++ b/src/fs/fs_common.hpp @@ -177,6 +177,8 @@ class NetworkFilesystem: public Filesystem { Smb, Nfs, Sftp, + Http, + Https, ProtocolMax, }; @@ -201,9 +203,16 @@ class NetworkFilesystem: public Filesystem { return "nfs"; case Protocol::Sftp: return "sftp"; + case Protocol::Http: + return "http"; + case Protocol::Https: + return "https"; } } + public: + Protocol protocol = Protocol::Smb; + protected: bool is_connected = false; }; diff --git a/src/fs/fs_http.cpp b/src/fs/fs_http.cpp new file mode 100644 index 0000000..df72359 --- /dev/null +++ b/src/fs/fs_http.cpp @@ -0,0 +1,442 @@ +// Copyright (c) 2024 averne +// +// This file is part of SwitchWave. +// +// SwitchWave is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// SwitchWave is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SwitchWave. If not, see . + +#include +#include +#include +#include +#include +#include + +#include + +#include "fs/fs_http.hpp" + +namespace sw::fs { + +namespace { + +std::size_t string_write_cb(char *ptr, std::size_t size, std::size_t nmemb, void *userdata) { + auto *str = static_cast(userdata); + auto total = size * nmemb; + str->append(ptr, total); + return total; +} + +std::string url_decode(std::string_view s) { + std::string result; + result.reserve(s.size()); + + for (std::size_t i = 0; i < s.size(); ++i) { + if (s[i] == '%' && i + 2 < s.size()) { + char hex[3] = { s[i+1], s[i+2], '\0' }; + char *end; + auto val = std::strtoul(hex, &end, 16); + if (end == hex + 2) { + result += static_cast(val); + i += 2; + continue; + } + } + result += s[i]; + } + + return result; +} + +std::string url_encode_path(std::string_view s) { + std::string result; + result.reserve(s.size()); + + for (auto c: s) { + if (c == '/' || std::isalnum(static_cast(c)) || + c == '-' || c == '_' || c == '.' || c == '~') + result += c; + else { + char buf[4]; + std::snprintf(buf, sizeof(buf), "%%%02X", static_cast(c)); + result += buf; + } + } + + return result; +} + +void parse_autoindex(std::string_view html, std::vector &entries) { + // Narrow search to or if present + auto body = html; + if (auto pos = html.find(" tags + std::string_view needle = "href=\""; + std::size_t search_pos = 0; + + while (search_pos < body.size()) { + auto href_start = body.find(needle, search_pos); + if (href_start == std::string_view::npos) + break; + + href_start += needle.size(); + auto href_end = body.find('"', href_start); + if (href_end == std::string_view::npos) + break; + + search_pos = href_end + 1; + + auto href = body.substr(href_start, href_end - href_start); + + // Skip parent directory links + if (href == "../" || href == "..") + continue; + + // Skip external links (absolute URLs) + if (href.find("://") != std::string_view::npos) + continue; + + // Skip query/anchor-only links + if (!href.empty() && (href[0] == '?' || href[0] == '#')) + continue; + + // Skip absolute paths that go to parent + if (!href.empty() && href[0] == '/') + continue; + + auto decoded = url_decode(href); + bool is_dir = !decoded.empty() && decoded.back() == '/'; + + // Remove trailing slash for directory names + if (is_dir && decoded.size() > 1) + decoded.pop_back(); + + if (!decoded.empty()) + entries.push_back({ std::move(decoded), is_dir }); + } +} + +} // namespace + +HttpFs::HttpFs(Context &context, std::string_view name, std::string_view mount_name): context(context) { + this->type = Filesystem::Type::Network; + this->name = name; + this->mount_name = mount_name; + + this->devoptab = { + .name = this->name.data(), + + .structSize = 0, + .open_r = HttpFs::http_open, + .close_r = HttpFs::http_close, + .read_r = HttpFs::http_read, + .seek_r = HttpFs::http_seek, + .fstat_r = HttpFs::http_fstat, + + .stat_r = HttpFs::http_stat, + + .dirStateSize = sizeof(HttpFsDir), + .diropen_r = HttpFs::http_diropen, + .dirreset_r = HttpFs::http_dirreset, + .dirnext_r = HttpFs::http_dirnext, + .dirclose_r = HttpFs::http_dirclose, + + .deviceData = this, + + .lstat_r = HttpFs::http_lstat, + }; +} + +HttpFs::~HttpFs() { + if (this->is_connected) + this->disconnect(); + + this->unregister_fs(); +} + +int HttpFs::initialize() { + if (auto rc = ::curl_global_init(CURL_GLOBAL_DEFAULT); rc) + return EIO; + + return 0; +} + +int HttpFs::connect(std::string_view host, std::uint16_t port, std::string_view share, + std::string_view username, std::string_view password) { + bool is_https = (this->protocol == Protocol::Https); + auto scheme = is_https ? "https://" : "http://"; + auto default_port = is_https ? std::uint16_t(443) : std::uint16_t(80); + + // Build host[:port] string + auto host_port = std::string(host); + if (port && port != default_port) { + host_port += ':'; + host_port += std::to_string(port); + } + + while (!share.empty() && share.front() == '/') + share.remove_prefix(1); + while (!share.empty() && share.back() == '/') + share.remove_suffix(1); + + // Build base URL: http(s)://host:port[/share] + auto base = Path(std::string(scheme) + host_port); + if (!share.empty()) + base /= Path(std::string(share)); + this->base_url = base.base(); + + // Store credentials for curl operations + this->userpwd.clear(); + if (!username.empty()) { + this->userpwd = username; + this->userpwd += ':'; + this->userpwd += password; + } + + // Build auth URL prefix with embedded credentials for make_url() + std::string auth_authority = scheme; + if (!username.empty()) { + auth_authority += username; + auth_authority += ':'; + auth_authority += password; + auth_authority += '@'; + } + auth_authority += host_port; + + auto auth_base = Path(std::move(auth_authority)); + if (!share.empty()) + auth_base /= Path(std::string(share)); + this->auth_url_prefix = auth_base.base(); + + // Test connection with HEAD request + auto *curl = ::curl_easy_init(); + if (!curl) + return ENOMEM; + + this->setup_curl_handle(curl); + ::curl_easy_setopt(curl, CURLOPT_URL, this->base_url.c_str()); + ::curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + ::curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L); + + auto res = ::curl_easy_perform(curl); + ::curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + std::printf("HTTP connect failed: %s\n", ::curl_easy_strerror(res)); + return ECONNREFUSED; + } + + auto lk = std::scoped_lock(this->session_mutex); + this->is_connected = true; + + return 0; +} + +int HttpFs::disconnect() { + auto lk = std::scoped_lock(this->session_mutex); + + this->is_connected = false; + + ::curl_global_cleanup(); + + return 0; +} + +void HttpFs::setup_curl_handle(void *handle) { + auto *curl = static_cast(handle); + + ::curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + ::curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + ::curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); + ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + ::curl_easy_setopt(curl, CURLOPT_USERAGENT, "SwitchWave/1.0"); + + if (!this->userpwd.empty()) { + ::curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + ::curl_easy_setopt(curl, CURLOPT_USERPWD, this->userpwd.c_str()); + } +} + +std::string HttpFs::translate_path(const char *path) { + return this->cwd + (path + this->mount_name.length()); +} + +std::string HttpFs::make_url(std::string_view path) const { + auto internal = Path::internal(path); + return this->auth_url_prefix + url_encode_path(internal); +} + +// File operations: not supported, files are accessed via direct HTTP URL +int HttpFs::http_open(struct _reent *r, void *fileStruct, const char *path, int flags, int mode) { + __errno_r(r) = ENOSYS; + return -1; +} + +int HttpFs::http_close(struct _reent *r, void *fd) { + __errno_r(r) = ENOSYS; + return -1; +} + +ssize_t HttpFs::http_read(struct _reent *r, void *fd, char *ptr, size_t len) { + __errno_r(r) = ENOSYS; + return -1; +} + +off_t HttpFs::http_seek(struct _reent *r, void *fd, off_t pos, int dir) { + __errno_r(r) = ENOSYS; + return -1; +} + +int HttpFs::http_fstat(struct _reent *r, void *fd, struct stat *st) { + __errno_r(r) = ENOSYS; + return -1; +} + +int HttpFs::http_stat(struct _reent *r, const char *file, struct stat *st) { + auto *priv = static_cast(r->deviceData); + + auto internal_path = priv->translate_path(file); + auto url = priv->base_url + url_encode_path(internal_path); + + auto lk = std::scoped_lock(priv->session_mutex); + + auto *curl = ::curl_easy_init(); + if (!curl) { + __errno_r(r) = ENOMEM; + return -1; + } + + priv->setup_curl_handle(curl); + ::curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + ::curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + + auto res = ::curl_easy_perform(curl); + if (res != CURLE_OK) { + ::curl_easy_cleanup(curl); + __errno_r(r) = ENOENT; + return -1; + } + + long http_code = 0; + ::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + *st = {}; + + int ret = 0; + switch (http_code) { + case 200: { + curl_off_t cl = -1; + ::curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl); + st->st_size = (cl >= 0) ? cl : 0; + st->st_mode = S_IFREG; + break; + } + case 301: + case 302: + st->st_mode = S_IFDIR; + break; + case 404: + __errno_r(r) = ENOENT, ret = -1; + break; + case 403: + __errno_r(r) = EACCES, ret = -1; + break; + default: + __errno_r(r) = EIO, ret = -1; + break; + } + + ::curl_easy_cleanup(curl); + return ret; +} + +int HttpFs::http_lstat(struct _reent *r, const char *file, struct stat *st) { + return HttpFs::http_stat(r, file, st); +} + +DIR_ITER *HttpFs::http_diropen(struct _reent *r, DIR_ITER *dirState, const char *path) { + auto *priv = static_cast(r->deviceData); + auto *priv_dir = std::construct_at(reinterpret_cast(dirState->dirStruct)); + priv_dir->index = 0; + + auto internal_path = priv->translate_path(path); + auto url = priv->base_url + url_encode_path(internal_path); + if (url.back() != '/') + url += '/'; + + auto lk = std::scoped_lock(priv->session_mutex); + + auto *curl = ::curl_easy_init(); + if (!curl) { + std::destroy_at(priv_dir); + __errno_r(r) = ENOMEM; + return nullptr; + } + + priv->setup_curl_handle(curl); + + std::string html; + ::curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, string_write_cb); + ::curl_easy_setopt(curl, CURLOPT_WRITEDATA, &html); + + auto res = ::curl_easy_perform(curl); + ::curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + std::destroy_at(priv_dir); + __errno_r(r) = EIO; + return nullptr; + } + + parse_autoindex(html, priv_dir->entries); + + return dirState; +} + +int HttpFs::http_dirreset(struct _reent *r, DIR_ITER *dirState) { + auto *priv_dir = static_cast(dirState->dirStruct); + priv_dir->index = 0; + + return 0; +} + +int HttpFs::http_dirnext(struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat) { + auto *priv_dir = static_cast(dirState->dirStruct); + + if (priv_dir->index >= priv_dir->entries.size()) { + __errno_r(r) = ENOENT; + return -1; + } + + auto &entry = priv_dir->entries[priv_dir->index++]; + std::strncpy(filename, entry.href.c_str(), NAME_MAX); + + *filestat = {}; + filestat->st_mode = entry.is_dir ? S_IFDIR : S_IFREG; + + return 0; +} + +int HttpFs::http_dirclose(struct _reent *r, DIR_ITER *dirState) { + auto *priv_dir = reinterpret_cast(dirState->dirStruct); + std::destroy_at(priv_dir); + + return 0; +} + +} // namespace sw::fs diff --git a/src/fs/fs_http.hpp b/src/fs/fs_http.hpp new file mode 100644 index 0000000..97d2591 --- /dev/null +++ b/src/fs/fs_http.hpp @@ -0,0 +1,80 @@ +// Copyright (c) 2024 averne +// +// This file is part of SwitchWave. +// +// SwitchWave is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// SwitchWave is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SwitchWave. If not, see . + +#pragma once + +#include +#include +#include +#include + +#include "context.hpp" +#include "fs/fs_common.hpp" + +namespace sw::fs { + +class HttpFs final: public NetworkFilesystem { + public: + HttpFs(Context &context, std::string_view name, std::string_view mount_name); + virtual ~HttpFs() override; + + virtual int initialize() override; + virtual int connect(std::string_view host, std::uint16_t port, std::string_view share, + std::string_view username, std::string_view password) override; + virtual int disconnect() override; + + std::string make_url(std::string_view path) const; + + struct DirEntry { + std::string href; + bool is_dir; + }; + + private: + std::string translate_path(const char *path); + void setup_curl_handle(void *curl); + + static int http_open (struct _reent *r, void *fileStruct, const char *path, int flags, int mode); + static int http_close (struct _reent *r, void *fd); + static ssize_t http_read (struct _reent *r, void *fd, char *ptr, size_t len); + static off_t http_seek (struct _reent *r, void *fd, off_t pos, int dir); + static int http_fstat (struct _reent *r, void *fd, struct stat *st); + static int http_stat (struct _reent *r, const char *file, struct stat *st); + static DIR_ITER *http_diropen (struct _reent *r, DIR_ITER *dirState, const char *path); + static int http_dirreset(struct _reent *r, DIR_ITER *dirState); + static int http_dirnext (struct _reent *r, DIR_ITER *dirState, char *filename, struct stat *filestat); + static int http_dirclose(struct _reent *r, DIR_ITER *dirState); + static int http_lstat (struct _reent *r, const char *file, struct stat *st); + + struct HttpFsDir { + std::vector entries; + std::size_t index; + }; + + private: + Context &context; + + std::string base_url; + std::string userpwd; + std::string auth_url_prefix; + + std::string cwd = ""; + + std::mutex session_mutex; +}; + +} // namespace sw::fs diff --git a/src/libmpv.cpp b/src/libmpv.cpp index 2f1fd48..e095511 100644 --- a/src/libmpv.cpp +++ b/src/libmpv.cpp @@ -31,6 +31,7 @@ int LibmpvController::initialize() { MPV_CALL(mpv_set_option_string(this->mpv, "config", "yes")); MPV_CALL(mpv_set_option_string(this->mpv, "config-dir", LibmpvController::MpvDirectory.data())); + MPV_CALL(mpv_set_option_string(this->mpv, "user-agent", "SwitchWave/1.0")); MPV_CALL(mpv_initialize(this->mpv)); diff --git a/src/main.cpp b/src/main.cpp index 7b52bad..3c79943 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,6 +36,7 @@ #include "fs/fs_common.hpp" #include "fs/fs_ums.hpp" #include "fs/fs_recent.hpp" +#include "fs/fs_http.hpp" using namespace std::chrono_literals; @@ -240,7 +241,25 @@ int video_loop(sw::Renderer &renderer, sw::Context &context) { auto lk = std::scoped_lock(g_setup_mtx); - lmpv.command("loadfile", context.cur_file.c_str()); + // For HTTP filesystems, pass the full HTTP URL directly to mpv + std::string loadfile_path = context.cur_file; + bool use_http_basic_preemptive_auth = false; + if (auto *fs = context.get_filesystem(sw::fs::Path::mountpoint(context.cur_file)); + fs && fs->type == sw::fs::Filesystem::Type::Network) { + auto *net_fs = static_cast(fs); + if (net_fs->protocol == sw::fs::NetworkFilesystem::Protocol::Http || + net_fs->protocol == sw::fs::NetworkFilesystem::Protocol::Https) { + loadfile_path = static_cast(net_fs)->make_url(context.cur_file); + use_http_basic_preemptive_auth = true; + } + } + + if (use_http_basic_preemptive_auth) { + lmpv.command("loadfile", loadfile_path.c_str(), "replace", + "demuxer-lavf-o-add=auth_type=basic,stream-lavf-o-add=auth_type=basic"); + } else { + lmpv.command("loadfile", loadfile_path.c_str()); + } auto player_ui = std::make_unique(renderer, context, lmpv); diff --git a/src/ui/ui_main_menu.cpp b/src/ui/ui_main_menu.cpp index af80c95..e51f08b 100644 --- a/src/ui/ui_main_menu.cpp +++ b/src/ui/ui_main_menu.cpp @@ -42,6 +42,7 @@ extern "C" { #include "utils.hpp" #include "fs/fs_recent.hpp" +#include "fs/fs_http.hpp" #include "ui/ui_main_menu.hpp" @@ -234,15 +235,33 @@ void MediaExplorer::metadata_thread_fn(std::stop_token token) { MediaMetadata media_info = {}; if (entry) { + auto entry_path = Explorer::path_from_entry_name(entry->name); + + // For HTTP filesystems, pass the URL directly to avformat (ffmpeg supports HTTP natively) + std::string path; + AVDictionary *format_opts = nullptr; + SW_SCOPEGUARD([&format_opts] { av_dict_free(&format_opts); }); + if (auto *fs_ptr = this->context.get_filesystem(fs::Path::mountpoint(entry_path)); + fs_ptr && fs_ptr->type == fs::Filesystem::Type::Network) { + auto *net_fs = static_cast(fs_ptr); + if (net_fs->protocol == fs::NetworkFilesystem::Protocol::Http || + net_fs->protocol == fs::NetworkFilesystem::Protocol::Https) { + path = static_cast(net_fs)->make_url(entry_path); + av_dict_set(&format_opts, "auth_type", "basic", 0); + av_dict_set(&format_opts, "user_agent", "SwitchWave/1.0", 0); + } + } + // Add explicit protocol prefix, otherwise ffmpeg confuses the mountpoint for a protocol - auto path = std::string("file:") + Explorer::path_from_entry_name(entry->name).data(); + if (path.empty()) + path = std::string("file:") + entry_path.data(); auto *avformat_ctx = avformat_alloc_context(); SW_SCOPEGUARD([&avformat_ctx] { avformat_close_input(&avformat_ctx); }); if (!avformat_ctx) goto end; - if (auto rc = avformat_open_input(&avformat_ctx, path.c_str(), nullptr, nullptr); rc) { + if (auto rc = avformat_open_input(&avformat_ctx, path.c_str(), nullptr, &format_opts); rc) { char buf[AV_ERROR_MAX_STRING_SIZE]; std::printf("Failed to open input %s: %s\n", path.c_str(), av_make_error_string(buf, sizeof(buf), rc)); this->context.set_error(rc, Context::ErrorType::LibAv); @@ -357,14 +376,15 @@ void MediaExplorer::render() { return; auto &entry = this->explorer.entries[ent_idx]; - if (entry.type == fs::Node::Type::Directory) - return; ImGui::NewLine(); auto fname = Explorer::filename_from_entry_name(entry.name); ImGui::TextWrapped("Name: %.*s", int(fname.length()), fname.data()); + if (entry.type == fs::Node::Type::Directory) + return; + auto [size, suffix] = utils::to_human_size(entry.size); ImGui::Text("Size: %.2f%s", size, suffix.data());