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.
This commit is contained in:
arch1t3cht 2023-07-16 17:52:21 +02:00
parent 7ca8b4c008
commit 02567c2265
7 changed files with 144 additions and 57 deletions

View File

@ -24,6 +24,8 @@
#include <memory>
#include <string>
#pragma once
#undef CreateDirectory
namespace agi {

View File

@ -16,11 +16,13 @@
#include "audio_provider_factory.h"
#include "compat.h"
#include "factory_manager.h"
#include "options.h"
#include "utils.h"
#include <libaegisub/audio/provider.h>
#include <libaegisub/format.h>
#include <libaegisub/fs.h>
#include <libaegisub/log.h>
#include <libaegisub/path.h>
@ -39,6 +41,7 @@ struct factory {
const char *name;
std::unique_ptr<AudioProvider> (*create)(fs::path const&, BackgroundRunner *);
bool hidden;
std::function<bool(agi::fs::path const&)> 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<std::string> GetAudioProviderNames() {
return ::GetClasses(boost::make_iterator_range(std::begin(providers), std::end(providers)));
}
std::unique_ptr<agi::AudioProvider> GetAudioProvider(fs::path const& filename,
Path const& path_helper,
BackgroundRunner *br) {
std::unique_ptr<agi::AudioProvider> 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<AudioProvider> 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<const factory *> 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<std::string> 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<agi::AudioProvider> GetAudioProvider(fs::path const& filename,
Path const& path_helper,
BackgroundRunner *br) {
std::unique_ptr<agi::AudioProvider> provider = SelectAudioProvider(filename, path_helper, br);
bool needs_cache = provider->NeedsCache();
// Give it a converter if needed

View File

@ -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<typename T>

View File

@ -17,6 +17,8 @@
#include <string>
#include <vector>
#include <libaegisub/fs.h>
namespace {
template<typename Container>
std::vector<std::string> GetClasses(Container const& c) {
@ -50,4 +52,21 @@ auto GetSorted(Container const& c, std::string const& preferred) -> std::vector<
}
return sorted;
}
template<typename Container>
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;
}
}
}
}
}

View File

@ -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());

View File

@ -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 <libaegisub/fs.h>
#include <libaegisub/log.h>
#include <libaegisub/format.h>
#include <boost/range/iterator_range.hpp>
#include <wx/choicdlg.h>
std::unique_ptr<VideoProvider> CreateDummyVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
std::unique_ptr<VideoProvider> CreateYUV4MPEGVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
std::unique_ptr<VideoProvider> CreateFFmpegSourceVideoProvider(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
@ -39,6 +42,7 @@ namespace {
const char *name;
std::unique_ptr<VideoProvider> (*create)(agi::fs::path const&, std::string const&, agi::BackgroundRunner *);
bool hidden;
std::function<bool(agi::fs::path const&)> 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<VideoProvider> 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<VideoProvider> 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<VideoProvider> 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<const factory *> 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<std::string> 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());
}
}

View File

@ -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) {