/*
* (C) 2003-2006 Gabest
* (C) 2006-2013 see Authors.txt
*
* This file is part of MPC-HC.
*
* MPC-HC is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* MPC-HC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "stdafx.h"
#include "AviFile.h"
#include
//
// CAviFile
//
CAviFile::CAviFile(IAsyncReader* pAsyncReader, HRESULT& hr)
: CBaseSplitterFile(pAsyncReader, hr)
{
if (FAILED(hr)) {
return;
}
m_isamv = false;
hr = Init();
}
HRESULT CAviFile::Init()
{
Seek(0);
DWORD dw[3];
if (S_OK != Read(dw) || dw[0] != FCC('RIFF') || (dw[2] != FCC('AVI ') && dw[2] != FCC('AVIX') && dw[2] != FCC('AMV '))) {
return E_FAIL;
}
m_isamv = (dw[2] == FCC('AMV '));
Seek(0);
HRESULT hr = Parse(0, GetLength());
UNREFERENCED_PARAMETER(hr);
if (m_movis.IsEmpty()) { // FAILED(hr) is allowed as long as there was a movi chunk found
return E_FAIL;
}
if (m_avih.dwStreams == 0 && !m_strms.IsEmpty()) {
m_avih.dwStreams = (DWORD)m_strms.GetCount();
}
if (m_avih.dwStreams != m_strms.GetCount()) {
return E_FAIL;
}
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
strm_t* s = m_strms[i];
if (s->strh.fccType != FCC('auds')) {
continue;
}
WAVEFORMATEX* wfe = (WAVEFORMATEX*)s->strf.GetData();
if (wfe->wFormatTag == 0x55 && wfe->nBlockAlign == 1152
&& s->strh.dwScale == 1 && s->strh.dwRate != wfe->nSamplesPerSec) {
// correcting encoder bugs...
s->strh.dwScale = 1152;
s->strh.dwRate = wfe->nSamplesPerSec;
}
}
if (!m_isamv && (FAILED(BuildIndex()))) {
EmptyIndex();
}
return S_OK;
}
HRESULT CAviFile::BuildAMVIndex()
{
strm_t::chunk NewChunk;
DWORD ulType;
DWORD ulSize;
memset(&NewChunk, 0, sizeof(strm_t::chunk));
while ((Read(ulType) == S_OK) && (Read(ulSize) == S_OK)) {
switch (ulType) {
case FCC('00dc'): // 01bw: JPeg
NewChunk.size = ulSize;
NewChunk.filepos = GetPos();
NewChunk.orgsize = ulSize;
NewChunk.fKeyFrame = true;
m_strms[0]->cs.Add(NewChunk);
break;
case FCC('01wb'): // 00dc: Audio
NewChunk.size = ulSize;
NewChunk.orgsize = ulSize;
NewChunk.fKeyFrame = true;
NewChunk.filepos = GetPos();
m_strms[1]->cs.Add(NewChunk);
break;
}
Seek(GetPos() + ulSize);
}
TRACE(_T("Video packet : %d Audio packet :%d\n"), m_strms[0]->cs.GetCount(), m_strms[1]->cs.GetCount());
return S_OK;
}
HRESULT CAviFile::Parse(DWORD parentid, __int64 end)
{
HRESULT hr = S_OK;
CAutoPtr strm;
while (S_OK == hr && GetPos() < end) {
UINT64 pos = GetPos();
DWORD id = 0, size;
if (S_OK != Read(id) || id == 0) {
return E_FAIL;
}
if (id == FCC('RIFF') || id == FCC('LIST')) {
if (S_OK != Read(size) || S_OK != Read(id)) {
return E_FAIL;
}
if (m_isamv) {
size = (DWORD)(min(end, DWORD_MAX) - GetPos() - 8); // No size set in AVM : guess end of file...
}
size += (size & 1) + 8;
TRACE(_T("CAviFile::Parse(..): LIST '%c%c%c%c'\n"),
TCHAR((id >> 0) & 0xff),
TCHAR((id >> 8) & 0xff),
TCHAR((id >> 16) & 0xff),
TCHAR((id >> 24) & 0xff));
if (id == FCC('movi')) {
m_movis.AddTail(pos);
if (m_isamv) {
BuildAMVIndex();
}
} else {
hr = Parse(id, pos + size);
}
} else {
if (S_OK != Read(size)) {
return E_FAIL;
}
if (GetPos() + size > end) {
return E_FAIL; // broken chunk
}
TRACE(_T("CAviFile::Parse(..): '%c%c%c%c'\n"),
TCHAR((id >> 0) & 0xff),
TCHAR((id >> 8) & 0xff),
TCHAR((id >> 16) & 0xff),
TCHAR((id >> 24) & 0xff));
if (parentid == FCC('INFO') && size > 0) {
switch (id) {
case FCC('IARL'): // Archival Location. Indicates where the subject of the file is archived.
case FCC('IART'): // Artist. Lists the artist of the original subject of the file; for example, "Michaelangelo."
case FCC('ICMS'): // Commissioned. Lists the name of the person or organization that commissioned the subject of the file; for example, "Pope Julian II."
case FCC('ICMT'): // Comments. Provides general comments about the file or the subject of the file. If the comment is several sentences long, end each sentence with a period. Do not include new-line characters.
case FCC('ICOP'): // Copyright. Records the copyright information for the file; for example, "Copyright Encyclopedia International 1991." If there are multiple copyrights, separate them by a semicolon followed by a space.
case FCC('ICRD'): // Creation date. Specifies the date the subject of the file was created. List dates in year-month-day format, padding one-digit months and days with a zero on the left; for example, "1553-05-03" for May 3, 1553.
case FCC('ICRP'): // Cropped. Describes whether an image has been cropped and, if so, how it was cropped; for example, "lower-right corner."
case FCC('IDIM'): // Dimensions. Specifies the size of the original subject of the file; for example, "8.5 in h, 11 in w."
case FCC('IDPI'): // Dots Per Inch. Stores dots per inch setting of the digitizer used to produce the file, such as "300."
case FCC('IENG'): // Engineer. Stores the name of the engineer who worked on the file. If there are multiple engineers, separate the names by a semicolon and a blank; for example, "Smith, John; Adams, Joe."
case FCC('IGNR'): // Genre. Describes the original work, such as "landscape," "portrait," "still life," etc.
case FCC('IKEY'): // Keywords. Provides a list of keywords that refer to the file or subject of the file. Separate multiple keywords with a semicolon and a blank; for example, "Seattle; aerial view; scenery."
case FCC('ILGT'): // Lightness. Describes the changes in lightness settings on the digitizer required to produce the file. Note that the format of this information depends on hardware used.
case FCC('IMED'): // Medium. Describes the original subject of the file, such as "computer image," "drawing," "lithograph," and so on.
case FCC('INAM'): // Name. Stores the title of the subject of the file, such as "Seattle From Above."
case FCC('IPLT'): // Palette Setting. Specifies the number of colors requested when digitizing an image, such as "256."
case FCC('IPRD'): // Product. Specifies the name of the title the file was originally intended for, such as "Encyclopedia of Pacific Northwest Geography."
case FCC('ISBJ'): // Subject. Describes the contents of the file, such as "Aerial view of Seattle."
case FCC('ISFT'): // Software. Identifies the name of the software package used to create the file, such as "Microsoft WaveEdit."
case FCC('ISHP'): // Sharpness. Identifies the changes in sharpness for the digitizer required to produce the file (the format depends on the hardware used).
case FCC('ISRC'): // Source. Identifies the name of the person or organization who supplied the original subject of the file; for example, "Trey Research."
case FCC('ISRF'): // Source Form. Identifies the original form of the material that was digitized, such as "slide," "paper," "map," and so on. This is not necessarily the same as IMED.
case FCC('ITCH'): { // Technician. Identifies the technician who digitized the subject file; for example, "Smith, John."
CStringA str;
if (S_OK != ByteRead((BYTE*)str.GetBufferSetLength(size), size)) {
return E_FAIL;
}
m_info[id] = str;
break;
}
}
}
switch (id) {
case FCC('amvh'):
case FCC('avih'):
m_avih.fcc = id;
m_avih.cb = size;
if (S_OK != Read(m_avih, 8)) {
return E_FAIL;
}
break;
case FCC('strh'):
if (!strm) {
strm.Attach(DEBUG_NEW strm_t());
}
strm->strh.fcc = FCC('strh');
strm->strh.cb = size;
if (S_OK != Read(strm->strh, 8)) {
return E_FAIL;
}
if (m_isamv) {
// First alway video, second always audio
strm->strh.fccType = m_strms.IsEmpty() ? FCC('vids') : FCC('amva');
strm->strh.dwRate = m_avih.dwReserved[0] * 1000; // dwReserved[0] = fps!
strm->strh.dwScale = 1000;
}
break;
case FCC('strn'):
if (S_OK != ByteRead((BYTE*)strm->strn.GetBufferSetLength(size), size)) {
return E_FAIL;
}
break;
case FCC('strf'):
if (!strm) {
strm.Attach(DEBUG_NEW strm_t());
}
strm->strf.SetCount(size);
if (S_OK != ByteRead(strm->strf.GetData(), size)) {
return E_FAIL;
}
if (m_isamv) {
if (strm->strh.fccType == FCC('vids')) {
strm->strf.SetCount(sizeof(BITMAPINFOHEADER));
BITMAPINFOHEADER* pbmi = &((BITMAPINFO*)strm->strf.GetData())->bmiHeader;
pbmi->biSize = sizeof(BITMAPINFOHEADER);
pbmi->biHeight = m_avih.dwHeight;
pbmi->biWidth = m_avih.dwWidth;
pbmi->biCompression = FCC('AMVV');
pbmi->biPlanes = 1;
pbmi->biBitCount = 24;
pbmi->biSizeImage = pbmi->biHeight * pbmi->biWidth * (pbmi->biBitCount / 8);
}
m_strms.Add(strm);
}
break;
case FCC('indx'):
if (!strm) {
strm.Attach(DEBUG_NEW strm_t());
}
ASSERT(strm->indx == nullptr);
AVISUPERINDEX* pSuperIndex;
if (size < MAXDWORD - 8) {
// Fix buffer overrun vulnerability
try {
pSuperIndex = (AVISUPERINDEX*)DEBUG_NEW unsigned char [(size_t)(size + 8)];
} catch (CMemoryException* e) {
pSuperIndex = nullptr;
e->Delete();
}
if (pSuperIndex) {
strm->indx.Attach(pSuperIndex);
strm->indx->fcc = FCC('indx');
strm->indx->cb = size;
if (S_OK != ByteRead((BYTE*)(AVISUPERINDEX*)strm->indx + 8, size)) {
return E_FAIL;
}
ASSERT(strm->indx->wLongsPerEntry == 4 && strm->indx->bIndexType == AVI_INDEX_OF_INDEXES);
}
}
break;
case FCC('dmlh'):
if (S_OK != Read(m_dmlh)) {
return E_FAIL;
}
break;
case FCC('vprp'):
//if (S_OK != Read(m_vprp)) return E_FAIL;
break;
case FCC('idx1'):
ASSERT(m_idx1 == nullptr);
m_idx1.Attach((AVIOLDINDEX*)DEBUG_NEW BYTE[size + 8]);
m_idx1->fcc = FCC('idx1');
m_idx1->cb = size;
if (S_OK != ByteRead((BYTE*)(AVIOLDINDEX*)m_idx1 + 8, size)) {
return E_FAIL;
}
break;
default:
TRACE(_T("CAviFile::Parse(..): unknown tag '%c%c%c%c'\n"),
TCHAR((id >> 0) & 0xff),
TCHAR((id >> 8) & 0xff),
TCHAR((id >> 16) & 0xff),
TCHAR((id >> 24) & 0xff));
break;
}
size += (size & 1) + 8;
}
Seek(pos + size);
}
if (strm) {
m_strms.Add(strm);
}
return hr;
}
REFERENCE_TIME CAviFile::GetTotalTime()
{
REFERENCE_TIME t = 0/*10i64*m_avih.dwMicroSecPerFrame*m_avih.dwTotalFrames*/;
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
strm_t* s = m_strms[i];
if (s->IsRawSubtitleStream()) {
continue;
}
REFERENCE_TIME t2 = s->GetRefTime((DWORD)s->cs.GetCount(), s->totalsize);
t = max(t, t2);
}
if (t == 0) {
t = 10i64 * m_avih.dwMicroSecPerFrame * m_avih.dwTotalFrames;
}
return t;
}
HRESULT CAviFile::BuildIndex()
{
EmptyIndex();
DWORD nSuperIndexes = 0;
for (DWORD track = 0; track < m_avih.dwStreams; track++) {
strm_t* s = m_strms[track];
if (s->indx && s->indx->nEntriesInUse > 0) {
nSuperIndexes++;
}
}
if (nSuperIndexes == m_avih.dwStreams) {
for (DWORD track = 0; track < m_avih.dwStreams; track++) {
strm_t* s = m_strms[track];
AVISUPERINDEX* idx = (AVISUPERINDEX*)s->indx;
DWORD nEntriesInUse = 0;
for (DWORD j = 0; j < idx->nEntriesInUse; ++j) {
Seek(idx->aIndex[j].qwOffset);
AVISTDINDEX stdidx;
if (S_OK != ByteRead((BYTE*)&stdidx, FIELD_OFFSET(AVISTDINDEX, aIndex))) {
EmptyIndex();
return E_FAIL;
}
nEntriesInUse += stdidx.nEntriesInUse;
}
s->cs.SetCount(nEntriesInUse);
DWORD frame = 0;
UINT64 size = 0;
for (DWORD j = 0; j < idx->nEntriesInUse; ++j) {
Seek(idx->aIndex[j].qwOffset);
CAutoPtr p((AVISTDINDEX*)DEBUG_NEW BYTE[idx->aIndex[j].dwSize]);
if (!p || S_OK != ByteRead((BYTE*)(AVISTDINDEX*)p, idx->aIndex[j].dwSize) || p->qwBaseOffset >= (DWORDLONG)GetLength()) {
EmptyIndex();
return E_FAIL;
}
for (DWORD k = 0; k < p->nEntriesInUse; ++k) {
s->cs[frame].size = size;
s->cs[frame].filepos = p->qwBaseOffset + p->aIndex[k].dwOffset;
s->cs[frame].fKeyFrame = !(p->aIndex[k].dwSize & AVISTDINDEX_DELTAFRAME)
|| s->strh.fccType == FCC('auds');
s->cs[frame].fChunkHdr = false;
s->cs[frame].orgsize = p->aIndex[k].dwSize & AVISTDINDEX_SIZEMASK;
if (m_idx1) {
s->cs[frame].filepos -= 8;
s->cs[frame].fChunkHdr = true;
}
++frame;
size += s->GetChunkSize(p->aIndex[k].dwSize & AVISTDINDEX_SIZEMASK);
}
}
s->totalsize = size;
}
} else if (AVIOLDINDEX* idx = m_idx1) {
size_t len = idx->cb / sizeof(idx->aIndex[0]);
UINT64 offset = m_movis.GetHead() + 8;
//detect absolute chunk addressing (TODO: read AVI specification and make it better)
if (idx->aIndex[0].dwOffset > offset) {
DWORD id;
Seek(offset + idx->aIndex[0].dwOffset);
Read(id);
if (id != idx->aIndex[0].dwChunkId) {
TRACE(_T("WARNING: CAviFile::Init() detected absolute chunk addressing in \'idx1\'\n"));
offset = 0;
}
}
for (DWORD track = 0; track < m_avih.dwStreams; track++) {
strm_t* s = m_strms[track];
// calculate the number of frames and set index size before using it
size_t nFrames = 0;
for (size_t i = 0; i < len; i++) {
if (TRACKNUM(idx->aIndex[i].dwChunkId) == track) {
nFrames++;
}
}
s->cs.SetCount(nFrames);
// read index
size_t frame = 0;
UINT64 size = 0;
for (size_t i = 0; i < len; i++) {
if (TRACKNUM(idx->aIndex[i].dwChunkId) == track) {
s->cs[frame].size = size;
s->cs[frame].filepos = offset + idx->aIndex[i].dwOffset;
s->cs[frame].fKeyFrame = (idx->aIndex[i].dwFlags & AVIIF_KEYFRAME)
|| s->strh.fccType == FCC('auds') // FIXME: some audio index is without any kf flag
|| frame == 0; // grrr
s->cs[frame].fChunkHdr = i + 1 == len || idx->aIndex[i].dwOffset != idx->aIndex[i + 1].dwOffset;
s->cs[frame].orgsize = idx->aIndex[i].dwSize;
frame++;
size += s->GetChunkSize(idx->aIndex[i].dwSize);
}
}
s->totalsize = size;
}
}
m_idx1.Free();
for (DWORD track = 0; track < m_avih.dwStreams; track++) {
m_strms[track]->indx.Free();
}
return S_OK;
}
void CAviFile::EmptyIndex()
{
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
strm_t* s = m_strms[i];
s->cs.RemoveAll();
s->totalsize = 0;
}
}
bool CAviFile::IsInterleaved(bool fKeepInfo)
{
if (m_strms.GetCount() < 2) {
return true;
}
/*
if (m_avih.dwFlags&AVIF_ISINTERLEAVED) // not reliable, nandub can write f*cked up files and still sets it
return true;
*/
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
m_strms[i]->cs2.SetCount(m_strms[i]->cs.GetCount());
}
DWORD* curchunks = DEBUG_NEW DWORD[m_avih.dwStreams];
UINT64* cursizes = DEBUG_NEW UINT64[m_avih.dwStreams];
memset(curchunks, 0, sizeof(DWORD)*m_avih.dwStreams);
memset(cursizes, 0, sizeof(UINT64)*m_avih.dwStreams);
int end = 0;
for (;;) {
UINT64 fpmin = _I64_MAX;
DWORD n = (DWORD) - 1;
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
DWORD curchunk = curchunks[i];
CAtlArray& cs = m_strms[i]->cs;
if (curchunk >= cs.GetCount()) {
continue;
}
UINT64 fp = cs[curchunk].filepos;
if (fp < fpmin) {
fpmin = fp;
n = i;
}
}
if (n == -1) {
break;
}
strm_t* s = m_strms[n];
DWORD& curchunk = curchunks[n];
UINT64& cursize = cursizes[n];
if (!s->IsRawSubtitleStream()) {
strm_t::chunk2& cs2 = s->cs2[curchunk];
cs2.t = (DWORD)(s->GetRefTime(curchunk, cursize) >> 13); // for comparing later it is just as good as /10000 to get a near [ms] accuracy
//cs2.t = (DWORD)(s->GetRefTime(curchunk, cursize)/10000);
cs2.n = end++;
}
cursize = s->cs[curchunk].size;
++curchunk;
}
memset(curchunks, 0, sizeof(DWORD)*m_avih.dwStreams);
strm_t::chunk2 cs2last = {(DWORD) - 1, 0};
bool fInterleaved = true;
while (fInterleaved) {
strm_t::chunk2 cs2min = {LONG_MAX, LONG_MAX};
DWORD n = (DWORD) - 1;
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
DWORD curchunk = curchunks[i];
if (curchunk >= m_strms[i]->cs2.GetCount()) {
continue;
}
strm_t::chunk2& cs2 = m_strms[i]->cs2[curchunk];
if (cs2.t < cs2min.t) {
cs2min = cs2;
n = i;
}
}
if (n == -1) {
break;
}
++curchunks[n];
if (cs2last.t != (DWORD) - 1 && abs((int)cs2min.n - (int)cs2last.n) >= 1000) {
fInterleaved = false;
}
cs2last = cs2min;
}
delete [] curchunks;
delete [] cursizes;
if (fInterleaved && !fKeepInfo) {
// this is not needed anymore, let's save a little memory then
for (DWORD i = 0; i < m_avih.dwStreams; ++i) {
m_strms[i]->cs2.RemoveAll();
}
}
return fInterleaved;
}
REFERENCE_TIME CAviFile::strm_t::GetRefTime(DWORD frame, UINT64 size)
{
if (strh.dwRate == 0) {
return 0;
}
if (strh.fccType == FCC('auds')) {
WAVEFORMATEX* wfe = (WAVEFORMATEX*)strf.GetData();
if (wfe->nBlockAlign == 0) {
return 0;
}
return (REFERENCE_TIME)ceil(10000000.0 * size * strh.dwScale / (unsigned __int64(strh.dwRate) * wfe->nBlockAlign));
// need calculate in double, because the (10000000ui64 * size * strh.dwScale) can give overflow
// "ceil" is necessary to compensate for framenumber->reftime->framenumber conversion
}
return (REFERENCE_TIME)ceil(10000000.0 * frame * strh.dwScale / strh.dwRate);
// need calculate in double, because the (10000000ui64 * frame * strh.dwScale) can give overflow (verified in practice)
// "ceil" is necessary to compensate for framenumber->reftime->framenumber conversion
}
DWORD CAviFile::strm_t::GetFrame(REFERENCE_TIME rt)
{
DWORD frame;
if (strh.dwScale == 0 || rt <= 0 || cs.IsEmpty()) {
frame = 0;
} else if (strh.fccType == FCC('auds')) {
WAVEFORMATEX* wfe = (WAVEFORMATEX*)strf.GetData();
UINT64 size = (UINT64)(double(rt) * wfe->nBlockAlign * strh.dwRate / (strh.dwScale * 10000000.0));
// need calculate in double, because the (rt * wfe->nBlockAlign * strh.dwRate) can give overflow
frame = 1;
for (; frame < cs.GetCount(); ++frame) {
if (cs[frame].size > size) {
break;
}
}
--frame;
} else {
frame = (DWORD)(double(rt) * strh.dwRate / (strh.dwScale * 10000000.0));
// need calculate in double, because the (rt * strh.dwRate) can give overflow (verified in practice)
if (frame >= cs.GetCount()) {
frame = (DWORD)cs.GetCount() - 1;
}
}
return frame;
}
DWORD CAviFile::strm_t::GetKeyFrame(REFERENCE_TIME rt)
{
DWORD i = GetFrame(rt);
for (; i > 0; i--) {
if (cs[i].fKeyFrame) {
break;
}
}
return i;
}
DWORD CAviFile::strm_t::GetChunkSize(DWORD size)
{
if (strh.fccType == FCC('auds')) {
WORD nBlockAlign = ((WAVEFORMATEX*)strf.GetData())->nBlockAlign;
size = nBlockAlign ? (size + (nBlockAlign - 1)) / nBlockAlign * nBlockAlign : 0; // round up for nando's vbr hack
}
return size;
}
bool CAviFile::strm_t::IsRawSubtitleStream()
{
return strn.Find("Subtitle") == 0 || strh.fccType == FCC('txts') && cs.GetCount() == 1;
}