Aegisub/src/project.cpp
arch1t3cht 02567c2265 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.
2023-07-16 17:52:21 +02:00

541 lines
16 KiB
C++

// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/
#include "project.h"
#include "ass_dialogue.h"
#include "ass_file.h"
#include "async_video_provider.h"
#include "audio_controller.h"
#include "audio_provider_factory.h"
#include "base_grid.h"
#include "charset_detect.h"
#include "compat.h"
#include "dialog_progress.h"
#include "dialogs.h"
#include "format.h"
#include "include/aegisub/context.h"
#include "include/aegisub/video_provider.h"
#include "mkv_wrap.h"
#include "options.h"
#include "selection_controller.h"
#include "subs_controller.h"
#include "utils.h"
#include "video_controller.h"
#include "video_display.h"
#include <libaegisub/audio/provider.h>
#include <libaegisub/format_path.h>
#include <libaegisub/fs.h>
#include <libaegisub/keyframe.h>
#include <libaegisub/log.h>
#include <libaegisub/make_unique.h>
#include <libaegisub/path.h>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem/operations.hpp>
#include <wx/msgdlg.h>
Project::Project(agi::Context *c) : context(c) {
OPT_SUB("Audio/Cache/Type", &Project::ReloadAudio, this);
OPT_SUB("Audio/Provider", &Project::ReloadAudio, this);
OPT_SUB("Provider/Audio/FFmpegSource/Decode Error Handling", &Project::ReloadAudio, this);
OPT_SUB("Provider/Audio/FFmpegSource/Downmix", &Project::ReloadAudio, this);
OPT_SUB("Provider/Avisynth/Memory Max", &Project::ReloadVideo, this);
OPT_SUB("Provider/Video/FFmpegSource/Decoding Threads", &Project::ReloadVideo, this);
OPT_SUB("Provider/Video/FFmpegSource/Unsafe Seeking", &Project::ReloadVideo, this);
OPT_SUB("Subtitle/Provider", &Project::ReloadVideo, this);
OPT_SUB("Video/Provider", &Project::ReloadVideo, this);
}
Project::~Project() { }
void Project::UpdateRelativePaths() {
context->ass->Properties.audio_file = context->path->MakeRelative(audio_file, "?script").generic_string();
context->ass->Properties.video_file = context->path->MakeRelative(video_file, "?script").generic_string();
context->ass->Properties.timecodes_file = context->path->MakeRelative(timecodes_file, "?script").generic_string();
context->ass->Properties.keyframes_file = context->path->MakeRelative(keyframes_file, "?script").generic_string();
}
void Project::ReloadAudio() {
if (audio_provider)
LoadAudio(audio_file);
}
void Project::ReloadVideo() {
if (video_provider) {
DoLoadVideo(video_file);
context->videoController->JumpToFrame(context->videoController->GetFrameN());
}
}
void Project::ShowError(wxString const& message) {
wxMessageBox(message, "Error loading file", wxOK | wxICON_ERROR | wxCENTER, context->parent);
}
void Project::ShowError(std::string const& message) {
ShowError(to_wx(message));
}
void Project::SetPath(agi::fs::path& var, const char *token, const char *mru, agi::fs::path const& value) {
var = value;
if (*token)
context->path->SetToken(token, value);
if (*mru)
config::mru->Add(mru, value);
UpdateRelativePaths();
}
bool Project::DoLoadSubtitles(agi::fs::path const& path, std::string encoding, ProjectProperties &properties) {
try {
if (encoding.empty())
encoding = CharSetDetect::GetEncoding(path);
}
catch (agi::UserCancelException const&) {
return false;
}
catch (agi::fs::FileNotFound const&) {
config::mru->Remove("Subtitle", path);
ShowError(path.string() + " not found.");
return false;
}
if (encoding != "binary") {
// Try loading as timecodes and keyframes first since we can't
// distinguish them based on filename alone, and just ignore failures
// rather than trying to differentiate between malformed timecodes
// files and things that aren't timecodes files at all
try { DoLoadTimecodes(path); return false; } catch (...) { }
try { DoLoadKeyframes(path); return false; } catch (...) { }
}
try {
properties = context->subsController->Load(path, encoding);
}
catch (agi::UserCancelException const&) { return false; }
catch (agi::fs::FileNotFound const&) {
config::mru->Remove("Subtitle", path);
ShowError(path.string() + " not found.");
return false;
}
catch (agi::Exception const& e) {
ShowError(e.GetMessage());
return false;
}
catch (std::exception const& e) {
ShowError(std::string(e.what()));
return false;
}
catch (...) {
ShowError(wxString("Unknown error"));
return false;
}
Selection sel;
AssDialogue *active_line = nullptr;
if (!context->ass->Events.empty()) {
int row = mid<int>(0, properties.active_row, context->ass->Events.size() - 1);
active_line = &*std::next(context->ass->Events.begin(), row);
sel.insert(active_line);
}
context->selectionController->SetSelectionAndActive(std::move(sel), active_line);
context->subsGrid->ScrollTo(properties.scroll_position);
return true;
}
void Project::LoadSubtitles(agi::fs::path path, std::string encoding, bool load_linked) {
ProjectProperties properties;
if (DoLoadSubtitles(path, encoding, properties) && load_linked)
LoadUnloadFiles(properties);
}
void Project::CloseSubtitles() {
context->subsController->Close();
context->path->SetToken("?script", "");
LoadUnloadFiles(context->ass->Properties);
auto line = &*context->ass->Events.begin();
context->selectionController->SetSelectionAndActive({line}, line);
}
void Project::LoadUnloadFiles(ProjectProperties properties) {
auto load_linked = OPT_GET("App/Auto/Load Linked Files")->GetInt();
if (!load_linked) return;
auto audio = context->path->MakeAbsolute(properties.audio_file, "?script");
auto video = context->path->MakeAbsolute(properties.video_file, "?script");
auto timecodes = context->path->MakeAbsolute(properties.timecodes_file, "?script");
auto keyframes = context->path->MakeAbsolute(properties.keyframes_file, "?script");
if (video == video_file && audio == audio_file && keyframes == keyframes_file && timecodes == timecodes_file)
return;
if (load_linked == 2) {
wxString str = _("Do you want to load/unload the associated files?");
str += "\n";
auto append_file = [&](agi::fs::path const& p, wxString const& unload, wxString const& load) {
if (p.empty())
str += "\n" + unload;
else
str += "\n" + agi::wxformat(load, p);
};
if (audio != audio_file)
append_file(audio, _("Unload audio"), _("Load audio file: %s"));
if (video != video_file)
append_file(video, _("Unload video"), _("Load video file: %s"));
if (timecodes != timecodes_file)
append_file(timecodes, _("Unload timecodes"), _("Load timecodes file: %s"));
if (keyframes != keyframes_file)
append_file(keyframes, _("Unload keyframes"), _("Load keyframes file: %s"));
if (wxMessageBox(str, _("(Un)Load files?"), wxYES_NO | wxCENTRE, context->parent) != wxYES)
return;
}
bool loaded_video = false;
if (video != video_file) {
if (video.empty())
CloseVideo();
else if ((loaded_video = DoLoadVideo(video))) {
auto vc = context->videoController.get();
vc->JumpToFrame(properties.video_position);
auto ar_mode = static_cast<AspectRatio>(properties.ar_mode);
if (ar_mode == AspectRatio::Custom)
vc->SetAspectRatio(properties.ar_value);
else
vc->SetAspectRatio(ar_mode);
context->videoDisplay->SetWindowZoom(properties.video_zoom);
}
}
if (!timecodes.empty()) LoadTimecodes(timecodes);
if (!keyframes.empty()) LoadKeyframes(keyframes);
if (audio != audio_file) {
if (audio.empty())
CloseAudio();
else
DoLoadAudio(audio, false);
}
else if (loaded_video && OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
DoLoadAudio(video, true);
}
void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
if (!progress)
progress = new DialogProgress(context->parent);
try {
try {
audio_provider = GetAudioProvider(path, *context->path, progress);
}
catch (agi::UserCancelException const&) { return; }
catch (...) {
config::mru->Remove("Audio", path);
throw;
}
}
catch (agi::fs::FileNotFound const& e) {
return ShowError(_("The audio file was not found: ") + to_wx(e.GetMessage()));
}
catch (agi::AudioDataNotFound const& e) {
if (quiet) {
LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetMessage();
return;
}
else
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\n") + to_wx(e.GetMessage()));
}
catch (agi::Exception const& e) {
return ShowError(e.GetMessage());
}
SetPath(audio_file, "?audio", "Audio", path);
AnnounceAudioProviderModified(audio_provider.get());
}
void Project::LoadAudio(agi::fs::path path) {
DoLoadAudio(path, false);
}
void Project::CloseAudio() {
AnnounceAudioProviderModified(nullptr);
audio_provider.reset();
SetPath(audio_file, "?audio", "", "");
}
bool Project::DoLoadVideo(agi::fs::path const& path) {
if (!progress)
progress = new DialogProgress(context->parent);
try {
auto old_matrix = context->ass->GetScriptInfo("YCbCr Matrix");
video_provider = agi::make_unique<AsyncVideoProvider>(path, old_matrix, context->videoController.get(), progress);
}
catch (agi::UserCancelException const&) { return false; }
catch (agi::fs::FileSystemError const& err) {
config::mru->Remove("Video", path);
ShowError(to_wx(err.GetMessage()));
return false;
}
catch (VideoProviderError const& err) {
ShowError(to_wx(err.GetMessage()));
return false;
}
AnnounceVideoProviderModified(video_provider.get());
UpdateVideoProperties(context->ass.get(), video_provider.get(), context->parent);
video_provider->LoadSubtitles(context->ass.get());
timecodes = video_provider->GetFPS();
keyframes = video_provider->GetKeyFrames();
timecodes_file.clear();
keyframes_file.clear();
SetPath(video_file, "?video", "Video", path);
std::string warning = video_provider->GetWarning();
if (!warning.empty())
wxMessageBox(to_wx(warning), "Warning", wxICON_WARNING | wxOK);
video_has_subtitles = false;
if (agi::fs::HasExtension(path, "mkv"))
video_has_subtitles = MatroskaWrapper::HasSubtitles(path);
AnnounceKeyframesModified(keyframes);
AnnounceTimecodesModified(timecodes);
return true;
}
void Project::LoadVideo(agi::fs::path path) {
if (path.empty()) return;
if (!DoLoadVideo(path)) return;
if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file && video_provider->HasAudio())
DoLoadAudio(video_file, true);
double dar = video_provider->GetDAR();
if (dar > 0)
context->videoController->SetAspectRatio(dar);
else
context->videoController->SetAspectRatio(AspectRatio::Default);
context->videoController->JumpToFrame(0);
}
void Project::CloseVideo() {
AnnounceVideoProviderModified(nullptr);
video_provider.reset();
SetPath(video_file, "?video", "", "");
video_has_subtitles = false;
context->ass->Properties.ar_mode = 0;
context->ass->Properties.ar_value = 0.0;
context->ass->Properties.video_position = 0;
}
void Project::DoLoadTimecodes(agi::fs::path const& path) {
timecodes = agi::vfr::Framerate(path);
SetPath(timecodes_file, "", "Timecodes", path);
AnnounceTimecodesModified(timecodes);
}
void Project::LoadTimecodes(agi::fs::path path) {
try {
DoLoadTimecodes(path);
}
catch (agi::fs::FileSystemError const& e) {
ShowError(e.GetMessage());
config::mru->Remove("Timecodes", path);
}
catch (agi::vfr::Error const& e) {
ShowError("Failed to parse timecodes file: " + e.GetMessage());
config::mru->Remove("Timecodes", path);
}
}
void Project::CloseTimecodes() {
timecodes = video_provider ? video_provider->GetFPS() : agi::vfr::Framerate{};
SetPath(timecodes_file, "", "", "");
AnnounceTimecodesModified(timecodes);
}
void Project::DoLoadKeyframes(agi::fs::path const& path) {
keyframes = agi::keyframe::Load(path);
SetPath(keyframes_file, "", "Keyframes", path);
AnnounceKeyframesModified(keyframes);
}
void Project::LoadKeyframes(agi::fs::path path) {
try {
DoLoadKeyframes(path);
}
catch (agi::fs::FileSystemError const& e) {
ShowError(e.GetMessage());
config::mru->Remove("Keyframes", path);
}
catch (agi::keyframe::KeyframeFormatParseError const& e) {
ShowError("Failed to parse keyframes file: " + e.GetMessage());
config::mru->Remove("Keyframes", path);
}
catch (agi::keyframe::UnknownKeyframeFormatError const& e) {
ShowError("Keyframes file in unknown format: " + e.GetMessage());
config::mru->Remove("Keyframes", path);
}
}
void Project::CloseKeyframes() {
keyframes = video_provider ? video_provider->GetKeyFrames() : std::vector<int>{};
SetPath(keyframes_file, "", "", "");
AnnounceKeyframesModified(keyframes);
}
void Project::LoadList(std::vector<agi::fs::path> const& files) {
// Keep these lists sorted
// Video formats
const char *videoList[] = {
".asf",
".avi",
".avs",
".d2v",
".h264",
".hevc",
".m2ts",
".m4v",
".mkv",
".mov",
".mp4",
".mpeg",
".mpg",
".ogm",
".rm",
".rmvb",
".ts",
".webm",
".wmv",
".y4m",
".yuv"
};
// Subtitle formats
const char *subsList[] = {
".ass",
".srt",
".ssa",
".sub",
".ttxt"
};
// Audio formats
const char *audioList[] = {
".aac",
".ac3",
".ape",
".dts",
".eac3",
".flac",
".m4a",
".mka",
".mp3",
".ogg",
".opus",
".w64",
".wav",
".wma"
};
auto search = [](const char **begin, const char **end, std::string const& str) {
return std::binary_search(begin, end, str.c_str(), [](const char *a, const char *b) {
return strcmp(a, b) < 0;
});
};
agi::fs::path audio, video, subs, timecodes, keyframes;
for (auto file : files) {
if (file.is_relative()) file = absolute(file);
if (!agi::fs::FileExists(file)) continue;
auto ext = file.extension().string();
boost::to_lower(ext);
// Could be subtitles, keyframes or timecodes, so try loading as each
if (ext == ".txt" || ext == ".log") {
if (timecodes.empty()) {
try {
DoLoadTimecodes(file);
timecodes = file;
continue;
} catch (...) { }
}
if (keyframes.empty()) {
try {
DoLoadKeyframes(file);
keyframes = file;
continue;
} catch (...) { }
}
if (subs.empty() && ext != ".log")
subs = file;
continue;
}
if (subs.empty() && search(std::begin(subsList), std::end(subsList), ext))
subs = file;
if (video.empty() && search(std::begin(videoList), std::end(videoList), ext))
video = file;
if (audio.empty() && search(std::begin(audioList), std::end(audioList), ext))
audio = file;
}
ProjectProperties properties;
if (!subs.empty()) {
if (!DoLoadSubtitles(subs, "", properties))
subs.clear();
}
if (!video.empty() && DoLoadVideo(video)) {
double dar = video_provider->GetDAR();
if (dar > 0)
context->videoController->SetAspectRatio(dar);
else
context->videoController->SetAspectRatio(AspectRatio::Default);
context->videoController->JumpToFrame(0);
// We loaded these earlier, but loading video unloaded them
// Non-Do version of Load in case they've vanished or changed between
// then and now
if (!timecodes.empty())
LoadTimecodes(timecodes);
if (!keyframes.empty())
LoadKeyframes(keyframes);
}
if (!audio.empty())
DoLoadAudio(audio, false);
else if (OPT_GET("Video/Open Audio")->GetBool() && audio_file != video_file)
DoLoadAudio(video_file, true);
if (!subs.empty())
LoadUnloadFiles(properties);
}