/*
* (C) 2006-2014 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 "PGSSub.h"
#include "../DSUtil/GolombBuffer.h"
#include
#include
#include "SubtitleHelpers.h"
#if (0) // Set to 1 to activate PGS subtitles traces
#define TRACE_PGSSUB TRACE
#else
#define TRACE_PGSSUB __noop
#endif
CPGSSub::CPGSSub(CCritSec* pLock, const CString& name, LCID lcid)
: CRLECodedSubtitle(pLock, name, lcid)
, m_nCurSegment(NO_SEGMENT)
, m_pSegBuffer(nullptr)
, m_nTotalSegBuffer(0)
, m_nSegBufferPos(0)
, m_nSegSize(0)
{
if (m_name.IsEmpty() || m_name == _T("Unknown")) {
m_name = "PGS Embedded Subtitle";
}
}
CPGSSub::~CPGSSub()
{
Reset();
delete [] m_pSegBuffer;
}
// ISubPicProvider
STDMETHODIMP_(POSITION) CPGSSub::GetStartPosition(REFERENCE_TIME rt, double fps)
{
CAutoLock cAutoLock(&m_csCritSec);
POSITION pos = m_pPresentationSegments.GetHeadPosition();
while (pos) {
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(pos);
if (pPresentationSegment->rtStop <= rt) {
m_pPresentationSegments.GetNext(pos);
} else {
break;
}
}
return pos;
}
STDMETHODIMP_(POSITION) CPGSSub::GetNext(POSITION pos)
{
CAutoLock cAutoLock(&m_csCritSec);
m_pPresentationSegments.GetNext(pos);
return pos;
}
STDMETHODIMP_(REFERENCE_TIME) CPGSSub::GetStart(POSITION pos, double fps)
{
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(pos);
return pPresentationSegment ? pPresentationSegment->rtStart : INVALID_TIME;
}
STDMETHODIMP_(REFERENCE_TIME) CPGSSub::GetStop(POSITION pos, double fps)
{
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(pos);
return pPresentationSegment ? pPresentationSegment->rtStop : INVALID_TIME;
}
STDMETHODIMP_(bool) CPGSSub::IsAnimated(POSITION pos)
{
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(pos);
// If the end time isn't known yet, we consider the subtitle as animated to be sure it will properly updated
return (pPresentationSegment && pPresentationSegment->rtStop == UNKNOWN_TIME);
}
STDMETHODIMP CPGSSub::Render(SubPicDesc& spd, REFERENCE_TIME rt, double fps, RECT& bbox)
{
return Render(spd, rt, bbox, true);
}
STDMETHODIMP CPGSSub::GetTextureSize(POSITION pos, SIZE& MaxTextureSize, SIZE& VideoSize, POINT& VideoTopLeft)
{
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(pos);
if (pPresentationSegment) {
MaxTextureSize.cx = VideoSize.cx = pPresentationSegment->video_descriptor.nVideoWidth;
MaxTextureSize.cy = VideoSize.cy = pPresentationSegment->video_descriptor.nVideoHeight;
// The subs will be directly rendered into the proper position!
VideoTopLeft.x = 0; //pObject->m_horizontal_position;
VideoTopLeft.y = 0; //pObject->m_vertical_position;
return S_OK;
}
ASSERT(FALSE);
return E_INVALIDARG;
}
HRESULT CPGSSub::ParseSample(REFERENCE_TIME rtStart, REFERENCE_TIME rtStop, BYTE* pData, size_t nLen)
{
CheckPointer(pData, E_POINTER);
CAutoLock cAutoLock(&m_csCritSec);
CGolombBuffer sampleBuffer(pData, nLen);
while (!sampleBuffer.IsEOF()) {
if (m_nCurSegment == NO_SEGMENT) {
HDMV_SEGMENT_TYPE nSegType = (HDMV_SEGMENT_TYPE)sampleBuffer.ReadByte();
unsigned short nUnitSize = sampleBuffer.ReadShort();
nLen -= 3;
switch (nSegType) {
case PALETTE:
case OBJECT:
case PRESENTATION_SEG:
case END_OF_DISPLAY:
m_nCurSegment = nSegType;
AllocSegment(nUnitSize);
break;
case WINDOW_DEF:
case INTERACTIVE_SEG:
case HDMV_SUB1:
case HDMV_SUB2:
// Ignored stuff...
sampleBuffer.SkipBytes(nUnitSize);
break;
default:
return VFW_E_SAMPLE_REJECTED;
}
}
if (m_nCurSegment != NO_SEGMENT) {
if (m_nSegBufferPos < m_nSegSize) {
size_t nSize = std::min(m_nSegSize - m_nSegBufferPos, nLen);
sampleBuffer.ReadBuffer(m_pSegBuffer + m_nSegBufferPos, nSize);
m_nSegBufferPos += nSize;
}
if (m_nSegBufferPos >= m_nSegSize) {
CGolombBuffer SegmentBuffer(m_pSegBuffer, m_nSegSize);
switch (m_nCurSegment) {
case PALETTE:
TRACE_PGSSUB(_T("CPGSSub:PALETTE %s\n"), ReftimeToString(rtStart));
ParsePalette(&SegmentBuffer, m_nSegSize);
break;
case OBJECT:
TRACE_PGSSUB(_T("CPGSSub:OBJECT %s\n"), ReftimeToString(rtStart));
ParseObject(&SegmentBuffer, m_nSegSize);
break;
case PRESENTATION_SEG:
TRACE_PGSSUB(_T("CPGSSub:PRESENTATION_SEG %s (size=%d)\n"), ReftimeToString(rtStart), m_nSegSize);
if (rtStart == INVALID_TIME) {
break;
}
// Update the timestamp for the previous segment
UpdateTimeStamp(rtStart);
// Parse the new presentation segment
ParsePresentationSegment(rtStart, &SegmentBuffer);
break;
case WINDOW_DEF:
//TRACE_PGSSUB(_T("CPGSSub:WINDOW_DEF %s\n"), ReftimeToString(rtStart));
break;
case END_OF_DISPLAY:
TRACE_PGSSUB(_T("CPGSSub:END_OF_DISPLAY %s\n"), ReftimeToString(rtStart));
// Enqueue the current presentation segment if any
EnqueuePresentationSegment();
break;
default:
TRACE_PGSSUB(_T("CPGSSub:UNKNOWN Seg %d %s\n"), m_nCurSegment, ReftimeToString(rtStart));
}
m_nCurSegment = NO_SEGMENT;
}
}
}
return S_OK;
}
void CPGSSub::Reset()
{
CAutoLock cAutoLock(&m_csCritSec);
m_nSegBufferPos = m_nSegSize = 0;
m_nCurSegment = NO_SEGMENT;
m_pCurrentPresentationSegment.Free();
m_pPresentationSegments.RemoveAll();
for (int i = 0; i < _countof(m_compositionObjects); i++) {
m_compositionObjects[i].Reset();
}
}
void CPGSSub::AllocSegment(size_t nSize)
{
if (nSize > m_nTotalSegBuffer) {
delete[] m_pSegBuffer;
m_pSegBuffer = DEBUG_NEW BYTE[nSize];
m_nTotalSegBuffer = nSize;
}
m_nSegBufferPos = 0;
m_nSegSize = nSize;
}
HRESULT CPGSSub::Render(SubPicDesc& spd, REFERENCE_TIME rt, RECT& bbox, bool bRemoveOldSegments)
{
CAutoLock cAutoLock(&m_csCritSec);
bool bRendered = false;
if (bRemoveOldSegments) {
RemoveOldSegments(rt);
}
POSITION posPresentationSegment = FindPresentationSegment(rt);
if (posPresentationSegment) {
const auto& pPresentationSegment = m_pPresentationSegments.GetAt(posPresentationSegment);
bool BT709 = m_infoSourceTarget.sourceMatrix == BT_709 ? true : m_infoSourceTarget.sourceMatrix == NONE ? (pPresentationSegment->video_descriptor.nVideoWidth > 720) : false;
TRACE_PGSSUB(_T("CPGSSub:Render Presentation segment %d --> %s - %s\n"), pPresentationSegment->composition_descriptor.nNumber,
ReftimeToString(pPresentationSegment->rtStart),
(pPresentationSegment->rtStop == UNKNOWN_TIME) ? _T("?") : ReftimeToString(pPresentationSegment->rtStop));
bbox.left = bbox.top = LONG_MAX;
bbox.right = bbox.bottom = 0;
POSITION pos = pPresentationSegment->objects.GetHeadPosition();
while (pos) {
const auto& pObject = pPresentationSegment->objects.GetNext(pos);
if (pObject->GetRLEDataSize() && pObject->m_width > 0 && pObject->m_height > 0
&& spd.w >= (pObject->m_horizontal_position + pObject->m_width) && spd.h >= (pObject->m_vertical_position + pObject->m_height)) {
pObject->SetPalette(pPresentationSegment->CLUT.size, pPresentationSegment->CLUT.palette, BT709,
m_infoSourceTarget.sourceBlackLevel, m_infoSourceTarget.sourceWhiteLevel, m_infoSourceTarget.targetBlackLevel, m_infoSourceTarget.targetWhiteLevel);
bbox.left = std::min(pObject->m_horizontal_position, bbox.left);
bbox.top = std::min(pObject->m_vertical_position, bbox.top);
bbox.right = std::max(pObject->m_horizontal_position + pObject->m_width, bbox.right);
bbox.bottom = std::max(pObject->m_vertical_position + pObject->m_height, bbox.bottom);
TRACE_PGSSUB(_T(" --> Object %d (Pos=%dx%d, Res=%dx%d, SPDRes=%dx%d)\n"),
pObject->m_object_id_ref, pObject->m_horizontal_position, pObject->m_vertical_position, pObject->m_width, pObject->m_height, spd.w, spd.h);
pObject->RenderHdmv(spd);
bRendered = true;
} else {
TRACE_PGSSUB(_T(" --> Invalid object %d\n"), pObject->m_object_id_ref);
}
}
}
if (!bRendered) {
bbox = { 0, 0, 0, 0 };
}
return S_OK;
}
int CPGSSub::ParsePresentationSegment(REFERENCE_TIME rt, CGolombBuffer* pGBuffer)
{
m_pCurrentPresentationSegment = CAutoPtr(DEBUG_NEW HDMV_PRESENTATION_SEGMENT());
m_pCurrentPresentationSegment->rtStart = rt;
m_pCurrentPresentationSegment->rtStop = UNKNOWN_TIME; // Unknown for now
ParseVideoDescriptor(pGBuffer, &m_pCurrentPresentationSegment->video_descriptor);
ParseCompositionDescriptor(pGBuffer, &m_pCurrentPresentationSegment->composition_descriptor);
m_pCurrentPresentationSegment->palette_update_flag = !!(pGBuffer->ReadByte() & 0x80);
m_pCurrentPresentationSegment->CLUT.id = pGBuffer->ReadByte();
m_pCurrentPresentationSegment->objectCount = pGBuffer->ReadByte();
TRACE_PGSSUB(_T("CPGSSub::ParsePresentationSegment Size = %d, state = %#x, nObjectNumber = %d\n"), pGBuffer->GetSize(),
m_pCurrentPresentationSegment->composition_descriptor.bState, m_pCurrentPresentationSegment->objectCount);
for (int i = 0; i < m_pCurrentPresentationSegment->objectCount; i++) {
CAutoPtr pCompositionObject(DEBUG_NEW CompositionObject());
ParseCompositionObject(pGBuffer, pCompositionObject);
m_pCurrentPresentationSegment->objects.AddTail(pCompositionObject);
}
return m_pCurrentPresentationSegment->objectCount;
}
void CPGSSub::EnqueuePresentationSegment()
{
if (m_pCurrentPresentationSegment) {
if (m_pCurrentPresentationSegment->objectCount > 0) {
m_pCurrentPresentationSegment->CLUT = m_CLUTs[m_pCurrentPresentationSegment->CLUT.id];
// Get the objects' data
POSITION pos = m_pCurrentPresentationSegment->objects.GetHeadPosition();
while (pos) {
const auto& pObject = m_pCurrentPresentationSegment->objects.GetNext(pos);
const CompositionObject& pObjectData = m_compositionObjects[pObject->m_object_id_ref];
pObject->m_width = pObjectData.m_width;
pObject->m_height = pObjectData.m_height;
if (pObjectData.GetRLEData()) {
pObject->SetRLEData(pObjectData.GetRLEData(), pObjectData.GetRLEPos(), pObjectData.GetRLEDataSize());
}
}
TRACE_PGSSUB(_T("CPGSSub: Enqueue Presentation Segment %d - %s => ?\n"), m_pCurrentPresentationSegment->composition_descriptor.nNumber,
ReftimeToString(m_pCurrentPresentationSegment->rtStart));
m_pPresentationSegments.AddTail(m_pCurrentPresentationSegment);
} else {
TRACE_PGSSUB(_T("CPGSSub: Delete empty Presentation Segment %d\n"), m_pCurrentPresentationSegment->composition_descriptor.nNumber);
m_pCurrentPresentationSegment.Free();
}
}
}
void CPGSSub::UpdateTimeStamp(REFERENCE_TIME rtStop)
{
if (!m_pPresentationSegments.IsEmpty()) {
const auto& pPresentationSegment = m_pPresentationSegments.GetTail();
// Since we drop empty segments we might be trying to update a segment that isn't
// in the queue so we update the timestamp only if it was previously unknown.
if (pPresentationSegment->rtStop == UNKNOWN_TIME) {
pPresentationSegment->rtStop = rtStop;
TRACE_PGSSUB(_T("CPGSSub: Update Presentation Segment TimeStamp %d - %s => %s\n"), pPresentationSegment->composition_descriptor.nNumber,
ReftimeToString(pPresentationSegment->rtStart),
ReftimeToString(pPresentationSegment->rtStop));
}
}
}
void CPGSSub::ParsePalette(CGolombBuffer* pGBuffer, size_t nSize) // #497
{
BYTE palette_id = pGBuffer->ReadByte();
HDMV_CLUT& CLUT = m_CLUTs[palette_id];
CLUT.id = palette_id;
CLUT.version_number = pGBuffer->ReadByte();
ASSERT((nSize - 2) % sizeof(HDMV_PALETTE) == 0);
CLUT.size = BYTE((nSize - 2) / sizeof(HDMV_PALETTE));
for (int i = 0; i < CLUT.size; i++) {
CLUT.palette[i].entry_id = pGBuffer->ReadByte();
CLUT.palette[i].Y = pGBuffer->ReadByte();
CLUT.palette[i].Cr = pGBuffer->ReadByte();
CLUT.palette[i].Cb = pGBuffer->ReadByte();
CLUT.palette[i].T = pGBuffer->ReadByte();
}
}
void CPGSSub::ParseObject(CGolombBuffer* pGBuffer, size_t nUnitSize) // #498
{
short object_id = pGBuffer->ReadShort();
ASSERT(object_id < _countof(m_compositionObjects));
CompositionObject& pObject = m_compositionObjects[object_id];
pObject.m_version_number = pGBuffer->ReadByte();
BYTE m_sequence_desc = pGBuffer->ReadByte();
if (m_sequence_desc & 0x80) {
int object_data_length = (int)pGBuffer->BitRead(24);
pObject.m_width = pGBuffer->ReadShort();
pObject.m_height = pGBuffer->ReadShort();
pObject.SetRLEData(pGBuffer->GetBufferPos(), nUnitSize - 11, object_data_length - 4);
TRACE_PGSSUB(_T("CPGSSub:ParseObject %d (size=%ld, %dx%d)\n"), object_id, object_data_length, pObject.m_width, pObject.m_height);
} else {
pObject.AppendRLEData(pGBuffer->GetBufferPos(), nUnitSize - 4);
}
}
void CPGSSub::ParseCompositionObject(CGolombBuffer* pGBuffer, const CAutoPtr& pCompositionObject)
{
BYTE bTemp;
pCompositionObject->m_object_id_ref = pGBuffer->ReadShort();
pCompositionObject->m_window_id_ref = pGBuffer->ReadByte();
bTemp = pGBuffer->ReadByte();
pCompositionObject->m_object_cropped_flag = !!(bTemp & 0x80);
pCompositionObject->m_forced_on_flag = !!(bTemp & 0x40);
pCompositionObject->m_horizontal_position = pGBuffer->ReadShort();
pCompositionObject->m_vertical_position = pGBuffer->ReadShort();
if (pCompositionObject->m_object_cropped_flag) {
pCompositionObject->m_cropping_horizontal_position = pGBuffer->ReadShort();
pCompositionObject->m_cropping_vertical_position = pGBuffer->ReadShort();
pCompositionObject->m_cropping_width = pGBuffer->ReadShort();
pCompositionObject->m_cropping_height = pGBuffer->ReadShort();
}
}
void CPGSSub::ParseVideoDescriptor(CGolombBuffer* pGBuffer, VIDEO_DESCRIPTOR* pVideoDescriptor)
{
pVideoDescriptor->nVideoWidth = pGBuffer->ReadShort();
pVideoDescriptor->nVideoHeight = pGBuffer->ReadShort();
pVideoDescriptor->bFrameRate = pGBuffer->ReadByte();
}
void CPGSSub::ParseCompositionDescriptor(CGolombBuffer* pGBuffer, COMPOSITION_DESCRIPTOR* pCompositionDescriptor)
{
pCompositionDescriptor->nNumber = pGBuffer->ReadShort();
pCompositionDescriptor->bState = pGBuffer->ReadByte() >> 6;
}
POSITION CPGSSub::FindPresentationSegment(REFERENCE_TIME rt) const
{
POSITION pos = m_pPresentationSegments.GetHeadPosition();
while (pos) {
POSITION currentPos = pos;
const auto& pPresentationSegment = m_pPresentationSegments.GetNext(pos);
if (pPresentationSegment->rtStart <= rt && pPresentationSegment->rtStop > rt) {
return currentPos;
}
}
return nullptr;
}
void CPGSSub::RemoveOldSegments(REFERENCE_TIME rt)
{
// Cleanup the old presentation segments. We keep a 2 min buffer to play nice with the queue.
while (!m_pPresentationSegments.IsEmpty()
&& m_pPresentationSegments.GetHead()->rtStop != UNKNOWN_TIME
&& m_pPresentationSegments.GetHead()->rtStop + 120 * 10000000i64 < rt) {
auto pPresentationSegment = m_pPresentationSegments.RemoveHead();
TRACE_PGSSUB(_T("CPGSSub::RemoveOldSegments Remove presentation segment %d %s => %s (rt=%s)\n"),
pPresentationSegment->composition_descriptor.nNumber,
ReftimeToString(pPresentationSegment->rtStart),
ReftimeToString(pPresentationSegment->rtStop),
ReftimeToString(rt));
}
}
CPGSSubFile::CPGSSubFile(CCritSec* pLock)
: CPGSSub(pLock, _T("PGS External Subtitle"), 0)
, m_bStopParsing(false)
{
}
CPGSSubFile::~CPGSSubFile()
{
m_bStopParsing = true;
if (m_parsingThread.joinable()) {
m_parsingThread.join();
}
}
STDMETHODIMP CPGSSubFile::Render(SubPicDesc& spd, REFERENCE_TIME rt, double fps, RECT& bbox)
{
return __super::Render(spd, rt, bbox, false);
}
bool CPGSSubFile::Open(CString fn, CString name /*= _T("")*/, CString videoName /*= _T("")*/)
{
bool bOpened = false;
if (name.IsEmpty()) {
m_name = Subtitle::GuessSubtitleName(fn, videoName);
} else {
m_name = name;
}
CFile f;
if (f.Open(fn, CFile::modeRead | CFile::shareDenyWrite)) {
WORD wSyncCode = 0;
f.Read(&wSyncCode, sizeof(wSyncCode));
wSyncCode = _byteswap_ushort(wSyncCode);
if (wSyncCode == PGS_SYNC_CODE) {
m_parsingThread = std::thread([this, fn] { ParseFile(fn); });
bOpened = true;
}
}
return bOpened;
}
void CPGSSubFile::ParseFile(CString fn)
{
CFile f;
if (!f.Open(fn, CFile::modeRead | CFile::shareDenyWrite)) {
return;
}
// Header: Sync code | start time | stop time | segment type | segment size
std::array < BYTE, 2 + 2 * 4 + 1 + 2 > header;
const int nExtraSize = 1 + 2; // segment type + segment size
std::vector segBuff;
while (!m_bStopParsing && f.Read(header.data(), (UINT)header.size()) == header.size()) {
// Parse the header
CGolombBuffer headerBuffer(header.data(), (int)header.size());
if (WORD(headerBuffer.ReadShort()) != PGS_SYNC_CODE) {
break;
}
REFERENCE_TIME rtStart = REFERENCE_TIME(headerBuffer.ReadDword()) * 1000 / 9;
REFERENCE_TIME rtStop = REFERENCE_TIME(headerBuffer.ReadDword()) * 1000 / 9;
headerBuffer.ReadByte(); // segment type
WORD wLenSegment = (WORD)headerBuffer.ReadShort();
// Let some round to add the segment type and size
int nLenData = nExtraSize + wLenSegment;
segBuff.resize(nLenData);
memcpy(segBuff.data(), &header[header.size() - nExtraSize], nExtraSize);
// Read the segment
if (wLenSegment && f.Read(&segBuff[nExtraSize], wLenSegment) != wLenSegment) {
break;
}
// Parse the data (even if the segment size is 0 because the header itself is important)
TRACE_PGSSUB(_T("--------- CPGSSubFile::ParseFile rtStart=%s, rtStop=%s, len=%d ---------\n"),
ReftimeToString(rtStart), ReftimeToString(rtStop), nLenData);
ParseSample(rtStart, rtStop, segBuff.data(), nLenData);
}
}