mirror of https://github.com/odrling/Aegisub
FFMS2 beta 4
This commit breaks the shit out of linux Originally committed to SVN as r2571.
This commit is contained in:
parent
8a4e4efff3
commit
ff8d019d58
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2004-2008 Mike Matsnev. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* $Id: CoParser.h,v 1.21 2008/03/29 15:41:28 mike Exp $
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef COPARSER_H
|
||||||
|
#define COPARSER_H
|
||||||
|
|
||||||
|
// Generic multimedia container parsers support
|
||||||
|
|
||||||
|
// random access stream, this will be provided by
|
||||||
|
// IMMContainer's client
|
||||||
|
[uuid("8E192E9F-E536-4027-8D46-664CC7A102C5")]
|
||||||
|
interface IMMStream : public IUnknown {
|
||||||
|
// read count bytes starting at position
|
||||||
|
STDMETHOD(Read)( unsigned long long position,
|
||||||
|
void *buffer,
|
||||||
|
unsigned int *count) = 0;
|
||||||
|
|
||||||
|
// scan the file starting at position for the signature
|
||||||
|
// signature can't have zero bytes
|
||||||
|
STDMETHOD(Scan)( unsigned long long *position,
|
||||||
|
unsigned int signature) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[uuid("A237C873-C6AD-422E-90DB-7CB4627DCFD9")]
|
||||||
|
interface IMMStreamOpen : public IUnknown {
|
||||||
|
STDMETHOD(Open)(LPCWSTR name) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[uuid("D8FF7213-6E09-4256-A2E5-5872C798B128")]
|
||||||
|
interface IMMFrame : public IMediaSample2 {
|
||||||
|
// track number must be the same as returned by
|
||||||
|
// IMMContainer->EnumTracks() iterator
|
||||||
|
STDMETHOD_(unsigned, GetTrack)() = 0;
|
||||||
|
STDMETHOD(SetTrack)(unsigned) = 0;
|
||||||
|
|
||||||
|
STDMETHOD_(unsigned, GetPre)() = 0;
|
||||||
|
STDMETHOD(SetPre)(unsigned) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[uuid("B8324E2A-21A9-46A1-8922-70C55D06311A")]
|
||||||
|
interface IMMErrorInfo : public IUnknown {
|
||||||
|
STDMETHOD(LogError)(BSTR message) = 0; // message is owned by the caller
|
||||||
|
STDMETHOD(LogWarning)(BSTR message) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[uuid("C7120EDB-528C-4ebe-BB53-DA8E70E618EE")]
|
||||||
|
interface IMemAlloc : public IUnknown {
|
||||||
|
STDMETHOD(GetBuffer)(HANDLE hAbortEvt, DWORD size, IMMFrame **pS) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// container itself should support IPropertyBag and return
|
||||||
|
// at least Duration[UI8] (in ns) property
|
||||||
|
// if a containter supports multiple segments in the same
|
||||||
|
// physical file it should return SegmentTop[UI8] property
|
||||||
|
// that return the offset of the first byte after this
|
||||||
|
// segment's end
|
||||||
|
[uuid("A369001B-F292-45f7-A942-84F9C8C0718A")]
|
||||||
|
interface IMMContainer : public IUnknown {
|
||||||
|
STDMETHOD(Open)( IMMStream *stream,
|
||||||
|
unsigned long long position,
|
||||||
|
IMMErrorInfo *einfo,
|
||||||
|
IMemAlloc *alloc) = 0;
|
||||||
|
STDMETHOD(GetProgress)(unsigned long long *cur,unsigned long long *max) = 0;
|
||||||
|
STDMETHOD(AbortOpen)() = 0;
|
||||||
|
|
||||||
|
// pu->Next() returns objects supporting IPropertyBag interface
|
||||||
|
STDMETHOD(EnumTracks)(IEnumUnknown **pu) = 0;
|
||||||
|
|
||||||
|
// pu->Next() returns objects supporting IProperyBag and IEnumUnknown interfaces
|
||||||
|
STDMETHOD(EnumEditions)(IEnumUnknown **pu) = 0;
|
||||||
|
|
||||||
|
// pu->Next() returns objects supporting IProperyBag and IMMStream interfaces
|
||||||
|
STDMETHOD(EnumAttachments)(IEnumUnknown **pu) = 0;
|
||||||
|
|
||||||
|
// S_FALSE is end of stream, S_OK next valid frame returned, E_ABORT wait aborted
|
||||||
|
STDMETHOD(ReadFrame)(HANDLE hAbortEvt, IMMFrame **frame) = 0;
|
||||||
|
|
||||||
|
// seeking
|
||||||
|
STDMETHOD(Seek)(unsigned long long timecode,unsigned flags) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* FIXME: duplicated in matroska parser
|
||||||
|
enum { // Track type
|
||||||
|
TT_VIDEO = 1,
|
||||||
|
TT_AUDIO = 2,
|
||||||
|
TT_SUBS = 17,
|
||||||
|
|
||||||
|
TT_INTERLEAVED = 0x10001,
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
enum { // Seek flags
|
||||||
|
MMSF_PREV_KF = 1,
|
||||||
|
MMSF_NEXT_KF = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Track properties [IPropertyBag from EnumTracks->Next()]
|
||||||
|
Name Type Optional
|
||||||
|
DefaultDuration UI8 Yes in ns
|
||||||
|
Video.Interlaced BOOL Yes
|
||||||
|
Video.DisplayWidth UI4 Yes
|
||||||
|
Video.DisplayHeight UI4 Yes
|
||||||
|
Video.PixelWidth UI4 No
|
||||||
|
Video.PixelHeight UI4 No
|
||||||
|
CodecID BSTR No
|
||||||
|
Type UI4 No TT_* enumeration
|
||||||
|
CodecPrivate ARRAY|UI1 Yes
|
||||||
|
Audio.Channels UI4 No
|
||||||
|
Audio.BitDepth UI4 Yes
|
||||||
|
Audio.SamplingFreq UI4 No
|
||||||
|
Audio.OutputSamplingFreq UI4 Yes
|
||||||
|
Language BSTR Yes
|
||||||
|
Name BSTR Yes
|
||||||
|
FOURCC UI4 Yes
|
||||||
|
*/
|
||||||
|
|
||||||
|
#endif
|
|
@ -264,7 +264,7 @@ MatroskaAudioSource::MatroskaAudioSource(const char *SourceFile, int Track, Fram
|
||||||
CodecContext->extradata = (uint8_t *)TI->CodecPrivate;
|
CodecContext->extradata = (uint8_t *)TI->CodecPrivate;
|
||||||
CodecContext->extradata_size = TI->CodecPrivateSize;
|
CodecContext->extradata_size = TI->CodecPrivateSize;
|
||||||
|
|
||||||
Codec = avcodec_find_decoder(MatroskaToFFCodecID(TI));
|
Codec = avcodec_find_decoder(MatroskaToFFCodecID(TI->CodecID, TI->CodecPrivate));
|
||||||
if (Codec == NULL) {
|
if (Codec == NULL) {
|
||||||
Free(false);
|
Free(false);
|
||||||
_snprintf(ErrorMsg, MsgSize, "Video codec not found");
|
_snprintf(ErrorMsg, MsgSize, "Video codec not found");
|
||||||
|
|
|
@ -219,12 +219,18 @@ AVSValue __cdecl CreateSWScale(AVSValue Args, void* UserData, IScriptEnvironment
|
||||||
return new SWScale(Args[0].AsClip(), Args[1].AsInt(0), Args[2].AsInt(0), Args[3].AsString("BICUBIC"), Args[4].AsString(""), Env);
|
return new SWScale(Args[0].AsClip(), Args[1].AsInt(0), Args[2].AsInt(0), Args[3].AsString("BICUBIC"), Args[4].AsString(""), Env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AVSValue __cdecl FFNoLog(AVSValue Args, void* UserData, IScriptEnvironment* Env) {
|
||||||
|
av_log_set_level(AV_LOG_QUIET);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" __declspec(dllexport) const char* __stdcall AvisynthPluginInit2(IScriptEnvironment* Env) {
|
extern "C" __declspec(dllexport) const char* __stdcall AvisynthPluginInit2(IScriptEnvironment* Env) {
|
||||||
Env->AddFunction("FFIndex", "[source]s[cachefile]s[indexmask]i[dumpmask]i[audiofile]s[overwrite]b", CreateFFIndex, 0);
|
Env->AddFunction("FFIndex", "[source]s[cachefile]s[indexmask]i[dumpmask]i[audiofile]s[overwrite]b", CreateFFIndex, 0);
|
||||||
Env->AddFunction("FFVideoSource", "[source]s[track]i[cache]b[cachefile]s[fpsnum]i[fpsden]i[pp]s[threads]i[timecodes]s[seekmode]i", CreateFFVideoSource, 0);
|
Env->AddFunction("FFVideoSource", "[source]s[track]i[cache]b[cachefile]s[fpsnum]i[fpsden]i[pp]s[threads]i[timecodes]s[seekmode]i", CreateFFVideoSource, 0);
|
||||||
Env->AddFunction("FFAudioSource", "[source]s[track]i[cache]b[cachefile]s", CreateFFAudioSource, 0);
|
Env->AddFunction("FFAudioSource", "[source]s[track]i[cache]b[cachefile]s", CreateFFAudioSource, 0);
|
||||||
Env->AddFunction("FFPP", "c[pp]s", CreateFFPP, 0);
|
Env->AddFunction("FFPP", "c[pp]s", CreateFFPP, 0);
|
||||||
Env->AddFunction("SWScale", "c[width]i[height]i[resizer]s[colorspace]s", CreateSWScale, 0);
|
Env->AddFunction("SWScale", "c[width]i[height]i[resizer]s[colorspace]s", CreateSWScale, 0);
|
||||||
|
Env->AddFunction("FFNoLog", "", FFNoLog, 0);
|
||||||
|
|
||||||
return "FFmpegSource - The Second Coming";
|
return "FFmpegSource - The Second Coming";
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,7 @@ FFMS_API(VideoBase *) FFMS_CreateVideoSource(const char *SourceFile, int Track,
|
||||||
switch (TrackIndices->Decoder) {
|
switch (TrackIndices->Decoder) {
|
||||||
case 0: return new FFVideoSource(SourceFile, Track, TrackIndices, PP, Threads, SeekMode, ErrorMsg, MsgSize);
|
case 0: return new FFVideoSource(SourceFile, Track, TrackIndices, PP, Threads, SeekMode, ErrorMsg, MsgSize);
|
||||||
case 1: return new MatroskaVideoSource(SourceFile, Track, TrackIndices, PP, Threads, ErrorMsg, MsgSize);
|
case 1: return new MatroskaVideoSource(SourceFile, Track, TrackIndices, PP, Threads, ErrorMsg, MsgSize);
|
||||||
|
case 2: return new HaaliTSVideoSource(SourceFile, Track, TrackIndices, PP, Threads, ErrorMsg, MsgSize);
|
||||||
default:
|
default:
|
||||||
_snprintf(ErrorMsg, MsgSize, "Unsupported format");
|
_snprintf(ErrorMsg, MsgSize, "Unsupported format");
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
|
@ -55,7 +55,7 @@ enum TrackType {
|
||||||
FFMS_TYPE_AUDIO = 1,
|
FFMS_TYPE_AUDIO = 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// PixelFormat declarations from avutil.h so external libraries don't necessarily have to include and ffmpeg headers
|
// PixelFormat declarations from avutil.h so external libraries don't necessarily have to include ffmpeg headers
|
||||||
enum FFMS_PixelFormat {
|
enum FFMS_PixelFormat {
|
||||||
FFMS_PIX_FMT_NONE= -1,
|
FFMS_PIX_FMT_NONE= -1,
|
||||||
FFMS_PIX_FMT_YUV420P, ///< Planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
|
FFMS_PIX_FMT_YUV420P, ///< Planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
|
||||||
|
|
|
@ -29,7 +29,7 @@ Opens files using ffmpeg and nothing else. May be frame accurate on good days. T
|
||||||
|
|
||||||
<h2>Usage</h2>
|
<h2>Usage</h2>
|
||||||
<p>
|
<p>
|
||||||
<b>FFIndex("string source, string cachefile = source + ".ffindex", int indexmask = 0, int dumpmask = 0, string audiofile = source, bool overwrite = false)</b><br />
|
<b>FFIndex(string source, string cachefile = source + ".ffindex", int indexmask = 0, int dumpmask = 0, string audiofile = source, bool overwrite = false)</b><br />
|
||||||
Used to invoke indexing separately and to write audio tracks to disk as wave64 files
|
Used to invoke indexing separately and to write audio tracks to disk as wave64 files
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -38,6 +38,11 @@ Opens files using ffmpeg and nothing else. May be frame accurate on good days. T
|
||||||
Opens video, will invoke indexing with the defaults if no preexisting index is found
|
Opens video, will invoke indexing with the defaults if no preexisting index is found
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<b>FFAudioSource(string source, int track, bool cache = true, string cachefile = source + ".ffindex")</b><br />
|
||||||
|
Opens audio, <b>if an index already exists it needs to contain a suitable audio index or empty audio will be returned</b>, will invoke indexing with the defaults if no preexisting index is found
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>FFPP(clip, string pp)</b><br />
|
<b>FFPP(clip, string pp)</b><br />
|
||||||
Separate postprocessing which also seems to include a few simple deinterlacers
|
Separate postprocessing which also seems to include a few simple deinterlacers
|
||||||
|
@ -48,6 +53,11 @@ Opens files using ffmpeg and nothing else. May be frame accurate on good days. T
|
||||||
Separate postprocessing which also seems to include a few simple deinterlacers
|
Separate postprocessing which also seems to include a few simple deinterlacers
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<b>FFNoLog()</b><br />
|
||||||
|
Disable all logging output from FFmpeg
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<b>source:</b>
|
<b>source:</b>
|
||||||
Source file.
|
Source file.
|
||||||
|
@ -191,13 +201,19 @@ Note that --enable-w32threads is required for multithreaded decoding to work.
|
||||||
|
|
||||||
<h2>Changes</h2>
|
<h2>Changes</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>2.00 beta 4<ul>
|
||||||
|
<li>Added the function FFNoLog which suppresses all messages from ffmpeg</li>
|
||||||
|
<li>Experimental new TS parsing using Haali's splitter (with bugs)</li>
|
||||||
|
<li>Everything is now compiled with VS2008 and GCC 4.3.2</li>
|
||||||
|
<li>Updated FFmpeg to rev 16383 (no libfaad2 this time)</li>
|
||||||
|
</ul></li>
|
||||||
|
|
||||||
<li>2.00 beta 3<ul>
|
<li>2.00 beta 3<ul>
|
||||||
<li>Compiled with libfaad2 again (has anyone seen a single aac file lavc can open right now?)</li>
|
<li>Compiled with libfaad2 again (has anyone seen a single aac file lavc can open right now?)</li>
|
||||||
<li>More API changes (and even more are likely to come)</li>
|
<li>More API changes (and even more are likely to come)</li>
|
||||||
<li>Several access violations and memory leaks on opening and indexing files fixed</li>
|
<li>Several access violations and memory leaks on opening and indexing files fixed</li>
|
||||||
<li>Added a VFR to CFR mode</li>
|
<li>Added a VFR to CFR mode</li>
|
||||||
<li>Readded FFAudioSource support for other containers (glitches still present now and then but no separate raw cache is required and possibly less buggy</li>
|
<li>Readded FFAudioSource support for other containers (glitches still present now and then but no separate raw cache is required and possibly less buggy)</li>
|
||||||
<li>Renamed the dll to FFMS2.dll, FFMS2 is now the official short name of the project</li>
|
<li>Renamed the dll to FFMS2.dll, FFMS2 is now the official short name of the project</li>
|
||||||
<li>Updated FFmpeg to rev 15522</li>
|
<li>Updated FFmpeg to rev 15522</li>
|
||||||
</ul></li>
|
</ul></li>
|
||||||
|
@ -220,4 +236,3 @@ Note that --enable-w32threads is required for multithreaded decoding to work.
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -327,7 +327,7 @@ AVFrameLite *FFVideoSource::GetFrame(int n, char *ErrorMsg, unsigned MsgSize) {
|
||||||
} else {
|
} else {
|
||||||
// 10 frames is used as a margin to prevent excessive seeking since the predicted best keyframe isn't always selected by avformat
|
// 10 frames is used as a margin to prevent excessive seeking since the predicted best keyframe isn't always selected by avformat
|
||||||
if (n < CurrentFrame || ClosestKF > CurrentFrame + 10 || (SeekMode == 3 && n > CurrentFrame + 10)) {
|
if (n < CurrentFrame || ClosestKF > CurrentFrame + 10 || (SeekMode == 3 && n > CurrentFrame + 10)) {
|
||||||
av_seek_frame(FormatContext, VideoTrack, (SeekMode == 3) ? Frames[n].DTS : Frames[ClosestKF].DTS, AVSEEK_FLAG_BACKWARD);
|
av_seek_frame(FormatContext, VideoTrack, (SeekMode == 3) ? Frames[n].DTS : Frames[FFMAX(ClosestKF - 20, 0)].DTS, AVSEEK_FLAG_BACKWARD);
|
||||||
avcodec_flush_buffers(CodecContext);
|
avcodec_flush_buffers(CodecContext);
|
||||||
HasSeeked = true;
|
HasSeeked = true;
|
||||||
}
|
}
|
||||||
|
@ -429,7 +429,7 @@ MatroskaVideoSource::MatroskaVideoSource(const char *SourceFile, int Track,
|
||||||
CodecContext->extradata_size = TI->CodecPrivateSize;
|
CodecContext->extradata_size = TI->CodecPrivateSize;
|
||||||
CodecContext->thread_count = Threads;
|
CodecContext->thread_count = Threads;
|
||||||
|
|
||||||
Codec = avcodec_find_decoder(MatroskaToFFCodecID(TI));
|
Codec = avcodec_find_decoder(MatroskaToFFCodecID(TI->CodecID, TI->CodecPrivate));
|
||||||
if (Codec == NULL) {
|
if (Codec == NULL) {
|
||||||
Free(false);
|
Free(false);
|
||||||
_snprintf(ErrorMsg, MsgSize, "Video codec not found");
|
_snprintf(ErrorMsg, MsgSize, "Video codec not found");
|
||||||
|
@ -560,3 +560,232 @@ AVFrameLite *MatroskaVideoSource::GetFrame(int n, char *ErrorMsg, unsigned MsgSi
|
||||||
LastFrameNum = n;
|
LastFrameNum = n;
|
||||||
return OutputFrame(DecodeFrame);
|
return OutputFrame(DecodeFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HaaliTSVideoSource::Free(bool CloseCodec) {
|
||||||
|
if (CloseCodec)
|
||||||
|
avcodec_close(CodecContext);
|
||||||
|
av_free(CodecContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
HaaliTSVideoSource::HaaliTSVideoSource(const char *SourceFile, int Track,
|
||||||
|
FrameIndex *TrackIndices, const char *PP,
|
||||||
|
int Threads, char *ErrorMsg, unsigned MsgSize) {
|
||||||
|
|
||||||
|
AVCodec *Codec = NULL;
|
||||||
|
CodecContext = NULL;
|
||||||
|
VideoTrack = Track;
|
||||||
|
Frames = (*TrackIndices)[VideoTrack];
|
||||||
|
|
||||||
|
if (Frames.size() == 0) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Video track contains no frames");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||||
|
|
||||||
|
CLSID clsid = Haali_TS_Parser;
|
||||||
|
|
||||||
|
if (FAILED(pMMC.CoCreateInstance(clsid))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create parser");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IMemAlloc> pMA;
|
||||||
|
if (FAILED(pMA.CoCreateInstance(CLSID_MemAlloc))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create memory allocator");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IMMStream> pMS;
|
||||||
|
if (FAILED(pMS.CoCreateInstance(CLSID_DiskFile))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create disk file reader");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR WSourceFile[2048];
|
||||||
|
mbstowcs(WSourceFile, SourceFile, 2000);
|
||||||
|
CComQIPtr<IMMStreamOpen> pMSO(pMS);
|
||||||
|
if (FAILED(pMSO->Open(WSourceFile))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't open file");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED(pMMC->Open(pMS, 0, NULL, pMA))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't parse file");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
BSTR CodecID = NULL;
|
||||||
|
uint8_t * CodecPrivate = NULL;
|
||||||
|
int CodecPrivateSize = 0;
|
||||||
|
int CurrentTrack = 0;
|
||||||
|
CComPtr<IEnumUnknown> pEU;
|
||||||
|
if (SUCCEEDED(pMMC->EnumTracks(&pEU))) {
|
||||||
|
CComPtr<IUnknown> pU;
|
||||||
|
while (pEU->Next(1, &pU, NULL) == S_OK) {
|
||||||
|
if (CurrentTrack++ == Track) {
|
||||||
|
CComQIPtr<IPropertyBag> pBag = pU;
|
||||||
|
if (pBag) {
|
||||||
|
VARIANT pV;
|
||||||
|
|
||||||
|
pV.vt = VT_EMPTY;
|
||||||
|
if (pBag->Read(L"CodecID", &pV, NULL) == S_OK)
|
||||||
|
CodecID = pV.bstrVal;
|
||||||
|
|
||||||
|
pV.vt = VT_EMPTY;
|
||||||
|
if (pBag->Read(L"CodecPrivate", &pV, NULL) == S_OK) {
|
||||||
|
CodecPrivate = (uint8_t *)pV.parray->pvData;
|
||||||
|
CodecPrivateSize = pV.parray->cbElements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pU = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CodecContext = avcodec_alloc_context();
|
||||||
|
CodecContext->extradata = CodecPrivate;
|
||||||
|
CodecContext->extradata_size = CodecPrivateSize;
|
||||||
|
CodecContext->thread_count = Threads;
|
||||||
|
|
||||||
|
char ACodecID[2048];
|
||||||
|
wcstombs(ACodecID, CodecID, 2000);
|
||||||
|
Codec = avcodec_find_decoder(MatroskaToFFCodecID(ACodecID, CodecPrivate));
|
||||||
|
if (Codec == NULL) {
|
||||||
|
Free(false);
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Video codec not found");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open(CodecContext, Codec) < 0) {
|
||||||
|
Free(false);
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Could not open video codec");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always try to decode a frame to make sure all required parameters are known
|
||||||
|
int64_t Dummy;
|
||||||
|
if (DecodeNextFrame(DecodeFrame, &Dummy, ErrorMsg, MsgSize)) {
|
||||||
|
Free(true);
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
VP.Width = CodecContext->width;
|
||||||
|
VP.Height = CodecContext->height;;
|
||||||
|
VP.FPSDenominator = 1;
|
||||||
|
VP.FPSNumerator = 30;
|
||||||
|
VP.NumFrames = Frames.size();
|
||||||
|
VP.PixelFormat = CodecContext->pix_fmt;
|
||||||
|
VP.FirstTime = ((Frames.front().DTS * Frames.TB.Num) / (double)Frames.TB.Den) / 1000;
|
||||||
|
VP.LastTime = ((Frames.back().DTS * Frames.TB.Num) / (double)Frames.TB.Den) / 1000;
|
||||||
|
|
||||||
|
if (VP.Width <= 0 || VP.Height <= 0) {
|
||||||
|
Free(true);
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Codec returned zero size video");
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InitPP(PP, CodecContext->pix_fmt, ErrorMsg, MsgSize)) {
|
||||||
|
Free(true);
|
||||||
|
throw ErrorMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the average framerate
|
||||||
|
if (Frames.size() >= 2) {
|
||||||
|
double DTSDiff = (double)(Frames.back().DTS - Frames.front().DTS);
|
||||||
|
// FIXME
|
||||||
|
VP.FPSDenominator = (unsigned int)((DTSDiff * 1000000000) / (double)1000 / (double)(VP.NumFrames - 1) + 0.5);
|
||||||
|
VP.FPSNumerator = 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the already decoded frame so it isn't wasted
|
||||||
|
OutputFrame(DecodeFrame);
|
||||||
|
LastFrameNum = 0;
|
||||||
|
|
||||||
|
// Set AR variables
|
||||||
|
// VP.SARNum = TI->AV.Video.DisplayWidth * TI->AV.Video.PixelHeight;
|
||||||
|
// VP.SARDen = TI->AV.Video.DisplayHeight * TI->AV.Video.PixelWidth;
|
||||||
|
|
||||||
|
// Set crop variables
|
||||||
|
// VP.CropLeft = TI->AV.Video.CropL;
|
||||||
|
// VP.CropRight = TI->AV.Video.CropR;
|
||||||
|
// VP.CropTop = TI->AV.Video.CropT;
|
||||||
|
// VP.CropBottom = TI->AV.Video.CropB;
|
||||||
|
}
|
||||||
|
|
||||||
|
HaaliTSVideoSource::~HaaliTSVideoSource() {
|
||||||
|
Free(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int HaaliTSVideoSource::DecodeNextFrame(AVFrame *AFrame, int64_t *AFirstStartTime, char *ErrorMsg, unsigned MsgSize) {
|
||||||
|
int FrameFinished = 0;
|
||||||
|
*AFirstStartTime = -1;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
CComPtr<IMMFrame> pMMF;
|
||||||
|
if (pMMC->ReadFrame(NULL, &pMMF) != S_OK)
|
||||||
|
break;
|
||||||
|
|
||||||
|
REFERENCE_TIME Ts, Te;
|
||||||
|
if (*AFirstStartTime < 0 && SUCCEEDED(pMMF->GetTime(&Ts, &Te)))
|
||||||
|
*AFirstStartTime = Ts;
|
||||||
|
|
||||||
|
if (pMMF->GetTrack() == VideoTrack) {
|
||||||
|
BYTE *Data = NULL;
|
||||||
|
if (FAILED(pMMF->GetPointer(&Data)))
|
||||||
|
goto Error;
|
||||||
|
|
||||||
|
avcodec_decode_video(CodecContext, AFrame, &FrameFinished, Data, pMMF->GetActualDataLength());
|
||||||
|
|
||||||
|
if (FrameFinished)
|
||||||
|
goto Done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the last frames
|
||||||
|
if (CodecContext->has_b_frames)
|
||||||
|
avcodec_decode_video(CodecContext, AFrame, &FrameFinished, NULL, 0);
|
||||||
|
|
||||||
|
if (!FrameFinished)
|
||||||
|
goto Error;
|
||||||
|
|
||||||
|
Error:
|
||||||
|
Done:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFrameLite *HaaliTSVideoSource::GetFrame(int n, char *ErrorMsg, unsigned MsgSize) {
|
||||||
|
// PPFrame always holds frame LastFrameNum even if no PP is applied
|
||||||
|
if (LastFrameNum == n)
|
||||||
|
return OutputFrame(DecodeFrame);
|
||||||
|
|
||||||
|
bool HasSeeked = false;
|
||||||
|
|
||||||
|
if (n < CurrentFrame || Frames.FindClosestKeyFrame(n) > CurrentFrame) {
|
||||||
|
int64_t dtsp = Frames[n].DTS;
|
||||||
|
pMMC->Seek(Frames[n].DTS, MMSF_PREV_KF);
|
||||||
|
// FIXME for some reason required to make it seek properly
|
||||||
|
//avcodec_flush_buffers(CodecContext);
|
||||||
|
HasSeeked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
int64_t StartTime;
|
||||||
|
if (DecodeNextFrame(DecodeFrame, &StartTime, ErrorMsg, MsgSize))
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (HasSeeked) {
|
||||||
|
HasSeeked = false;
|
||||||
|
|
||||||
|
if (StartTime < 0 || (CurrentFrame = Frames.FrameFromDTS(StartTime)) < 0) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Frame accurate seeking is not possible in this file\n");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentFrame++;
|
||||||
|
} while (CurrentFrame <= n);
|
||||||
|
|
||||||
|
LastFrameNum = n;
|
||||||
|
return OutputFrame(DecodeFrame);
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,17 @@ extern "C" {
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "ffms.h"
|
#include "ffms.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# define _WIN32_DCOM
|
||||||
|
# include <windows.h>
|
||||||
|
# include <tchar.h>
|
||||||
|
# include <atlbase.h>
|
||||||
|
# include <dshow.h>
|
||||||
|
# include "CoParser.h"
|
||||||
|
# include <initguid.h>
|
||||||
|
# include "guids.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
class VideoBase {
|
class VideoBase {
|
||||||
private:
|
private:
|
||||||
pp_context_t *PPContext;
|
pp_context_t *PPContext;
|
||||||
|
@ -90,4 +101,16 @@ public:
|
||||||
AVFrameLite *GetFrame(int n, char *ErrorMsg, unsigned MsgSize);
|
AVFrameLite *GetFrame(int n, char *ErrorMsg, unsigned MsgSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class HaaliTSVideoSource : public VideoBase {
|
||||||
|
private:
|
||||||
|
CComPtr<IMMContainer> pMMC;
|
||||||
|
|
||||||
|
void Free(bool CloseCodec);
|
||||||
|
int DecodeNextFrame(AVFrame *AFrame, int64_t *AFirstStartTime, char *ErrorMsg, unsigned MsgSize);
|
||||||
|
public:
|
||||||
|
HaaliTSVideoSource(const char *SourceFile, int Track, FrameIndex *TrackIndices, const char *PP, int Threads, char *ErrorMsg, unsigned MsgSize);
|
||||||
|
~HaaliTSVideoSource();
|
||||||
|
AVFrameLite *GetFrame(int n, char *ErrorMsg, unsigned MsgSize);
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2004-2008 Mike Matsnev. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* $Id: guids.h,v 1.16 2008/03/29 15:41:30 mike Exp $
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef GUIDS_H
|
||||||
|
#define GUIDS_H
|
||||||
|
|
||||||
|
// {941A4793-A705-4312-8DFC-C11CA05F397E}
|
||||||
|
DEFINE_GUID(CLSID_RealAudioDecoder,
|
||||||
|
0x941A4793, 0xA705, 0x4312, 0x8D, 0xFC, 0xC1, 0x1C, 0xA0, 0x5F, 0x39, 0x7E);
|
||||||
|
|
||||||
|
// {B8CBBAD8-AA1A-4cea-B95E-730041A55EF0}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_WavpackHybrid,
|
||||||
|
0xb8cbbad8, 0xaa1a, 0x4cea, 0xb9, 0x5e, 0x73, 0x0, 0x41, 0xa5, 0x5e, 0xf0);
|
||||||
|
|
||||||
|
// {1541C5C0-CDDF-477d-BC0A-86F8AE7F8354}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_FLAC_FRAMED,
|
||||||
|
0x1541c5c0, 0xcddf, 0x477d, 0xbc, 0xa, 0x86, 0xf8, 0xae, 0x7f, 0x83, 0x54);
|
||||||
|
|
||||||
|
// {8D2FD10B-5841-4a6b-8905-588FEC1ADED9}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_Vorbis2,
|
||||||
|
0x8d2fd10b, 0x5841, 0x4a6b, 0x89, 0x5, 0x58, 0x8f, 0xec, 0x1a, 0xde, 0xd9);
|
||||||
|
|
||||||
|
// {B36E107F-A938-4387-93C7-55E966757473}
|
||||||
|
DEFINE_GUID(FORMAT_VorbisFormat2,
|
||||||
|
0xb36e107f, 0xa938, 0x4387, 0x93, 0xc7, 0x55, 0xe9, 0x66, 0x75, 0x74, 0x73);
|
||||||
|
|
||||||
|
// {E487EB08-6B26-4be9-9DD3-993434D313FD}
|
||||||
|
DEFINE_GUID(MEDIATYPE_Subtitle,
|
||||||
|
0xe487eb08, 0x6b26, 0x4be9, 0x9d, 0xd3, 0x99, 0x34, 0x34, 0xd3, 0x13, 0xfd);
|
||||||
|
|
||||||
|
// {87C0B230-03A8-4fdf-8010-B27A5848200D}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_UTF8,
|
||||||
|
0x87c0b230, 0x3a8, 0x4fdf, 0x80, 0x10, 0xb2, 0x7a, 0x58, 0x48, 0x20, 0xd);
|
||||||
|
|
||||||
|
// {3020560F-255A-4ddc-806E-6C5CC6DCD70A}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_SSA,
|
||||||
|
0x3020560f, 0x255a, 0x4ddc, 0x80, 0x6e, 0x6c, 0x5c, 0xc6, 0xdc, 0xd7, 0xa);
|
||||||
|
|
||||||
|
// {326444F7-686F-47ff-A4B2-C8C96307B4C2}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_ASS,
|
||||||
|
0x326444f7, 0x686f, 0x47ff, 0xa4, 0xb2, 0xc8, 0xc9, 0x63, 0x7, 0xb4, 0xc2);
|
||||||
|
|
||||||
|
// {370689E7-B226-4f67-978D-F10BC1A9C6AE}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_ASS2,
|
||||||
|
0x370689e7, 0xb226, 0x4f67, 0x97, 0x8d, 0xf1, 0xb, 0xc1, 0xa9, 0xc6, 0xae);
|
||||||
|
|
||||||
|
// {B753B29A-0A96-45be-985F-68351D9CAB90}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_USF,
|
||||||
|
0xb753b29a, 0xa96, 0x45be, 0x98, 0x5f, 0x68, 0x35, 0x1d, 0x9c, 0xab, 0x90);
|
||||||
|
|
||||||
|
// {F7239E31-9599-4e43-8DD5-FBAF75CF37F1}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_VOBSUB,
|
||||||
|
0xf7239e31, 0x9599, 0x4e43, 0x8d, 0xd5, 0xfb, 0xaf, 0x75, 0xcf, 0x37, 0xf1);
|
||||||
|
|
||||||
|
// {0B10E53F-ABF9-4581-BE9C-2C9A5EC6F2E0}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_VOBSUB2,
|
||||||
|
0xb10e53f, 0xabf9, 0x4581, 0xbe, 0x9c, 0x2c, 0x9a, 0x5e, 0xc6, 0xf2, 0xe0);
|
||||||
|
|
||||||
|
// {5965E924-63F9-4a64-B71E-F75188FD6384}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_DXRSub,
|
||||||
|
0x5965e924, 0x63f9, 0x4a64, 0xb7, 0x1e, 0xf7, 0x51, 0x88, 0xfd, 0x63, 0x84);
|
||||||
|
|
||||||
|
// {A33D2F7D-96BC-4337-B23B-A8B9FBC295E9}
|
||||||
|
DEFINE_GUID(FORMAT_SubtitleInfo,
|
||||||
|
0xa33d2f7d, 0x96bc, 0x4337, 0xb2, 0x3b, 0xa8, 0xb9, 0xfb, 0xc2, 0x95, 0xe9);
|
||||||
|
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_AC3,
|
||||||
|
0x00002000, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71);
|
||||||
|
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_EAC3,
|
||||||
|
0x0000EAC3, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71);
|
||||||
|
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_DTS,
|
||||||
|
0x00002001, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71);
|
||||||
|
|
||||||
|
// {B855F837-194F-4d9a-9358-E31ED6B49D03}
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_VC1,
|
||||||
|
0xb855f837, 0x194f, 0x4d9a, 0x93, 0x58, 0xe3, 0x1e, 0xd6, 0xb4, 0x9d, 0x3);
|
||||||
|
|
||||||
|
// {93A22E7A-5091-45ef-BA61-6DA26156A5D0}
|
||||||
|
DEFINE_GUID(CLSID_DirectVobSubFilter,
|
||||||
|
0x93a22e7a, 0x5091, 0x45ef, 0xba, 0x61, 0x6d, 0xa2, 0x61, 0x56, 0xa5, 0xd0);
|
||||||
|
|
||||||
|
// {9852A670-F845-491b-9BE6-EBD841B8A613}
|
||||||
|
DEFINE_GUID(CLSID_DirectVobSubFilter2,
|
||||||
|
0x9852a670, 0xf845, 0x491b, 0x9b, 0xe6, 0xeb, 0xd8, 0x41, 0xb8, 0xa6, 0x13);
|
||||||
|
|
||||||
|
// {F6D90F11-9C73-11D3-B32E-00C04F990BB4}
|
||||||
|
DEFINE_GUID(CLSID_MSXML2,
|
||||||
|
0xF6D90F11, 0x9C73, 0x11D3, 0xB3, 0x2E, 0x00, 0xC0, 0x4F, 0x99, 0x0B, 0xB4);
|
||||||
|
|
||||||
|
// {9F062738-CD84-4F54-A3C4-BD5EB44F416B}
|
||||||
|
DEFINE_GUID(CLSID_SonicAudioDec,
|
||||||
|
0x9F062738, 0xCD84, 0x4F54, 0xA3, 0xC4, 0xBD, 0x5E, 0xB4, 0x4F, 0x41, 0x6B);
|
||||||
|
|
||||||
|
// {53D9DE0B-FC61-4650-9773-74D13CC7E582}
|
||||||
|
DEFINE_GUID(CLSID_DiskFile,
|
||||||
|
0x53d9de0b, 0xfc61, 0x4650, 0x97, 0x73, 0x74, 0xd1, 0x3c, 0xc7, 0xe5, 0x82);
|
||||||
|
|
||||||
|
// {BD4FB4BE-809D-487b-ADD6-F7D164247E52}
|
||||||
|
DEFINE_GUID(CLSID_HTTPStream,
|
||||||
|
0xbd4fb4be, 0x809d, 0x487b, 0xad, 0xd6, 0xf7, 0xd1, 0x64, 0x24, 0x7e, 0x52);
|
||||||
|
|
||||||
|
// {90C7D10E-CE9A-479b-A238-1A0F2396DE43}
|
||||||
|
DEFINE_GUID(CLSID_MemAlloc,
|
||||||
|
0x90c7d10e, 0xce9a, 0x479b, 0xa2, 0x38, 0x1a, 0xf, 0x23, 0x96, 0xde, 0x43);
|
||||||
|
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_WVC1,
|
||||||
|
0x31435657, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71);
|
||||||
|
|
||||||
|
DEFINE_GUID(MEDIASUBTYPE_WMV3,
|
||||||
|
0x33564d57, 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71);
|
||||||
|
|
||||||
|
// FIXME: move somewhere else?
|
||||||
|
DEFINE_GUID(Haali_TS_Parser,
|
||||||
|
0xB841F346, 0x4835, 0x4de8, 0xAA, 0x5E, 0x2E, 0x7C, 0xD2, 0xD4, 0xC4, 0x35);
|
||||||
|
|
||||||
|
typedef struct tagVORBISFORMAT2
|
||||||
|
{
|
||||||
|
DWORD Channels;
|
||||||
|
DWORD SamplesPerSec;
|
||||||
|
DWORD BitsPerSample;
|
||||||
|
DWORD HeaderSize[3]; // 0: Identification, 1: Comment, 2: Setup
|
||||||
|
} VORBISFORMAT2, *PVORBISFORMAT2, FAR *LPVORBISFORMAT2;
|
||||||
|
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
typedef struct {
|
||||||
|
DWORD dwOffset;
|
||||||
|
CHAR IsoLang[4]; // three letter lang code + terminating zero
|
||||||
|
WCHAR TrackName[256]; // 256 bytes ought to be enough for everyone :)
|
||||||
|
} SUBTITLEINFO;
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
#endif
|
|
@ -27,6 +27,24 @@
|
||||||
#include "indexing.h"
|
#include "indexing.h"
|
||||||
#include "wave64writer.h"
|
#include "wave64writer.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include "MatroskaParser.h"
|
||||||
|
#include "stdiostream.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# define _WIN32_DCOM
|
||||||
|
# include <windows.h>
|
||||||
|
# include <tchar.h>
|
||||||
|
# include <atlbase.h>
|
||||||
|
# include <dshow.h>
|
||||||
|
# include "CoParser.h"
|
||||||
|
# include <initguid.h>
|
||||||
|
# include "guids.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
class MatroskaAudioContext {
|
class MatroskaAudioContext {
|
||||||
public:
|
public:
|
||||||
Wave64Writer *W64W;
|
Wave64Writer *W64W;
|
||||||
|
@ -71,6 +89,26 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef HAALITS
|
||||||
|
class HaaliTSIndexMemory {
|
||||||
|
private:
|
||||||
|
int16_t *DecodingBuffer;
|
||||||
|
MatroskaAudioContext *AudioContexts;
|
||||||
|
public:
|
||||||
|
HaaliTSIndexMemory(int Tracks, int16_t *&DecodingBuffer, MatroskaAudioContext *&AudioContexts) {
|
||||||
|
DecodingBuffer = new int16_t[AVCODEC_MAX_AUDIO_FRAME_SIZE*10];
|
||||||
|
AudioContexts = new MatroskaAudioContext[Tracks];
|
||||||
|
this->DecodingBuffer = DecodingBuffer;
|
||||||
|
this->AudioContexts = AudioContexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
~HaaliTSIndexMemory() {
|
||||||
|
delete [] DecodingBuffer;
|
||||||
|
delete [] AudioContexts;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
class MatroskaIndexMemory {
|
class MatroskaIndexMemory {
|
||||||
private:
|
private:
|
||||||
int16_t *DecodingBuffer;
|
int16_t *DecodingBuffer;
|
||||||
|
@ -158,6 +196,203 @@ int WriteIndex(const char *IndexFile, FrameIndex *TrackIndices, char *ErrorMsg,
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAALITS
|
||||||
|
static FrameIndex *MakeHaaliTSIndex(const char *SourceFile, int IndexMask, int DumpMask, const char *AudioFile, bool IgnoreDecodeErrors, IndexCallback IP, void *Private, char *ErrorMsg, unsigned MsgSize) {
|
||||||
|
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||||
|
|
||||||
|
CLSID clsid = Haali_TS_Parser;
|
||||||
|
|
||||||
|
CComPtr<IMMContainer> pMMC;
|
||||||
|
if (FAILED(pMMC.CoCreateInstance(clsid))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create parser");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IMemAlloc> pMA;
|
||||||
|
if (FAILED(pMA.CoCreateInstance(CLSID_MemAlloc))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create memory allocator");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IMMStream> pMS;
|
||||||
|
if (FAILED(pMS.CoCreateInstance(CLSID_DiskFile))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't create disk file reader");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR WSourceFile[2048];
|
||||||
|
mbstowcs(WSourceFile, SourceFile, 2000);
|
||||||
|
CComQIPtr<IMMStreamOpen> pMSO(pMS);
|
||||||
|
if (FAILED(pMSO->Open(WSourceFile))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't open file");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED(pMMC->Open(pMS, 0, NULL, pMA))) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Can't parse file");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int NumTracks = 0;
|
||||||
|
CComPtr<IEnumUnknown> pEU;
|
||||||
|
if (SUCCEEDED(pMMC->EnumTracks(&pEU))) {
|
||||||
|
CComPtr<IUnknown> pU;
|
||||||
|
while (pEU->Next(1, &pU, NULL) == S_OK) {
|
||||||
|
NumTracks++;
|
||||||
|
pU = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio stuff
|
||||||
|
|
||||||
|
int16_t *db;
|
||||||
|
MatroskaAudioContext *AudioContexts;
|
||||||
|
HaaliTSIndexMemory IM = HaaliTSIndexMemory(NumTracks, db, AudioContexts);
|
||||||
|
|
||||||
|
FrameIndex *TrackIndices = new FrameIndex();
|
||||||
|
TrackIndices->Decoder = 2;
|
||||||
|
|
||||||
|
int TrackTypes[32];
|
||||||
|
int CurrentTrack = 0;
|
||||||
|
pEU = NULL;
|
||||||
|
if (SUCCEEDED(pMMC->EnumTracks(&pEU))) {
|
||||||
|
CComPtr<IUnknown> pU;
|
||||||
|
while (pEU->Next(1, &pU, NULL) == S_OK) {
|
||||||
|
CComQIPtr<IPropertyBag> pBag = pU;
|
||||||
|
BSTR CodecID = NULL;
|
||||||
|
TrackTypes[CurrentTrack] = -200;
|
||||||
|
uint8_t * CodecPrivate = NULL;
|
||||||
|
int CodecPrivateSize = 0;
|
||||||
|
|
||||||
|
if (pBag) {
|
||||||
|
VARIANT pV;
|
||||||
|
|
||||||
|
pV.vt = VT_EMPTY;
|
||||||
|
if (pBag->Read(L"CodecID", &pV, NULL) == S_OK)
|
||||||
|
CodecID = pV.bstrVal;
|
||||||
|
|
||||||
|
pV.vt = VT_EMPTY;
|
||||||
|
if (pBag->Read(L"Type", &pV, NULL) == S_OK)
|
||||||
|
TrackTypes[CurrentTrack] = pV.uintVal;
|
||||||
|
|
||||||
|
pV.vt = VT_EMPTY;
|
||||||
|
if (pBag->Read(L"CodecPrivate", &pV, NULL) == S_OK) {
|
||||||
|
CodecPrivate = (uint8_t *)pV.parray->pvData;
|
||||||
|
CodecPrivateSize = pV.parray->cbElements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackIndices->push_back(FrameInfoVector(1, 1000000000, TrackTypes[CurrentTrack] - 1));
|
||||||
|
|
||||||
|
if (IndexMask & (1 << CurrentTrack) && TrackTypes[CurrentTrack] == TT_AUDIO) {
|
||||||
|
AVCodecContext *AudioCodecContext = avcodec_alloc_context();
|
||||||
|
AudioCodecContext->extradata = CodecPrivate;
|
||||||
|
AudioCodecContext->extradata_size = CodecPrivateSize;
|
||||||
|
AudioContexts[CurrentTrack].CTX = AudioCodecContext;
|
||||||
|
|
||||||
|
char ACodecID[2048];
|
||||||
|
wcstombs(ACodecID, CodecID, 2000);
|
||||||
|
AVCodec *AudioCodec = avcodec_find_decoder(MatroskaToFFCodecID(ACodecID, NULL));
|
||||||
|
if (AudioCodec == NULL) {
|
||||||
|
av_free(AudioCodecContext);
|
||||||
|
AudioContexts[CurrentTrack].CTX = NULL;
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Audio codec not found");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open(AudioCodecContext, AudioCodec) < 0) {
|
||||||
|
av_free(AudioCodecContext);
|
||||||
|
AudioContexts[CurrentTrack].CTX = NULL;
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Could not open audio codec");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IndexMask &= ~(1 << CurrentTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
pU = NULL;
|
||||||
|
CurrentTrack++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
if (IP) {
|
||||||
|
if ((*IP)(0, 0, 1, Private)) {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Cancelled by user");
|
||||||
|
delete TrackIndices;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IMMFrame> pMMF;
|
||||||
|
if (pMMC->ReadFrame(NULL, &pMMF) != S_OK)
|
||||||
|
break;
|
||||||
|
|
||||||
|
REFERENCE_TIME Ts, Te;
|
||||||
|
HRESULT hr = pMMF->GetTime(&Ts, &Te);
|
||||||
|
|
||||||
|
unsigned int CurrentTrack = pMMF->GetTrack();
|
||||||
|
|
||||||
|
// Only create index entries for video for now to save space
|
||||||
|
if (TrackTypes[CurrentTrack] == TT_VIDEO) {
|
||||||
|
(*TrackIndices)[CurrentTrack].push_back(FrameInfo(Ts, pMMF->IsSyncPoint() == S_OK));
|
||||||
|
} else if (TrackTypes[CurrentTrack] == TT_AUDIO && (IndexMask & (1 << CurrentTrack))) {
|
||||||
|
/* (*TrackIndices)[Track].push_back(FrameInfo(AudioContexts[Track].CurrentSample, FilePos, FrameSize, (FrameFlags & FRAME_KF) != 0));
|
||||||
|
ReadFrame(FilePos, FrameSize, AudioContexts[Track].CS, MC, ErrorMsg, MsgSize);
|
||||||
|
|
||||||
|
int Size = FrameSize;
|
||||||
|
uint8_t *Data = MC.Buffer;
|
||||||
|
AVCodecContext *AudioCodecContext = AudioContexts[Track].CTX;
|
||||||
|
|
||||||
|
while (Size > 0) {
|
||||||
|
int dbsize = AVCODEC_MAX_AUDIO_FRAME_SIZE*10;
|
||||||
|
int Ret = avcodec_decode_audio2(AudioCodecContext, db, &dbsize, Data, Size);
|
||||||
|
if (Ret < 0) {
|
||||||
|
if (IgnoreDecodeErrors) {
|
||||||
|
(*TrackIndices)[Track].clear();
|
||||||
|
IndexMask &= ~(1 << Track);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
_snprintf(ErrorMsg, MsgSize, "Audio decoding error");
|
||||||
|
delete TrackIndices;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Ret > 0) {
|
||||||
|
Size -= Ret;
|
||||||
|
Data += Ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbsize > 0)
|
||||||
|
AudioContexts[Track].CurrentSample += (dbsize * 8) / (av_get_bits_per_sample_format(AudioCodecContext->sample_fmt) * AudioCodecContext->channels);
|
||||||
|
|
||||||
|
if (dbsize > 0 && (DumpMask & (1 << Track))) {
|
||||||
|
// Delay writer creation until after an audio frame has been decoded. This ensures that all parameters are known when writing the headers.
|
||||||
|
if (!AudioContexts[Track].W64W) {
|
||||||
|
char ABuf[50];
|
||||||
|
std::string WN(AudioFile);
|
||||||
|
int Offset = StartTime * mkv_TruncFloat(mkv_GetTrackInfo(MF, Track)->TimecodeScale) / (double)1000000;
|
||||||
|
_snprintf(ABuf, sizeof(ABuf), ".%02d.delay.%d.w64", Track, Offset);
|
||||||
|
WN += ABuf;
|
||||||
|
|
||||||
|
AudioContexts[Track].W64W = new Wave64Writer(WN.c_str(), av_get_bits_per_sample_format(AudioCodecContext->sample_fmt),
|
||||||
|
AudioCodecContext->channels, AudioCodecContext->sample_rate, AudioFMTIsFloat(AudioCodecContext->sample_fmt));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioContexts[Track].W64W->WriteData(db, dbsize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SortTrackIndices(TrackIndices);
|
||||||
|
return TrackIndices;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
static FrameIndex *MakeMatroskaIndex(const char *SourceFile, int IndexMask, int DumpMask, const char *AudioFile, bool IgnoreDecodeErrors, IndexCallback IP, void *Private, char *ErrorMsg, unsigned MsgSize) {
|
static FrameIndex *MakeMatroskaIndex(const char *SourceFile, int IndexMask, int DumpMask, const char *AudioFile, bool IgnoreDecodeErrors, IndexCallback IP, void *Private, char *ErrorMsg, unsigned MsgSize) {
|
||||||
MatroskaFile *MF;
|
MatroskaFile *MF;
|
||||||
char ErrorMessage[256];
|
char ErrorMessage[256];
|
||||||
|
@ -188,13 +423,14 @@ static FrameIndex *MakeMatroskaIndex(const char *SourceFile, int IndexMask, int
|
||||||
MatroskaIndexMemory IM = MatroskaIndexMemory(mkv_GetNumTracks(MF), db, AudioContexts, MF, &MC);
|
MatroskaIndexMemory IM = MatroskaIndexMemory(mkv_GetNumTracks(MF), db, AudioContexts, MF, &MC);
|
||||||
|
|
||||||
for (unsigned int i = 0; i < mkv_GetNumTracks(MF); i++) {
|
for (unsigned int i = 0; i < mkv_GetNumTracks(MF); i++) {
|
||||||
if (IndexMask & (1 << i) && mkv_GetTrackInfo(MF, i)->Type == TT_AUDIO) {
|
TrackInfo *TI = mkv_GetTrackInfo(MF, i);
|
||||||
|
if (IndexMask & (1 << i) && TI->Type == TT_AUDIO) {
|
||||||
AVCodecContext *AudioCodecContext = avcodec_alloc_context();
|
AVCodecContext *AudioCodecContext = avcodec_alloc_context();
|
||||||
AudioCodecContext->extradata = (uint8_t *)mkv_GetTrackInfo(MF, i)->CodecPrivate;
|
AudioCodecContext->extradata = (uint8_t *)TI->CodecPrivate;
|
||||||
AudioCodecContext->extradata_size = mkv_GetTrackInfo(MF, i)->CodecPrivateSize;
|
AudioCodecContext->extradata_size = TI->CodecPrivateSize;
|
||||||
AudioContexts[i].CTX = AudioCodecContext;
|
AudioContexts[i].CTX = AudioCodecContext;
|
||||||
|
|
||||||
if (mkv_GetTrackInfo(MF, i)->CompEnabled) {
|
if (TI->CompEnabled) {
|
||||||
AudioContexts[i].CS = cs_Create(MF, i, ErrorMessage, sizeof(ErrorMessage));
|
AudioContexts[i].CS = cs_Create(MF, i, ErrorMessage, sizeof(ErrorMessage));
|
||||||
if (AudioContexts[i].CS == NULL) {
|
if (AudioContexts[i].CS == NULL) {
|
||||||
av_free(AudioCodecContext);
|
av_free(AudioCodecContext);
|
||||||
|
@ -204,7 +440,7 @@ static FrameIndex *MakeMatroskaIndex(const char *SourceFile, int IndexMask, int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AVCodec *AudioCodec = avcodec_find_decoder(MatroskaToFFCodecID(mkv_GetTrackInfo(MF, i)));
|
AVCodec *AudioCodec = avcodec_find_decoder(MatroskaToFFCodecID(TI->CodecID, TI->CodecPrivate));
|
||||||
if (AudioCodec == NULL) {
|
if (AudioCodec == NULL) {
|
||||||
av_free(AudioCodecContext);
|
av_free(AudioCodecContext);
|
||||||
AudioContexts[i].CTX = NULL;
|
AudioContexts[i].CTX = NULL;
|
||||||
|
@ -320,6 +556,14 @@ FrameIndex *MakeIndex(const char *SourceFile, int IndexMask, int DumpMask, const
|
||||||
av_close_input_file(FormatContext);
|
av_close_input_file(FormatContext);
|
||||||
return MakeMatroskaIndex(SourceFile, IndexMask, DumpMask, AudioFile, IgnoreDecodeErrors, IP, Private, ErrorMsg, MsgSize);
|
return MakeMatroskaIndex(SourceFile, IndexMask, DumpMask, AudioFile, IgnoreDecodeErrors, IP, Private, ErrorMsg, MsgSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAALITS
|
||||||
|
// Do haali ts indexing instead?
|
||||||
|
if (!strcmp(FormatContext->iformat->name, "mpegts")) {
|
||||||
|
av_close_input_file(FormatContext);
|
||||||
|
return MakeHaaliTSIndex(SourceFile, IndexMask, DumpMask, AudioFile, IgnoreDecodeErrors, IP, Private, ErrorMsg, MsgSize);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (av_find_stream_info(FormatContext) < 0) {
|
if (av_find_stream_info(FormatContext) < 0) {
|
||||||
av_close_input_file(FormatContext);
|
av_close_input_file(FormatContext);
|
||||||
|
@ -521,6 +765,9 @@ int FrameInfoVector::ClosestFrameFromDTS(int64_t DTS) {
|
||||||
Frame = i;
|
Frame = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//int64_t tmp = at(2).DTS - at(1).DTS;
|
||||||
|
//ATLASSERT(BestDiff == 0);
|
||||||
return Frame;
|
return Frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,19 +21,11 @@
|
||||||
#ifndef INDEXING_H
|
#ifndef INDEXING_H
|
||||||
#define INDEXING_H
|
#define INDEXING_H
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
|
|
||||||
#include "MatroskaParser.h"
|
|
||||||
#include "stdiostream.h"
|
|
||||||
}
|
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "ffms.h"
|
#include "ffms.h"
|
||||||
|
|
||||||
#define INDEXVERSION 8
|
#define INDEXVERSION 11
|
||||||
#define INDEXID 0x53920873
|
#define INDEXID 0x53920873
|
||||||
|
|
||||||
struct IndexHeader {
|
struct IndexHeader {
|
||||||
|
|
|
@ -144,12 +144,11 @@ typedef struct BITMAPINFOHEADER {
|
||||||
((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) |\
|
((uint32_t)(uint8_t)(ch0) | ((uint32_t)(uint8_t)(ch1) << 8) |\
|
||||||
((uint32_t)(uint8_t)(ch2) << 16) | ((uint32_t)(uint8_t)(ch3) << 24 ))
|
((uint32_t)(uint8_t)(ch2) << 16) | ((uint32_t)(uint8_t)(ch3) << 24 ))
|
||||||
|
|
||||||
CodecID MatroskaToFFCodecID(TrackInfo *TI) {
|
CodecID MatroskaToFFCodecID(char *Codec, void *CodecPrivate) {
|
||||||
char *Codec = TI->CodecID;
|
|
||||||
/* Video Codecs */
|
/* Video Codecs */
|
||||||
if (!strcmp(Codec, "V_MS/VFW/FOURCC")) {
|
if (!strcmp(Codec, "V_MS/VFW/FOURCC")) {
|
||||||
// fourcc list from ffdshow
|
// fourcc list from ffdshow
|
||||||
switch (((BITMAPINFOHEADER *)TI->CodecPrivate)->biCompression) {
|
switch (((BITMAPINFOHEADER *)CodecPrivate)->biCompression) {
|
||||||
case MAKEFOURCC('F', 'F', 'D', 'S'):
|
case MAKEFOURCC('F', 'F', 'D', 'S'):
|
||||||
case MAKEFOURCC('F', 'V', 'F', 'W'):
|
case MAKEFOURCC('F', 'V', 'F', 'W'):
|
||||||
case MAKEFOURCC('X', 'V', 'I', 'D'):
|
case MAKEFOURCC('X', 'V', 'I', 'D'):
|
||||||
|
@ -379,6 +378,7 @@ CodecID MatroskaToFFCodecID(TrackInfo *TI) {
|
||||||
else if (!strcmp(Codec, "A_DTS"))
|
else if (!strcmp(Codec, "A_DTS"))
|
||||||
return CODEC_ID_DTS;
|
return CODEC_ID_DTS;
|
||||||
else if (!strcmp(Codec, "A_PCM/INT/LIT")) {
|
else if (!strcmp(Codec, "A_PCM/INT/LIT")) {
|
||||||
|
/* FIXME
|
||||||
switch (TI->AV.Audio.BitDepth) {
|
switch (TI->AV.Audio.BitDepth) {
|
||||||
case 8: return CODEC_ID_PCM_S8;
|
case 8: return CODEC_ID_PCM_S8;
|
||||||
case 16: return CODEC_ID_PCM_S16LE;
|
case 16: return CODEC_ID_PCM_S16LE;
|
||||||
|
@ -386,7 +386,10 @@ CodecID MatroskaToFFCodecID(TrackInfo *TI) {
|
||||||
case 32: return CODEC_ID_PCM_S32LE;
|
case 32: return CODEC_ID_PCM_S32LE;
|
||||||
default: return CODEC_ID_NONE;
|
default: return CODEC_ID_NONE;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
return CODEC_ID_NONE;
|
||||||
} else if (!strcmp(Codec, "A_PCM/INT/BIG")) {
|
} else if (!strcmp(Codec, "A_PCM/INT/BIG")) {
|
||||||
|
/* FIXME
|
||||||
switch (TI->AV.Audio.BitDepth) {
|
switch (TI->AV.Audio.BitDepth) {
|
||||||
case 8: return CODEC_ID_PCM_S8;
|
case 8: return CODEC_ID_PCM_S8;
|
||||||
case 16: return CODEC_ID_PCM_S16BE;
|
case 16: return CODEC_ID_PCM_S16BE;
|
||||||
|
@ -394,6 +397,8 @@ CodecID MatroskaToFFCodecID(TrackInfo *TI) {
|
||||||
case 32: return CODEC_ID_PCM_S32BE;
|
case 32: return CODEC_ID_PCM_S32BE;
|
||||||
default: return CODEC_ID_NONE;
|
default: return CODEC_ID_NONE;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
return CODEC_ID_NONE;
|
||||||
} else if (!strcmp(Codec, "A_PCM/FLOAT/IEEE"))
|
} else if (!strcmp(Codec, "A_PCM/FLOAT/IEEE"))
|
||||||
return CODEC_ID_PCM_F32LE; // only a most likely guess, may do bad things
|
return CODEC_ID_PCM_F32LE; // only a most likely guess, may do bad things
|
||||||
else if (!strcmp(Codec, "A_FLAC"))
|
else if (!strcmp(Codec, "A_FLAC"))
|
||||||
|
|
|
@ -22,13 +22,14 @@
|
||||||
#define UTILS_H
|
#define UTILS_H
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libswscale/swscale.h>
|
#include <libswscale/swscale.h>
|
||||||
#include <libpostproc/postprocess.h>
|
#include <libpostproc/postprocess.h>
|
||||||
|
#include <libavutil/sha1.h>
|
||||||
#include "MatroskaParser.h"
|
#include "MatroskaParser.h"
|
||||||
}
|
|
||||||
|
|
||||||
#include "stdiostream.h"
|
#include "stdiostream.h"
|
||||||
|
}
|
||||||
|
|
||||||
struct MatroskaReaderContext {
|
struct MatroskaReaderContext {
|
||||||
public:
|
public:
|
||||||
|
@ -50,6 +51,6 @@ public:
|
||||||
int GetCPUFlags();
|
int GetCPUFlags();
|
||||||
int ReadFrame(uint64_t FilePos, unsigned int &FrameSize, CompressedStream *CS, MatroskaReaderContext &Context, char *ErrorMsg, unsigned MsgSize);
|
int ReadFrame(uint64_t FilePos, unsigned int &FrameSize, CompressedStream *CS, MatroskaReaderContext &Context, char *ErrorMsg, unsigned MsgSize);
|
||||||
bool AudioFMTIsFloat(SampleFormat FMT);
|
bool AudioFMTIsFloat(SampleFormat FMT);
|
||||||
CodecID MatroskaToFFCodecID(TrackInfo *TI);
|
CodecID MatroskaToFFCodecID(char *Codec, void *CodecPrivate);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue