diff --git a/.gitignore b/.gitignore index 76f92ff5c..744f0c708 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ subprojects/glib* subprojects/googletest-* subprojects/harfbuzz subprojects/icu -subprojects/jansson subprojects/libass subprojects/libffi* subprojects/libpng-* @@ -43,3 +42,4 @@ subprojects/zlib-* subprojects/dirent-* subprojects/hunspell-* subprojects/uchardet-* +subprojects/xxhash diff --git a/meson.build b/meson.build index 21293e1b9..2777bf4a0 100644 --- a/meson.build +++ b/meson.build @@ -224,7 +224,7 @@ needs_ffmpeg = false if get_option('bestsource').enabled() conf.set('WITH_BESTSOURCE', 1) - bs = subproject('bestsource') + bs = subproject('bestsource', default_options: ['link_static=' + (get_option('default_library') == 'static').to_string()]) deps += bs.get_variable('bestsource_dep') dep_avail += 'BestSource' needs_ffmpeg = true diff --git a/src/audio_provider_bestsource.cpp b/src/audio_provider_bestsource.cpp index 2fe16ba3c..50479ec32 100644 --- a/src/audio_provider_bestsource.cpp +++ b/src/audio_provider_bestsource.cpp @@ -22,9 +22,10 @@ #ifdef WITH_BESTSOURCE #include +#include "bestsource_common.h" + #include "audiosource.h" -#include "bestsource_common.h" #include "compat.h" #include "options.h" @@ -38,7 +39,7 @@ namespace { class BSAudioProvider final : public agi::AudioProvider { std::map bsopts; - BestAudioSource bs; + std::unique_ptr bs; AudioProperties properties; void FillBuffer(void *Buf, int64_t Start, int64_t Count) const override; @@ -52,32 +53,49 @@ public: /// @param filename The filename to open BSAudioProvider::BSAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br) try : bsopts() -, bs(filename.string(), -1, -1, 0, GetBSCacheFile(filename), &bsopts) { - bs.SetMaxCacheSize(OPT_GET("Provider/Audio/BestSource/Max Cache Size")->GetInt() << 20); + provider_bs::CleanBSCache(); + auto track = provider_bs::SelectTrack(filename, true).first; + + if (track == provider_bs::TrackSelection::NoTracks) + throw agi::AudioDataNotFound("no audio tracks found"); + else if (track == provider_bs::TrackSelection::None) + throw agi::UserCancelException("audio loading cancelled by user"); + + bool cancelled = false; br->Run([&](agi::ProgressSink *ps) { ps->SetTitle(from_wx(_("Indexing"))); - ps->SetMessage(from_wx(_("Creating cache... This can take a while!"))); - ps->SetIndeterminate(); - if (bs.GetExactDuration()) { - LOG_D("bs") << "File cached and has exact samples."; + ps->SetMessage(from_wx(_("Indexing file... This will take a while!"))); + try { + bs = agi::make_unique(filename.string(), static_cast(track), -1, false, 0, provider_bs::GetCacheFile(filename), &bsopts, 0, [=](int Track, int64_t Current, int64_t Total) { + ps->SetProgress(Current, Total); + return !ps->IsCancelled(); + }); + } catch (BestSourceException const& err) { + if (std::string(err.what()) == "Indexing canceled by user") + cancelled = true; + else + throw err; } }); - BSCleanCache(); - properties = bs.GetAudioProperties(); - float_samples = properties.IsFloat; - bytes_per_sample = properties.BytesPerSample; + if (cancelled) + throw agi::UserCancelException("audio loading cancelled by user"); + + bs->SetMaxCacheSize(OPT_GET("Provider/Audio/BestSource/Max Cache Size")->GetInt() << 20); + properties = bs->GetAudioProperties(); + float_samples = properties.AF.Float; + bytes_per_sample = properties.AF.BytesPerSample; sample_rate = properties.SampleRate; channels = properties.Channels; num_samples = properties.NumSamples; decoded_samples = OPT_GET("Provider/Audio/BestSource/Aegisub Cache")->GetBool() ? 0 : num_samples; } -catch (AudioException const& err) { +catch (BestSourceException const& err) { throw agi::AudioProviderError("Failed to create BestAudioSource"); } void BSAudioProvider::FillBuffer(void *Buf, int64_t Start, int64_t Count) const { - const_cast(bs).GetPackedAudio(reinterpret_cast(Buf), Start, Count); + bs->GetPackedAudio(reinterpret_cast(Buf), Start, Count); } } diff --git a/src/bestsource_common.cpp b/src/bestsource_common.cpp index 2cd478ef6..c01097ee0 100644 --- a/src/bestsource_common.cpp +++ b/src/bestsource_common.cpp @@ -21,7 +21,13 @@ #ifdef WITH_BESTSOURCE #include "bestsource_common.h" +#include "tracklist.h" +extern "C" { +#include +} + +#include "format.h" #include "options.h" #include "utils.h" @@ -31,26 +37,70 @@ #include #include +namespace provider_bs { -std::string GetBSCacheFile(agi::fs::path const& filename) { - // BS can store all its index data in a single file, but we make a separate index file - // for each video file to ensure that the old index is invalidated if the file is modified. - // While BS does check the filesize of the files, it doesn't check the modification time. - uintmax_t len = agi::fs::Size(filename); +std::pair SelectTrack(agi::fs::path const& filename, bool audio) { + std::map opts; + BestTrackList tracklist(filename.string(), &opts); + + int n = tracklist.GetNumTracks(); + AVMediaType type = audio ? AVMEDIA_TYPE_AUDIO : AVMEDIA_TYPE_VIDEO; + + std::vector TrackNumbers; + wxArrayString Choices; + bool has_audio = false; + + for (int i = 0; i < n; i++) { + BestTrackList::TrackInfo info = tracklist.GetTrackInfo(i); + has_audio = has_audio || (info.MediaType == AVMEDIA_TYPE_AUDIO); + + if (info.MediaType == type) { + TrackNumbers.push_back(i); + Choices.Add(agi::wxformat(_("Track %02d: %s"), i, info.CodecString)); + } + } + + TrackSelection result; + + if (TrackNumbers.empty()) { + result = TrackSelection::NoTracks; + } else if (TrackNumbers.size() == 1) { + result = static_cast(TrackNumbers[0]); + } else { + int Choice = wxGetSingleChoiceIndex( + audio ? _("Multiple video tracks detected, please choose the one you wish to load:") : _("Multiple audio tracks detected, please choose the one you wish to load:"), + audio ? _("Choose video track") : _("Choose audio track"), + Choices); + + if (Choice >= 0) + result = static_cast(TrackNumbers[Choice]) ; + else + result = TrackSelection::None; + } + + return std::make_pair(result, has_audio); +} + +std::string GetCacheFile(agi::fs::path const& filename) { boost::crc_32_type hash; hash.process_bytes(filename.string().c_str(), filename.string().size()); - auto result = config::path->Decode("?local/bsindex/" + filename.filename().string() + "_" + std::to_string(hash.checksum()) + "_" + std::to_string(len) + "_" + std::to_string(agi::fs::ModifiedTime(filename)) + ".json"); + auto result = config::path->Decode("?local/bsindex/" + filename.filename().string() + "_" + std::to_string(hash.checksum()) + "_" + std::to_string(agi::fs::ModifiedTime(filename))); agi::fs::CreateDirectory(result.parent_path()); return result.string(); } -void BSCleanCache() { +void CleanBSCache() { CleanCache(config::path->Decode("?local/bsindex/"), - "*.json", + "*.bsindex", OPT_GET("Provider/BestSource/Cache/Size")->GetInt(), OPT_GET("Provider/BestSource/Cache/Files")->GetInt()); + + // Delete old cache files: TODO remove this after a while + CleanCache(config::path->Decode("?local/bsindex/"), + "*.json", 0, 0); +} } #endif // WITH_BESTSOURCE diff --git a/src/bestsource_common.h b/src/bestsource_common.h index 53ab39fe4..b18546efe 100644 --- a/src/bestsource_common.h +++ b/src/bestsource_common.h @@ -21,9 +21,27 @@ #ifdef WITH_BESTSOURCE -#include +namespace std { class string_view; } -std::string GetBSCacheFile(agi::fs::path const& filename); -void BSCleanCache(); +#include + +#include +#include + +namespace provider_bs { + +// X11 memes +#undef None + +enum class TrackSelection : int { + None = -1, + NoTracks = -2, +}; + +std::pair SelectTrack(agi::fs::path const& filename, bool audio); +std::string GetCacheFile(agi::fs::path const& filename); +void CleanBSCache(); + +} #endif /* WITH_BESTSOURCE */ diff --git a/src/libresrc/default_config.json b/src/libresrc/default_config.json index ced08ca0b..61baf06e2 100644 --- a/src/libresrc/default_config.json +++ b/src/libresrc/default_config.json @@ -367,6 +367,7 @@ "BestSource" : { "Max Cache Size" : 1024, "Threads" : 0, + "Apply RFF": true, "Seek Preroll" : 12 } } diff --git a/src/libresrc/osx/default_config.json b/src/libresrc/osx/default_config.json index cf2f27b12..cf44afec4 100644 --- a/src/libresrc/osx/default_config.json +++ b/src/libresrc/osx/default_config.json @@ -367,6 +367,7 @@ "BestSource" : { "Max Cache Size" : 1024, "Threads" : 0, + "Apply RFF": true, "Seek Preroll" : 12 } } diff --git a/src/preferences.cpp b/src/preferences.cpp index 1945055e0..c51a5952e 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -464,6 +464,7 @@ void Advanced_Video(wxTreebook *book, Preferences *parent) { p->OptionAdd(bs, _("Max cache size (MB)"), "Provider/Video/BestSource/Max Cache Size"); p->OptionAdd(bs, _("Decoder Threads (0 to autodetect)"), "Provider/Video/BestSource/Threads"); p->OptionAdd(bs, _("Seek preroll (Frames)"), "Provider/Video/BestSource/Seek Preroll"); + p->OptionAdd(bs, _("Apply RFF"), "Provider/Video/BestSource/Apply RFF"); #endif p->SetSizerAndFit(p->sizer); diff --git a/src/video_provider_bestsource.cpp b/src/video_provider_bestsource.cpp index 41aa3b23f..ea94eaff7 100644 --- a/src/video_provider_bestsource.cpp +++ b/src/video_provider_bestsource.cpp @@ -22,9 +22,11 @@ #ifdef WITH_BESTSOURCE #include "include/aegisub/video_provider.h" +namespace std { class string_view; } + +#include "bestsource_common.h" + #include "videosource.h" -#include "audiosource.h" -#include "BSRational.h" extern "C" { #include @@ -32,7 +34,6 @@ extern "C" { #include } -#include "bestsource_common.h" #include "options.h" #include "compat.h" #include "video_frame.h" @@ -40,10 +41,12 @@ namespace agi { class BackgroundRunner; } #include #include +#include #include #include #include #include +#include namespace { @@ -51,14 +54,21 @@ namespace { /// @brief Implements video loading through BestSource. class BSVideoProvider final : public VideoProvider { std::map bsopts; - BestVideoSource bs; + bool apply_rff; + + std::unique_ptr bs; VideoProperties properties; std::vector Keyframes; agi::vfr::Framerate Timecodes; + AVPixelFormat pixfmt; std::string colorspace; bool has_audio = false; + bool is_linear = false; + + agi::scoped_holder sws_context; + public: BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br); @@ -102,27 +112,42 @@ std::string colormatrix_description(const AVFrame *frame) { } BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string const& colormatrix, agi::BackgroundRunner *br) try -: bsopts() -, bs(filename.string(), "", 0, -1, false, OPT_GET("Provider/Video/BestSource/Threads")->GetInt(), GetBSCacheFile(filename), &bsopts) +: apply_rff(OPT_GET("Provider/Video/BestSource/Apply RFF")) +, sws_context(nullptr, sws_freeContext) { - bs.SetMaxCacheSize(OPT_GET("Provider/Video/BestSource/Max Cache Size")->GetInt() << 20); - bs.SetSeekPreRoll(OPT_GET("Provider/Video/BestSource/Seek Preroll")->GetInt()); - try { - BestAudioSource dummysource(filename.string(), -1, 0, 0, ""); - has_audio = true; - } catch (AudioException const& err) { - has_audio = false; - } + provider_bs::CleanBSCache(); + auto track_info = provider_bs::SelectTrack(filename, false); + has_audio = track_info.second; + + if (track_info.first == provider_bs::TrackSelection::NoTracks) + throw VideoNotSupported("no video tracks found"); + else if (track_info.first == provider_bs::TrackSelection::None) + throw agi::UserCancelException("video loading cancelled by user"); + + bool cancelled = false; br->Run([&](agi::ProgressSink *ps) { ps->SetTitle(from_wx(_("Indexing"))); - ps->SetMessage(from_wx(_("Creating cache... This can take a while!"))); - ps->SetIndeterminate(); - if (bs.GetExactDuration()) { - LOG_D("provider/video/bestsource") << "File cached and has exact samples."; + ps->SetMessage(from_wx(_("Decoding the full track to ensure perfect frame accuracy. This will take a while!"))); + try { + bs = agi::make_unique(filename.string(), "", 0, static_cast(track_info.first), false, OPT_GET("Provider/Video/BestSource/Threads")->GetInt(), provider_bs::GetCacheFile(filename), &bsopts, [=](int Track, int64_t Current, int64_t Total) { + ps->SetProgress(Current, Total); + return !ps->IsCancelled(); + }); + } catch (BestSourceException const& err) { + if (std::string(err.what()) == "Indexing canceled by user") + cancelled = true; + else + throw err; } }); - properties = bs.GetVideoProperties(); + if (cancelled) + throw agi::UserCancelException("video loading cancelled by user"); + + bs->SetMaxCacheSize(OPT_GET("Provider/Video/BestSource/Max Cache Size")->GetInt() << 20); + bs->SetSeekPreRoll(OPT_GET("Provider/Video/BestSource/Seek Preroll")->GetInt()); + + properties = bs->GetVideoProperties(); br->Run([&](agi::ProgressSink *ps) { ps->SetTitle(from_wx(_("Scanning"))); @@ -130,23 +155,18 @@ BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string cons std::vector TimecodesVector; for (int n = 0; n < properties.NumFrames; n++) { - if (ps->IsCancelled()) { - return; - } - std::unique_ptr frame(bs.GetFrame(n)); - if (frame == nullptr) { - throw VideoOpenError("Couldn't read frame!"); - } -#if (LIBAVUTIL_VERSION_MAJOR == 58 && LIBAVUTIL_VERSION_MINOR >= 7) || LIBAVUTIL_VERSION_MAJOR >= 59 - if (frame->GetAVFrame()->flags & AV_FRAME_FLAG_KEY) { -#else - if (frame->GetAVFrame()->key_frame) { -#endif + const BestVideoSource::FrameInfo &info = bs->GetFrameInfo(n); + if (info.KeyFrame) { Keyframes.push_back(n); } - TimecodesVector.push_back(1000 * frame->Pts * properties.TimeBase.Num / properties.TimeBase.Den); - ps->SetProgress(n, properties.NumFrames); + TimecodesVector.push_back(1000 * info.PTS * properties.TimeBase.Num / properties.TimeBase.Den); + + if (n % 16 == 0) { + if (ps->IsCancelled()) + return; + ps->SetProgress(n, properties.NumFrames); + } } if (TimecodesVector.size() < 2 || TimecodesVector.front() == TimecodesVector.back()) { @@ -156,51 +176,63 @@ BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string cons } }); - BSCleanCache(); + // Decode the first frame to get the color space and pixel format + std::unique_ptr frame(bs->GetFrame(0)); + auto avframe = frame->GetAVFrame(); + colorspace = colormatrix_description(avframe); + pixfmt = (AVPixelFormat) avframe->format; + + sws_context = sws_getContext( + properties.Width, properties.Height, pixfmt, + properties.Width, properties.Height, AV_PIX_FMT_BGR0, + SWS_BICUBIC, nullptr, nullptr, nullptr); + + if (sws_context == nullptr) { + throw VideoDecodeError("Cannot convert frame to RGB!"); + } - // Decode the first frame to get the color space - std::unique_ptr frame(bs.GetFrame(0)); - colorspace = colormatrix_description(frame->GetAVFrame()); } -catch (VideoException const& err) { +catch (BestSourceException const& err) { throw VideoOpenError(agi::format("Failed to create BestVideoSource: %s", + err.what())); } void BSVideoProvider::GetFrame(int n, VideoFrame &out) { - std::unique_ptr bsframe(bs.GetFrame(n)); + std::unique_ptr bsframe(apply_rff ? bs->GetFrameWithRFF(n) : bs->GetFrame(n)); if (bsframe == nullptr) { throw VideoDecodeError("Couldn't read frame!"); } - const AVFrame *frame = bsframe->GetAVFrame(); - SwsContext *context = sws_getContext( - frame->width, frame->height, (AVPixelFormat) frame->format, // TODO figure out aegi's color space forcing. - frame->width, frame->height, AV_PIX_FMT_BGR0, - SWS_BICUBIC, nullptr, nullptr, nullptr); + if (!is_linear && bs->GetLinearDecodingState()) { + agi::dispatch::Main().Async([] { + wxMessageBox(_("BestSource had to fall back to linear decoding. Seeking through the video will be very slow now. You may want to try a different video provider, but note that those are not guaranteed to be frame-exact."), _("Warning"), wxOK | wxICON_WARNING | wxCENTER); + }); - if (context == nullptr) { - throw VideoDecodeError("Couldn't convert frame!"); + is_linear = true; } + const AVFrame *frame = bsframe->GetAVFrame(); + int range = frame->color_range == AVCOL_RANGE_JPEG; const int *coefficients = sws_getCoefficients(frame->colorspace == AVCOL_SPC_UNSPECIFIED ? AVCOL_SPC_BT709 : frame->colorspace); - sws_setColorspaceDetails(context, - coefficients, range, - coefficients, range, - 0, 1 << 16, 1 << 16); + if (frame->format != pixfmt || frame->width != properties.Width || frame->height != properties.Height) + throw VideoDecodeError("Video has variable format!"); + + // TODO apply color space forcing. + sws_setColorspaceDetails(sws_context, + coefficients, range, + coefficients, range, + 0, 1 << 16, 1 << 16); out.data.resize(frame->width * frame->height * 4); uint8_t *data[1] = {&out.data[0]}; int stride[1] = {frame->width * 4}; - sws_scale(context, frame->data, frame->linesize, 0, frame->height, data, stride); + sws_scale(sws_context, frame->data, frame->linesize, 0, frame->height, data, stride); out.width = frame->width; out.height = frame->height; out.pitch = stride[0]; out.flipped = false; // TODO figure out flipped - - sws_freeContext(context); } } diff --git a/src/video_provider_manager.cpp b/src/video_provider_manager.cpp index 480b31457..8ec7c18fc 100644 --- a/src/video_provider_manager.cpp +++ b/src/video_provider_manager.cpp @@ -50,7 +50,7 @@ namespace { {"Avisynth", CreateAvisynthVideoProvider, false}, #endif #ifdef WITH_BESTSOURCE - {"BestSource (SLOW)", CreateBSVideoProvider, false}, + {"BestSource", CreateBSVideoProvider, false}, #endif }; } diff --git a/subprojects/bestsource.wrap b/subprojects/bestsource.wrap index 4a4dcac9c..a1eefcf03 100644 --- a/subprojects/bestsource.wrap +++ b/subprojects/bestsource.wrap @@ -1,6 +1,6 @@ [wrap-git] url = https://github.com/vapoursynth/bestsource -revision = R1 +revision = 9d7e218588867bf2b1334e5382b0f4d1b6a45aa1 clone-recursive = true diff_files = bestsource/0001.patch diff --git a/subprojects/jansson.wrap b/subprojects/jansson.wrap deleted file mode 100644 index 51ff9a82b..000000000 --- a/subprojects/jansson.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -directory = jansson -url = https://github.com/akheron/jansson.git -revision = v2.14 diff --git a/subprojects/packagefiles/bestsource/0001.patch b/subprojects/packagefiles/bestsource/0001.patch index a57bf3a76..e9beef4aa 100644 --- a/subprojects/packagefiles/bestsource/0001.patch +++ b/subprojects/packagefiles/bestsource/0001.patch @@ -1,9 +1,11 @@ diff --git a/meson.build b/meson.build -index 38de461..d56af62 100644 +index f7bdbda..3351e53 100644 --- a/meson.build +++ b/meson.build -@@ -2,10 +2,6 @@ project('BestSource', 'cpp', - default_options: ['buildtype=release', 'b_lto=true', 'b_ndebug=if-release', 'cpp_std=c++14'], +@@ -1,21 +1,15 @@ + project('BestSource', 'cpp', +- default_options: ['buildtype=release', 'b_lto=true', 'b_ndebug=if-release', 'cpp_std=c++17'], ++ default_options: ['buildtype=release', 'b_ndebug=if-release', 'cpp_std=c++17'], license: 'MIT', meson_version: '>=0.53.0', - version: '.'.join([ @@ -13,15 +15,17 @@ index 38de461..d56af62 100644 ) link_static = get_option('link_static') -@@ -15,7 +11,6 @@ sources = [ - 'src/BSRational.cpp', - 'src/BSShared.cpp', - 'src/SrcAttribCache.cpp', + + sources = [ + 'src/audiosource.cpp', +- 'src/avisynth.cpp', + 'src/bsshared.cpp', + 'src/tracklist.cpp', - 'src/vapoursynth.cpp', 'src/videosource.cpp' ] -@@ -46,17 +41,23 @@ if host_machine.cpu_family().startswith('x86') +@@ -46,10 +40,7 @@ if host_machine.cpu_family().startswith('x86') ) endif @@ -29,32 +33,16 @@ index 38de461..d56af62 100644 - deps = [ - vapoursynth_dep, -- dependency('jansson', version: '>=2.12', static: link_static), dependency('libavcodec', version: '>=60.31.0', static: link_static), dependency('libavformat', version: '>=60.16.0', static: link_static), dependency('libavutil', version: '>=58.29.0', static: link_static), - dependency('libswscale', version: '>=7.5.0', static: link_static) - ] - -+jansson_dep = dependency('jansson', version: '>= 2.12', required: false) -+ -+if jansson_dep.found() -+ deps += jansson_dep -+else -+ cmake = import('cmake') -+ jansson = cmake.subproject('jansson') -+ deps += jansson.dependency('jansson') -+endif -+ - is_gnu_linker = meson.get_compiler('cpp').get_linker_id() in ['ld.bfd', 'ld.gold', 'ld.mold'] - link_args = [] - -@@ -66,11 +67,11 @@ elif is_gnu_linker +@@ -65,12 +56,12 @@ elif is_gnu_linker link_args += ['-Wl,-Bsymbolic'] endif -shared_module('bestsource', sources, +bs_lib = static_library('bestsource', sources, + cpp_args: ['-D_FILE_OFFSET_BITS=64'], dependencies: deps, gnu_symbol_visibility: 'hidden', - install: true, diff --git a/subprojects/xxhash.wrap b/subprojects/xxhash.wrap new file mode 100644 index 000000000..e4d612565 --- /dev/null +++ b/subprojects/xxhash.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = xxHash-0.8.2 +source_url = https://github.com/Cyan4973/xxHash/archive/v0.8.2.tar.gz +source_filename = xxHash-0.8.2.tar.gz +source_hash = baee0c6afd4f03165de7a4e67988d16f0f2b257b51d0e3cb91909302a26a79c4 +patch_filename = xxhash_0.8.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/xxhash_0.8.2-1/get_patch +patch_hash = e721ef7a4c4ee0ade8b8440f6f7cb9f935b68e825249d74cb1c2503c53e68d25 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/xxhash_0.8.2-1/xxHash-0.8.2.tar.gz +wrapdb_version = 0.8.2-1 + +[provide] +libxxhash = xxhash_dep