bestsource: Bump to bestsource2

Update API usage, incorporate new features, and refactor a bit:
- Use new progress callback to show progress bar
- Use new frame info functions to get timecodes/keyframes instead of
  manually decoding
- Use new tracklist API to show track selection dialogs
- Expose option of whether to apply RFF
- Show a warning when bestsource had to revert to linear decoding
- Reuse the swscale context across frames and ensure the format
  stays constant
- Add xxhash wrap
- No longer mark the bestsource video provider as slow
This commit is contained in:
arch1t3cht 2024-04-03 16:34:25 +02:00
parent 4f8cb846cc
commit 900876cc7f
14 changed files with 230 additions and 112 deletions

2
.gitignore vendored
View file

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

View file

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

View file

@ -22,9 +22,10 @@
#ifdef WITH_BESTSOURCE
#include <libaegisub/audio/provider.h>
#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<std::string, std::string> bsopts;
BestAudioSource bs;
std::unique_ptr<BestAudioSource> 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<BestAudioSource>(filename.string(), static_cast<int>(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<BestAudioSource &>(bs).GetPackedAudio(reinterpret_cast<uint8_t *>(Buf), Start, Count);
bs->GetPackedAudio(reinterpret_cast<uint8_t *>(Buf), Start, Count);
}
}

View file

@ -21,7 +21,13 @@
#ifdef WITH_BESTSOURCE
#include "bestsource_common.h"
#include "tracklist.h"
extern "C" {
#include <libavutil/avutil.h>
}
#include "format.h"
#include "options.h"
#include "utils.h"
@ -31,26 +37,70 @@
#include <boost/crc.hpp>
#include <boost/filesystem/path.hpp>
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<TrackSelection, bool> SelectTrack(agi::fs::path const& filename, bool audio) {
std::map<std::string, std::string> opts;
BestTrackList tracklist(filename.string(), &opts);
int n = tracklist.GetNumTracks();
AVMediaType type = audio ? AVMEDIA_TYPE_AUDIO : AVMEDIA_TYPE_VIDEO;
std::vector<int> 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<TrackSelection>(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<TrackSelection>(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

View file

@ -21,9 +21,27 @@
#ifdef WITH_BESTSOURCE
#include <libaegisub/fs_fwd.h>
namespace std { class string_view; }
std::string GetBSCacheFile(agi::fs::path const& filename);
void BSCleanCache();
#include <bsshared.h>
#include <libaegisub/fs_fwd.h>
#include <libaegisub/background_runner.h>
namespace provider_bs {
// X11 memes
#undef None
enum class TrackSelection : int {
None = -1,
NoTracks = -2,
};
std::pair<TrackSelection, bool> SelectTrack(agi::fs::path const& filename, bool audio);
std::string GetCacheFile(agi::fs::path const& filename);
void CleanBSCache();
}
#endif /* WITH_BESTSOURCE */

View file

@ -367,6 +367,7 @@
"BestSource" : {
"Max Cache Size" : 1024,
"Threads" : 0,
"Apply RFF": true,
"Seek Preroll" : 12
}
}

View file

@ -367,6 +367,7 @@
"BestSource" : {
"Max Cache Size" : 1024,
"Threads" : 0,
"Apply RFF": true,
"Seek Preroll" : 12
}
}

View file

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

View file

@ -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 <libavutil/frame.h>
@ -32,7 +34,6 @@ extern "C" {
#include <libswscale/swscale.h>
}
#include "bestsource_common.h"
#include "options.h"
#include "compat.h"
#include "video_frame.h"
@ -40,10 +41,12 @@ namespace agi { class BackgroundRunner; }
#include <libaegisub/fs.h>
#include <libaegisub/path.h>
#include <libaegisub/dispatch.h>
#include <libaegisub/make_unique.h>
#include <libaegisub/background_runner.h>
#include <libaegisub/log.h>
#include <libaegisub/format.h>
#include <libaegisub/scoped_ptr.h>
namespace {
@ -51,14 +54,21 @@ namespace {
/// @brief Implements video loading through BestSource.
class BSVideoProvider final : public VideoProvider {
std::map<std::string, std::string> bsopts;
BestVideoSource bs;
bool apply_rff;
std::unique_ptr<BestVideoSource> bs;
VideoProperties properties;
std::vector<int> Keyframes;
agi::vfr::Framerate Timecodes;
AVPixelFormat pixfmt;
std::string colorspace;
bool has_audio = false;
bool is_linear = false;
agi::scoped_holder<SwsContext *> 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<BestVideoSource>(filename.string(), "", 0, static_cast<int>(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,24 +155,19 @@ BSVideoProvider::BSVideoProvider(agi::fs::path const& filename, std::string cons
std::vector<int> TimecodesVector;
for (int n = 0; n < properties.NumFrames; n++) {
if (ps->IsCancelled()) {
return;
}
std::unique_ptr<BestVideoFrame> 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);
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()) {
Timecodes = (double) properties.FPS.Num / properties.FPS.Den;
@ -156,36 +176,50 @@ 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<BestVideoFrame> frame(bs->GetFrame(0));
auto avframe = frame->GetAVFrame();
colorspace = colormatrix_description(avframe);
pixfmt = (AVPixelFormat) avframe->format;
// Decode the first frame to get the color space
std::unique_ptr<BestVideoFrame> frame(bs.GetFrame(0));
colorspace = colormatrix_description(frame->GetAVFrame());
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!");
}
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<BestVideoFrame> bsframe(bs.GetFrame(n));
std::unique_ptr<BestVideoFrame> 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,
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);
@ -193,14 +227,12 @@ void BSVideoProvider::GetFrame(int n, VideoFrame &out) {
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);
}
}

View file

@ -50,7 +50,7 @@ namespace {
{"Avisynth", CreateAvisynthVideoProvider, false},
#endif
#ifdef WITH_BESTSOURCE
{"BestSource (SLOW)", CreateBSVideoProvider, false},
{"BestSource", CreateBSVideoProvider, false},
#endif
};
}

View file

@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/vapoursynth/bestsource
revision = R1
revision = 9d7e218588867bf2b1334e5382b0f4d1b6a45aa1
clone-recursive = true
diff_files = bestsource/0001.patch

View file

@ -1,4 +0,0 @@
[wrap-git]
directory = jansson
url = https://github.com/akheron/jansson.git
revision = v2.14

View file

@ -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,

13
subprojects/xxhash.wrap Normal file
View file

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