Aegisub/aegisub/video_provider_avs.cpp

599 lines
21 KiB
C++

// Copyright (c) 2006, Fredrik Mellbin
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// -----------------------------------------------------------------------------
//
// AEGISUB
//
// Website: http://aegisub.cellosoft.com
// Contact: mailto:zeratul@cellosoft.com
//
///////////
// Headers
#include <wx/wxprec.h>
#ifdef WITH_AVISYNTH
#include <wx/filename.h>
#include <wx/msw/registry.h>
#include <wx/filename.h>
#include "video_provider_avs.h"
#include "video_context.h"
#include "options.h"
#include "standard_paths.h"
#include "vfr.h"
#include "ass_file.h"
#include "gl_wrap.h"
#include "mkv_wrap.h"
#include "vfw_wrap.h"
///////////////
// Constructor
AvisynthVideoProvider::AvisynthVideoProvider(Aegisub::String _filename, double _fps) {
AVSTRACE(wxString::Format(_T("AvisynthVideoProvider: Creating new AvisynthVideoProvider: \"%s\", \"%s\""), _filename, _subfilename));
bool mpeg2dec3_priority = true;
RGB32Video = NULL;
SubtitledVideo = NULL;
fps = _fps;
num_frames = 0;
last_fnum = -1;
byFrame = false;
KeyFrames.Clear();
keyFramesLoaded = false;
isVfr = false;
AVSTRACE(_T("AvisynthVideoProvider: Loading Subtitles Renderer"));
LoadRenderer();
AVSTRACE(_T("AvisynthVideoProvider: Subtitles Renderer loaded"));
AVSTRACE(_T("AvisynthVideoProvider: Opening video"));
RGB32Video = OpenVideo(_filename,mpeg2dec3_priority);
AVSTRACE(_T("AvisynthVideoProvider: Video opened"));
SubtitledVideo = RGB32Video;
AVSTRACE(_T("AvisynthVideoProvider: Applied subtitles"));
vi = SubtitledVideo->GetVideoInfo();
AVSTRACE(_T("AvisynthVideoProvider: Got video info"));
AVSTRACE(_T("AvisynthVideoProvider: Done creating AvisynthVideoProvider"));
}
//////////////
// Destructor
AvisynthVideoProvider::~AvisynthVideoProvider() {
AVSTRACE(_T("AvisynthVideoProvider: Destroying AvisynthVideoProvider"));
RGB32Video = NULL;
SubtitledVideo = NULL;
AVSTRACE(_T("AvisynthVideoProvider: Destroying frame"));
iframe.Clear();
AVSTRACE(_T("AvisynthVideoProvider: AvisynthVideoProvider destroyed"));
}
////////////////////////////////////// VIDEO PROVIDER //////////////////////////////////////
/////////////////////////////////////////
// Actually open the video into Avisynth
PClip AvisynthVideoProvider::OpenVideo(Aegisub::String _filename, bool mpeg2dec3_priority) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening video"));
wxMutexLocker lock(AviSynthMutex);
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Got AVS mutex"));
AVSValue script;
byFrame = false;
usedDirectShow = false;
decoderName = _("Unknown");
wxString extension = wxString(_filename.c_str()).Right(4);
extension.LowerCase();
try {
// Prepare filename
//char *videoFilename = env->SaveString(_filename.mb_str(wxConvLocal));
wxFileName fname(_filename);
char *videoFilename = env->SaveString(fname.GetShortPath().mb_str(wxConvLocal));
// Avisynth file, just import it
if (extension == _T(".avs")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening .avs file with Import"));
script = env->Invoke("Import", videoFilename);
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Finished"));
decoderName = _T("Import");
}
// Open avi file with AviSource
else if (extension == _T(".avi")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening .avi file with AviSource"));
try {
const char *argnames[2] = { 0, "audio" };
AVSValue args[2] = { videoFilename, false };
script = env->Invoke("AviSource", AVSValue(args,2), argnames);
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Successfully opened .avi file without audio"));
byFrame = true;
decoderName = _T("AviSource");
}
// On Failure, fallback to DSS
catch (AvisynthError &) {
AVSTRACE(_T("Failed to open .avi file with AviSource, switching to DirectShowSource"));
goto directshowOpen;
}
}
// Open d2v with mpeg2dec3
else if (extension == _T(".d2v") && env->FunctionExists("Mpeg2Dec3_Mpeg2Source") && mpeg2dec3_priority) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening .d2v file with Mpeg2Dec3_Mpeg2Source"));
script = env->Invoke("Mpeg2Dec3_Mpeg2Source", videoFilename);
decoderName = _T("Mpeg2Dec3_Mpeg2Source");
//if avisynth is 2.5.7 beta 2 or newer old mpeg2decs will crash without this
if (env->FunctionExists("SetPlanarLegacyAlignment")) {
AVSValue args[2] = { script, true };
script = env->Invoke("SetPlanarLegacyAlignment", AVSValue(args,2));
}
}
// If that fails, try opening it with DGDecode
else if (extension == _T(".d2v") && env->FunctionExists("DGDecode_Mpeg2Source")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening .d2v file with DGDecode_Mpeg2Source"));
script = env->Invoke("Mpeg2Source", videoFilename);
decoderName = _T("DGDecode_Mpeg2Source");
//note that DGDecode will also have issues like if the version is too ancient but no sane person
//would use that anyway
}
else if (extension == _T(".d2v") && env->FunctionExists("Mpeg2Source")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening .d2v file with other Mpeg2Source"));
script = env->Invoke("Mpeg2Source", videoFilename);
decoderName = _T("Mpeg2Source");
//if avisynth is 2.5.7 beta 2 or newer old mpeg2decs will crash without this
if (env->FunctionExists("SetPlanarLegacyAlignment"))
script = env->Invoke("SetPlanarLegacyAlignment", script);
}
// Some other format, such as mkv, mp4, ogm... try FFMpegSource and DirectShowSource
else {
// Try loading FFMpegSource
directshowOpen:
bool ffsource = false;
if (env->FunctionExists("ffmpegsource")) ffsource = true;
if (!ffsource) {
wxFileName ffsourcepath(StandardPaths::DecodePath(_T("?data/ffmpegsource.dll")));
if (ffsourcepath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loading FFMpegSource"));
env->Invoke("LoadPlugin",env->SaveString(ffsourcepath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loaded FFMpegSource"));
byFrame = true;
}
}
// If FFMpegSource loaded properly, try using it
ffsource = false;
if (env->FunctionExists("ffmpegsource")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Invoking FFMpegSource"));
const char *argnames[2] = { "source", "vcache" };
AVSValue args[2] = { videoFilename, false };
script = env->Invoke("ffmpegsource", AVSValue(args,2), argnames);
//script = env->Invoke("ffmpegsource", videoFilename);
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Successfully opened file with FFMpegSource"));
ffsource = true;
decoderName = _T("FFmpegSource");
}
// DirectShowSource
if (!ffsource) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Opening file with DirectShowSource"));
// Try loading DirectShowSource2
bool dss2 = false;
if (env->FunctionExists("dss2")) dss2 = true;
if (!dss2) {
wxFileName dss2path(StandardPaths::DecodePath(_T("?data/avss.dll")));
if (dss2path.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loading DirectShowSource2"));
env->Invoke("LoadPlugin",env->SaveString(dss2path.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loaded DirectShowSource2"));
}
}
// If DSS2 loaded properly, try using it
dss2 = false;
if (env->FunctionExists("dss2")) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Invoking DSS2"));
if (fps == 0.0) script = env->Invoke("DSS2", videoFilename);
else {
const char *argnames[2] = { 0, "fps" };
AVSValue args[2] = { videoFilename, fps };
script = env->Invoke("DSS2", AVSValue(args,2), argnames);
}
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Successfully opened file with DSS2"));
dss2 = true;
decoderName = _T("DSS2");
}
// Try DirectShowSource
if (!dss2) {
// Load DirectShowSource.dll from app dir if it exists
wxFileName dsspath(StandardPaths::DecodePath(_T("?data/DirectShowSource.dll")));
if (dsspath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loading DirectShowSource"));
env->Invoke("LoadPlugin",env->SaveString(dsspath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Loaded DirectShowSource"));
}
// Then try using DSS
if (env->FunctionExists("DirectShowSource")) {
if (fps == 0.0) {
const char *argnames[3] = { 0, "video", "audio" };
AVSValue args[3] = { videoFilename, true, false };
script = env->Invoke("DirectShowSource", AVSValue(args,3), argnames);
}
else {
const char *argnames[4] = { 0, "video", "audio" , "fps" };
AVSValue args[4] = { videoFilename, true, false , fps };
script = env->Invoke("DirectShowSource", AVSValue(args,4), argnames);
}
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Successfully opened file with DSS without audio"));
usedDirectShow = true;
decoderName = _T("DirectShowSource");
}
// Failed to find a suitable function
else {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: DSS function not found"));
throw AvisynthError("No function suitable for opening the video found");
}
}
}
}
}
// Catch errors
catch (AvisynthError &err) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Avisynth error: ") + wxString(err.msg,wxConvLocal));
throw _T("AviSynth error: ") + wxString(err.msg,wxConvLocal);
}
// Check if video was loaded properly
if (!script.IsClip() || !script.AsClip()->GetVideoInfo().HasVideo()) {
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: No suitable video found"));
throw _T("Avisynth: No usable video found in ") + _filename;
}
// Read keyframes and timecodes from MKV file
isVfr = false;
FrameRate temp;
double overFps = 0;
bool mkvOpen = MatroskaWrapper::wrapper.IsOpen();
KeyFrames.Clear();
if (extension == _T(".mkv") || mkvOpen) {
// Parse mkv
if (!mkvOpen) MatroskaWrapper::wrapper.Open(_filename);
// Get keyframes
KeyFrames = MatroskaWrapper::wrapper.GetKeyFrames();
keyFramesLoaded = true;
// Ask to override timecodes
int override = wxYES;
if (VFR_Output.IsLoaded()) override = wxMessageBox(_("You already have timecodes loaded. Replace them with the timecodes from the Matroska file?"),_("Replace timecodes?"),wxYES_NO | wxICON_QUESTION);
if (override == wxYES) {
MatroskaWrapper::wrapper.SetToTimecodes(temp);
isVfr = temp.GetFrameRateType() == VFR;
if (isVfr) {
overFps = temp.GetCommonFPS();
MatroskaWrapper::wrapper.SetToTimecodes(VFR_Input);
MatroskaWrapper::wrapper.SetToTimecodes(VFR_Output);
trueFrameRate = temp;
}
}
// Close mkv
MatroskaWrapper::wrapper.Close();
}
// check if we have windows, if so we can load keyframes from AVI files using VFW
#ifdef __WINDOWS__
else if (extension == _T(".avi")) {
keyFramesLoaded = false;
KeyFrames.Clear();
KeyFrames = VFWWrapper::GetKeyFrames(_filename);
keyFramesLoaded = true;
}
#endif /* __WINDOWS__ */
// Check if the file is all keyframes
bool isAllKeyFrames = true;
for (unsigned int i=1; i<KeyFrames.GetCount(); i++) {
// Is the last keyframe not this keyframe -1?
if (KeyFrames[i-1] != (int)(i-1)) {
// It's not all keyframes, go ahead
isAllKeyFrames = false;
break;
}
}
// If it is all keyframes, discard the keyframe info as it is useless
if (isAllKeyFrames) {
KeyFrames.Clear();
keyFramesLoaded = false;
}
// Convert to RGB32
// If "Avisynth renders its own subs" is enabled, this should always be done,
// regardless of shaders being enabled or not. (Since VSFilter will convert
// to RGB32 and back again itself either way.)
if (!OpenGLWrapper::UseShaders()) {
script = env->Invoke("ConvertToRGB32", script);
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Converted to RGB32"));
}
// Cache
AVSTRACE(_T("AvisynthVideoProvider::OpenVideo: Finished opening video, AVS mutex will be released now"));
return (env->Invoke("Cache", script)).AsClip();
}
////////////////////////
// Actually get a frame
const AegiVideoFrame AvisynthVideoProvider::GetFrame(int _n,int formatMask) {
// Transform n if overriden
int n = _n;
if (frameTime.Count()) {
if (n < 0) n = 0;
if (n >= (signed) frameTime.Count()) n = frameTime.Count()-1;
int time = frameTime[n];
double curFps = (double)vi.fps_numerator/(double)vi.fps_denominator;
n = time * curFps / 1000.0;
}
// Get avs frame
AVSTRACE(_T("AvisynthVideoProvider::GetFrame"));
wxMutexLocker lock(AviSynthMutex);
PVideoFrame frame = SubtitledVideo->GetFrame(n,env);
int Bpp = vi.BitsPerPixel() / 8;
// Aegisub's video frame
AegiVideoFrame &final = iframe;
final.flipped = false;
final.cppAlloc = true;
final.invertChannels = false;
// Format
if (vi.IsRGB32()) {
final.format = FORMAT_RGB32;
final.flipped = true;
final.invertChannels = true;
}
else if (vi.IsRGB24()) {
final.format = FORMAT_RGB24;
final.flipped = true;
final.invertChannels = true;
}
else if (vi.IsYV12()) final.format = FORMAT_YV12;
else if (vi.IsYUY2()) final.format = FORMAT_YUY2;
// Set size properties
int uvpitch = 0;
if (final.format == FORMAT_YV12) uvpitch = frame->GetPitch(PLANAR_U);
final.pitch[0] = frame->GetPitch();
final.pitch[1] = uvpitch;
final.pitch[2] = uvpitch;
final.w = frame->GetRowSize() / Bpp;
final.h = frame->GetHeight();
// Allocate
final.Allocate();
// Copy
memcpy(final.data[0],frame->GetReadPtr(),final.pitch[0] * final.h);
// Copy second and third planes for YV12
if (final.format == FORMAT_YV12) {
int uvh = frame->GetHeight(PLANAR_U);
memcpy(final.data[1],frame->GetReadPtr(PLANAR_U),uvpitch * uvh);
memcpy(final.data[2],frame->GetReadPtr(PLANAR_V),uvpitch * uvh);
}
// Set last number
last_fnum = n;
return final;
}
////////////////////////////////////////////////////////
// Apply VSFilter subtitles, or whatever is appropriate
PClip AvisynthVideoProvider::ApplySubtitles(Aegisub::String _filename, PClip videosource) {
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Applying subtitles"));
wxMutexLocker lock(AviSynthMutex);
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Got AVS mutex"));
// Insert subs
AVSValue script;
char temp[512];
wxFileName fname(_filename);
strcpy(temp,fname.GetShortPath().mb_str(wxConvLocal));
AVSValue args[2] = { videosource, temp };
try {
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Now invoking ") + rendererCallString);
script = env->Invoke(wxString(rendererCallString.c_str()).mb_str(wxConvUTF8), AVSValue(args,2));
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Invoked successfully"));
}
catch (AvisynthError &err) {
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Avisynth error: ") + wxString(err.msg,wxConvLocal));
throw _T("AviSynth error: ") + wxString(err.msg,wxConvLocal);
}
// Cache
AVSTRACE(_T("AvisynthVideoProvider::ApplySutitles: Subtitles applied, AVS mutex will be released now"));
return (env->Invoke("Cache", script)).AsClip();
}
////////////////////////////////////// SUBTITLES PROVIDER //////////////////////////////////////
/////////////////////////////
// Get as subtitles provider
SubtitlesProvider *AvisynthVideoProvider::GetAsSubtitlesProvider() {
if (Options.AsBool(_T("Avisynth render own subs"))) return this;
return NULL;
}
/////////////////////
// Refresh subtitles
void AvisynthVideoProvider::LoadSubtitles(AssFile *subs) {
// Reset
AVSTRACE(_T("AvisynthVideoProvider::RefreshSubtitles: Refreshing subtitles"));
SubtitledVideo = NULL;
// Dump subs to disk
wxString subfilename = VideoContext::Get()->GetTempWorkFile();
subs->Save(subfilename,false,false,_T("UTF-8"));
delete subs;
// Load subtitles
SubtitledVideo = ApplySubtitles(subfilename.c_str(), RGB32Video);
AVSTRACE(_T("AvisynthVideoProvider::RefreshSubtitles: Subtitles refreshed"));
vi = SubtitledVideo->GetVideoInfo();
AVSTRACE(_T("AvisynthVideoProvider: Got video info"));
}
/////////////////////////////
// Load appropriate renderer
void AvisynthVideoProvider::LoadRenderer() {
// Get prefferred
wxString prefferred = Options.AsText(_T("Avisynth subs renderer"));
// Load
if (prefferred.Lower() == _T("asa")) LoadASA();
else LoadVSFilter();
}
/////////////////
// Load VSFilter
void AvisynthVideoProvider::LoadVSFilter() {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Loading VSFilter"));
// Loading an avisynth plugin multiple times does almost nothing
wxFileName vsfilterPath(StandardPaths::DecodePath(_T("?data/csri/vsfilter.dll")));
if (!vsfilterPath.FileExists())
vsfilterPath = wxFileName(StandardPaths::DecodePath(_T("?data/vsfilter.dll")));
rendererCallString = _T("TextSub");
if (vsfilterPath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Invoking LoadPlugin"));
env->Invoke("LoadPlugin",env->SaveString(vsfilterPath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Loaded"));
}
else {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: VSFilter.dll not found in Aegisub dir, trying to locate registered DShow filter"));
wxRegKey reg(_T("HKEY_CLASSES_ROOT\\CLSID\\{9852A670-F845-491B-9BE6-EBD841B8A613}\\InprocServer32"));
if (reg.Exists()) {
wxString fn;
reg.QueryValue(_T(""),fn);
vsfilterPath = fn;
if (vsfilterPath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Found as DShow filter, loading"));
env->Invoke("LoadPlugin",env->SaveString(vsfilterPath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Loaded"));
return;
}
vsfilterPath = _T("vsfilter.dll");
}
else if (vsfilterPath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Found on system path, loading"));
env->Invoke("LoadPlugin",env->SaveString(vsfilterPath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Loaded"));
}
else if (!env->FunctionExists("TextSub")) {
AVSTRACE(_T("AvisynthVideoProvider::LoadVSFilter: Couldn't locate VSFilter"));
throw _T("Couldn't locate VSFilter for Avisynth internal subtitle rendering");
}
}
}
////////////
// Load asa
void AvisynthVideoProvider::LoadASA() {
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Loading asa"));
// Loading an avisynth plugin multiple times does almost nothing
wxFileName asaPath(StandardPaths::DecodePath(_T("?data/asa.dll")));
rendererCallString = _T("asa");
if (asaPath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Invoking LoadPlugin"));
env->Invoke("LoadPlugin",env->SaveString(asaPath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Loaded"));
}
else {
asaPath = _T("asa.dll");
if (asaPath.FileExists()) {
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Invoking LoadPlugin"));
env->Invoke("LoadPlugin",env->SaveString(asaPath.GetFullPath().mb_str(wxConvLocal)));
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Loaded"));
}
else if (!env->FunctionExists("asa")) {
AVSTRACE(_T("AvisynthVideoProvider::LoadASA: Couldn't locate asa"));
throw _T("Couldn't locate asa for Avisynth internal subtitle rendering");
}
}
}
////////////////////////
// Override frame times
void AvisynthVideoProvider::OverrideFrameTimeList(wxArrayInt list) {
frameTime = list;
num_frames = frameTime.Count();
}
///////////////
// Get warning
Aegisub::String AvisynthVideoProvider::GetWarning() {
if (usedDirectShow) return L"Warning! The file is being opened using Avisynth's DirectShowSource, which has unreliable seeking. Frame numbers might not match the real number. PROCEED AT YOUR OWN RISK!";
else return L"";
}
#endif