From 02567c22655bd478406a8d05d6ee4909e359e0b7 Mon Sep 17 00:00:00 2001 From: arch1t3cht Date: Sun, 16 Jul 2023 17:52:21 +0200 Subject: [PATCH] Rework the audio/video provider system This became necessary now that more providers were added. Providers can be proritized for certain file types (e.g. .vpy files will always be opened with VapourSynth), and when the default provider fails on a file, the user will be notified and be asked to pick an alternative provider. --- libaegisub/include/libaegisub/fs.h | 2 + src/audio_provider_factory.cpp | 99 +++++++++++++++++++----------- src/audio_provider_vs.cpp | 4 +- src/factory_manager.h | 19 ++++++ src/project.cpp | 4 +- src/video_provider_manager.cpp | 67 ++++++++++++++++---- src/video_provider_vs.cpp | 6 +- 7 files changed, 144 insertions(+), 57 deletions(-) diff --git a/libaegisub/include/libaegisub/fs.h b/libaegisub/include/libaegisub/fs.h index 6b3e212ce..1dc1f0476 100644 --- a/libaegisub/include/libaegisub/fs.h +++ b/libaegisub/include/libaegisub/fs.h @@ -24,6 +24,8 @@ #include #include +#pragma once + #undef CreateDirectory namespace agi { diff --git a/src/audio_provider_factory.cpp b/src/audio_provider_factory.cpp index d2597cee4..d5904a532 100644 --- a/src/audio_provider_factory.cpp +++ b/src/audio_provider_factory.cpp @@ -16,11 +16,13 @@ #include "audio_provider_factory.h" +#include "compat.h" #include "factory_manager.h" #include "options.h" #include "utils.h" #include +#include #include #include #include @@ -39,6 +41,7 @@ struct factory { const char *name; std::unique_ptr (*create)(fs::path const&, BackgroundRunner *); bool hidden; + std::function wants_to_open = [](auto p) { return false; }; }; const factory providers[] = { @@ -48,13 +51,13 @@ const factory providers[] = { {"FFmpegSource", CreateFFmpegSourceAudioProvider, false}, #endif #ifdef WITH_AVISYNTH - {"Avisynth", CreateAvisynthAudioProvider, false}, + {"Avisynth", CreateAvisynthAudioProvider, false, [](auto p) { return agi::fs::HasExtension(p, "avs"); }}, #endif #ifdef WITH_BESTSOURCE {"BestSource", CreateBSAudioProvider, false}, #endif #ifdef WITH_VAPOURSYNTH - {"VapourSynth", CreateVapourSynthAudioProvider, false}, + {"VapourSynth", CreateVapourSynthAudioProvider, false, [](auto p) { return agi::fs::HasExtension(p, "py") || agi::fs::HasExtension(p, "vpy"); }}, #endif }; } @@ -63,52 +66,76 @@ std::vector GetAudioProviderNames() { return ::GetClasses(boost::make_iterator_range(std::begin(providers), std::end(providers))); } -std::unique_ptr GetAudioProvider(fs::path const& filename, - Path const& path_helper, - BackgroundRunner *br) { +std::unique_ptr SelectAudioProvider(fs::path const& filename, + Path const& path_helper, + BackgroundRunner *br) { auto preferred = OPT_GET("Audio/Provider")->GetString(); auto sorted = GetSorted(boost::make_iterator_range(std::begin(providers), std::end(providers)), preferred); - std::unique_ptr provider; - bool found_file = false; - bool found_audio = false; - std::string msg_all; // error messages from all attempted providers - std::string msg_partial; // error messages from providers that could partially load the file (knows container, missing codec) + RearrangeWithPriority(sorted, filename); - for (auto const& factory : sorted) { + bool found_file = false; + std::string errors; + + auto tried_providers = sorted.begin(); + + for (; tried_providers < sorted.end(); tried_providers++) { + auto factory = *tried_providers; + std::string err; try { - provider = factory->create(filename, br); + auto provider = factory->create(filename, br); if (!provider) continue; LOG_I("audio_provider") << "Using audio provider: " << factory->name; + return provider; + } + catch (AudioDataNotFound const& ex) { + found_file = true; + err = ex.GetMessage(); + } + catch (AudioProviderError const& ex) { + found_file = true; + err = ex.GetMessage(); + } + + errors += std::string(factory->name) + ": " + err + "\n"; + LOG_D("audio_provider") << factory->name << ": " << err; + if (factory->name == preferred) break; - } - catch (fs::FileNotFound const& err) { - LOG_D("audio_provider") << err.GetMessage(); - msg_all += std::string(factory->name) + ": " + err.GetMessage() + " not found.\n"; - } - catch (AudioDataNotFound const& err) { - LOG_D("audio_provider") << err.GetMessage(); - found_file = true; - msg_all += std::string(factory->name) + ": " + err.GetMessage() + "\n"; - } - catch (AudioProviderError const& err) { - LOG_D("audio_provider") << err.GetMessage(); - found_audio = true; - found_file = true; - std::string thismsg = std::string(factory->name) + ": " + err.GetMessage() + "\n"; - msg_all += thismsg; - msg_partial += thismsg; - } } - if (!provider) { - if (found_audio) - throw AudioProviderError(msg_partial); - if (found_file) - throw AudioDataNotFound(msg_all); - throw fs::FileNotFound(filename); + std::vector remaining_providers(tried_providers + 1, sorted.end()); + + if (!remaining_providers.size()) { + // No provider could open the file + LOG_E("audio_provider") << "Could not open " << filename; + std::string msg = "Could not open " + filename.string() + ":\n" + errors; + + if (!found_file) throw AudioDataNotFound(filename.string()); + throw AudioProviderError(msg); } + std::vector names; + for (auto const& f : remaining_providers) + names.push_back(f->name); + + int choice = wxGetSingleChoiceIndex(agi::format("Could not open %s with the preferred provider:\n\n%s\nPlease choose a different audio provider to try:", filename.string(), errors), _("Error loading audio"), to_wx(names)); + if (choice == -1) { + throw agi::UserCancelException("audio loading cancelled by user"); + } + + auto factory = remaining_providers[choice]; + auto provider = factory->create(filename, br); + if (!provider) + throw AudioProviderError("Audio provider returned null pointer"); + LOG_I("audio_provider") << factory->name << ": opened " << filename; + return provider; +} + +std::unique_ptr GetAudioProvider(fs::path const& filename, + Path const& path_helper, + BackgroundRunner *br) { + std::unique_ptr provider = SelectAudioProvider(filename, path_helper, br); + bool needs_cache = provider->NeedsCache(); // Give it a converter if needed diff --git a/src/audio_provider_vs.cpp b/src/audio_provider_vs.cpp index 2d81f06bb..bdc6bc161 100644 --- a/src/audio_provider_vs.cpp +++ b/src/audio_provider_vs.cpp @@ -86,9 +86,7 @@ VapourSynthAudioProvider::VapourSynthAudioProvider(agi::fs::path const& filename num_samples = vi->numSamples; } catch (VapourSynthError const& err) { - // Unlike the video provider manager, the audio provider factory catches AudioProviderErrors and picks whichever source doesn't throw one. - // So just rethrow the Error here with an extra label so the user will see the error message and know the audio wasn't loaded with VS - throw VapourSynthError(agi::format("VapourSynth error: %s", err.GetMessage())); + throw agi::AudioProviderError(err.GetMessage()); } template diff --git a/src/factory_manager.h b/src/factory_manager.h index 89a20d0ba..e88ba7718 100644 --- a/src/factory_manager.h +++ b/src/factory_manager.h @@ -17,6 +17,8 @@ #include #include +#include + namespace { template std::vector GetClasses(Container const& c) { @@ -50,4 +52,21 @@ auto GetSorted(Container const& c, std::string const& preferred) -> std::vector< } return sorted; } + +template +auto RearrangeWithPriority(Container &c, agi::fs::path const& filename) { + size_t end_of_hidden = 0; + for (size_t i = 0; i < c.size(); i++) { + auto provider = c[i]; + if (provider->hidden) { + end_of_hidden = i; + } else { + if (provider->wants_to_open(filename)) { + c.erase(c.begin() + i); + c.insert(c.begin() + end_of_hidden + 1, provider); + break; + } + } + } +} } diff --git a/src/project.cpp b/src/project.cpp index 89196b9e3..62690fa1f 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -260,10 +260,10 @@ void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) { return; } else - return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage())); + return ShowError(_("None of the available audio providers recognised the selected file as containing audio data:\n\n") + to_wx(e.GetMessage())); } catch (agi::AudioProviderError const& e) { - return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage())); + return ShowError(_("None of the available audio providers have a codec available to handle the selected file:\n\n") + to_wx(e.GetMessage())); } catch (agi::Exception const& e) { return ShowError(e.GetMessage()); diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index 9374a7c84..cf3c6f1b2 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -16,15 +16,18 @@ #include "video_provider_manager.h" +#include "compat.h" #include "factory_manager.h" #include "include/aegisub/video_provider.h" #include "options.h" -#include #include +#include #include +#include + std::unique_ptr CreateDummyVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); std::unique_ptr CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); @@ -39,6 +42,7 @@ namespace { const char *name; std::unique_ptr (*create)(agi::fs::path const&, std::string const&, agi::BackgroundRunner *); bool hidden; + std::function wants_to_open = [](auto p) { return false; }; }; const factory providers[] = { @@ -48,13 +52,13 @@ namespace { {"FFmpegSource", CreateFFmpegSourceVideoProvider, false}, #endif #ifdef WITH_AVISYNTH - {"Avisynth", CreateAvisynthVideoProvider, false}, + {"Avisynth", CreateAvisynthVideoProvider, false, [](auto p) { return agi::fs::HasExtension(p, "avs"); }}, #endif #ifdef WITH_BESTSOURCE {"BestSource (SLOW)", CreateBSVideoProvider, false}, #endif #ifdef WITH_VAPOURSYNTH - {"VapourSynth", CreateVapourSynthVideoProvider, false}, + {"VapourSynth", CreateVapourSynthVideoProvider, false, [](auto p) { return agi::fs::HasExtension(p, "py") || agi::fs::HasExtension(p, "vpy"); }}, #endif }; } @@ -67,22 +71,31 @@ std::unique_ptr VideoProviderFactory::GetProvider(agi::fs::path c auto preferred = OPT_GET("Video/Provider")->GetString(); auto sorted = GetSorted(boost::make_iterator_range(std::begin(providers), std::end(providers)), preferred); + RearrangeWithPriority(sorted, filename); + bool found = false; bool supported = false; std::string errors; errors.reserve(1024); - for (auto factory : sorted) { + auto tried_providers = sorted.begin(); + + auto finalize_provider = [&](std::unique_ptr provider) { + return provider->WantsCaching() ? CreateCacheVideoProvider(std::move(provider)) : std::move(provider); + }; + + for (; tried_providers < sorted.end(); tried_providers++) { + auto factory = *tried_providers; std::string err; try { auto provider = factory->create(filename, colormatrix, br); if (!provider) continue; LOG_I("manager/video/provider") << factory->name << ": opened " << filename; - return provider->WantsCaching() ? CreateCacheVideoProvider(std::move(provider)) : std::move(provider); + return finalize_provider(std::move(provider)); } - catch (VideoNotSupported const&) { + catch (VideoNotSupported const& ex) { found = true; - err = "video is not in a supported format."; + err = "video is not in a supported format: " + ex.GetMessage(); } catch (VideoOpenError const& ex) { supported = true; @@ -95,13 +108,41 @@ std::unique_ptr VideoProviderFactory::GetProvider(agi::fs::path c errors += std::string(factory->name) + ": " + err + "\n"; LOG_D("manager/video/provider") << factory->name << ": " << err; + if (factory->name == preferred) + break; } - // No provider could open the file - LOG_E("manager/video/provider") << "Could not open " << filename; - std::string msg = "Could not open " + filename.string() + ":\n" + errors; + // We failed to load the video using the preferred video provider (and the ones before it). + // The user might want to know about this, so show a dialog and let them choose what other provider to try. + std::vector remaining_providers(tried_providers + 1, sorted.end()); - if (!found) throw agi::fs::FileNotFound(filename.string()); - if (!supported) throw VideoNotSupported(msg); - throw VideoOpenError(msg); + if (!remaining_providers.size()) { + // No provider could open the file + LOG_E("manager/video/provider") << "Could not open " << filename; + std::string msg = "Could not open " + filename.string() + ":\n" + errors; + + if (!found) throw agi::fs::FileNotFound(filename.string()); + if (!supported) throw VideoNotSupported(msg); + throw VideoOpenError(msg); + } + + std::vector names; + for (auto const& f : remaining_providers) + names.push_back(f->name); + + int choice = wxGetSingleChoiceIndex(agi::format("Could not open %s with the preferred provider:\n\n%s\nPlease choose a different video provider to try:", filename.string(), errors), _("Error loading video"), to_wx(names)); + if (choice == -1) { + throw agi::UserCancelException("video loading cancelled by user"); + } + + try { + auto factory = remaining_providers[choice]; + auto provider = factory->create(filename, colormatrix, br); + if (!provider) + throw VideoNotSupported("Video provider returned null pointer"); + LOG_I("manager/video/provider") << factory->name << ": opened " << filename; + return finalize_provider(std::move(provider)); + } catch (agi::vfr::Error const& ex) { + throw VideoOpenError(ex.GetMessage()); + } } diff --git a/src/video_provider_vs.cpp b/src/video_provider_vs.cpp index da4a3d1a5..57f9e2cb0 100644 --- a/src/video_provider_vs.cpp +++ b/src/video_provider_vs.cpp @@ -284,7 +284,7 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename vs.GetAPI()->freeMap(result); vs.GetAPI()->freeNode(node); vs.GetScriptAPI()->freeScript(script); - throw VideoProviderError(agi::format("Failed to convert to RGB24: %s", error)); + throw VideoOpenError(agi::format("Failed to convert to RGB24: %s", error)); } int err; vs.GetAPI()->freeNode(node); @@ -292,7 +292,7 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename vs.GetAPI()->freeMap(result); if (err) { vs.GetScriptAPI()->freeScript(script); - throw VideoProviderError("Failed to get resize output node"); + throw VideoOpenError("Failed to get resize output node"); } // Finally, try to get the first frame again, so if the filter does crash, it happens before loading finishes @@ -308,7 +308,7 @@ VapourSynthVideoProvider::VapourSynthVideoProvider(agi::fs::path const& filename } } catch (VapourSynthError const& err) { // for the entire constructor - throw VideoProviderError(agi::format("VapourSynth error: %s", err.GetMessage())); + throw VideoOpenError(err.GetMessage()); } const VSFrame *VapourSynthVideoProvider::GetVSFrame(int n) {