mmplugins/mmfwplugins/src/Plugin/Controller/Video/AviPlayController/srtdecoder/srtdecoder.cpp
// Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
// All rights reserved.
// This component and the accompanying materials are made available
// under the terms of "Eclipse Public License v1.0"
// which accompanies this distribution, and is available
// at the URL "http://www.eclipse.org/legal/epl-v10.html".
//
// Initial Contributors:
// Nokia Corporation - initial contribution.
//
// Contributors:
//
// Description:
//
#include <bitdev.h>
#include <fbs.h>
#include <gdi.h>
#include <utf.h>
#include "subtitlesource.h"
#include "srtdecoder.h"
#include "srtframe.h"
// Define the separators for each subtitle frame in SRT file
_LIT8(KSrtFrameSeparator1, "\r\n\r\n" );
_LIT8(KSrtFrameSeparator2, "\n\n" );
// The length of the separators
const TInt KSrtFrameSeparator1Len = 4;
const TInt KSrtFrameSeparator2Len = 2;
// The margin size
const TInt KSrtBmpMarginWidth = 6;
const TInt KSrtBmpMarginHeight = 6;
// The width of subtitle region is the same as the width of the video region, while the height is 33
// percent of the height of the video region.
const TInt KSrtSubtitleRegionYFactor = 33;
// Define panic category
_LIT(KSrtSubtitleDecoderPanicCategory, "SrtSubDec");
// TSrtSubtitleDecoderPanics is an enumeration with the following entries:
// EBadCall indicates a bad call
enum TSrtSubtitleDecoderPanics
{
ESrtBadCall = 1,
ESrtUnexpected,
ESrtNotStarted,
ESrtAlreadyStarted
};
/** Constructor with a reference to the subtitle source
@param aSubtitleSource a reference to the subtitle source
*/
CSrtSubtitleDecoder::CSrtSubtitleDecoder(MSubtitleSource& aSubtitleSource) :
iSource(aSubtitleSource)
{
}
EXPORT_C CSrtSubtitleDecoder::~CSrtSubtitleDecoder()
{
delete iFrameParser;
iDecodeBuffer.Close();
}
/** Allocates and constructs a srt subtitle decoder
@param aSubtitleSource a reference to the subtitle source
@return A pointer to the new object.
@leave If an error occurs, the method will leave with a system-wide error code.
*/
EXPORT_C CSrtSubtitleDecoder* CSrtSubtitleDecoder::NewL(MSubtitleSource& aSubtitleSource)
{
CSrtSubtitleDecoder* self = new(ELeave) CSrtSubtitleDecoder(aSubtitleSource);
CleanupStack::PushL(self);
self->ConstructL();
CleanupStack::Pop(self);
return self;
}
/** Initialises the decoder
@leave If an error occurs, the method will leave with a system-wide error code.
*/
void CSrtSubtitleDecoder::ConstructL()
{
iFrameParser = CSrtFrame::NewL();
iDecodeBuffer.CreateL(KSubtitleDecoderBufferLength);
}
EXPORT_C TInt CSrtSubtitleDecoder::CalculateSubtitleRegion(const TRect& aScreenSize, TRect& aSubtitleRegion)
{
// TRect::IsEmpty is not used because the following test tests the minus values.
if ((aScreenSize.iBr.iX < 0) || (aScreenSize.iBr.iY < 0) ||
(aScreenSize.iTl.iX < 0) || (aScreenSize.iTl.iY < 0))
{
return KErrArgument;
}
if ((aScreenSize.iBr.iX <= aScreenSize.iTl.iX) || (aScreenSize.iBr.iY <= aScreenSize.iTl.iY))
{
return KErrArgument;
}
TPoint topleft;
topleft.iX = aScreenSize.iTl.iX;
topleft.iY = aScreenSize.iTl.iY + aScreenSize.Height() * (100 - KSrtSubtitleRegionYFactor) / 100;
aSubtitleRegion.SetRect(topleft, aScreenSize.iBr);
return KErrNone;
}
/** Calculate the number of the characters in the first line. The full words are supposed to be displayed
unless the length of a word is bigger than one line space.
@param aFont The font in current use.
@param aText The descriptor
@param aWidthInPixels The available width for character display.
@param aNumOfCharsToSkip The number of characters to skip, it can be 0, 1 (\n) or 2 (\r\n)
@return The number of characters which will be able to be displayed without exceeding the specified width.
The count starts from the beginning of the descriptor.
*/
TInt CSrtSubtitleDecoder::CalculateCharsInNextLine(const CFont &aFont, const TDesC &aText, TInt aWidthInPixels, TInt& aNumOfCharsToSkip)
{
const TChar KMMFSubtitleCharSpace(' ');
const TChar KMMFSubtitleCharCR('\r');
const TChar KMMFSubtitleCharLF('\n');
TInt charsInLine = aFont.TextCount(aText, aWidthInPixels);
aNumOfCharsToSkip = 0;
if (charsInLine < aText.Length())
{
// For finding the last space of a descriptor which is not very big, using a simple loop
// shall be more efficient than using TLex
for (TInt i = charsInLine; i > 0; i--)
{
if (KMMFSubtitleCharSpace == aText[i - 1])
{
charsInLine = i;
break;
}
}
}
// Check \r\n or \n, the following loop is more efficient than TDesC::Find() in most cases
// because both \r\n and \n are handled in one loop.
for (TInt j = 0; j < charsInLine; j++)
{
if (KMMFSubtitleCharLF == aText[j])
{
if (j > 0)
{
// ignore \r if there is a \n
if (KMMFSubtitleCharCR == aText[j - 1])
{
charsInLine = j;
aNumOfCharsToSkip = 2;
}
else
{
charsInLine = j + 1;
aNumOfCharsToSkip = 1;
}
}
else
{
charsInLine = j + 1;
aNumOfCharsToSkip = 1;
}
break;
}
}
return charsInLine;
}
/** Based on a descriptor containing a SRT subtitle frame, the function generates a single frame of
subtitle animation for the current time and outputs the region of the subtitle frame that has been
updated.
@param aContent The subtitle frame as a descriptor
@param aSubtitleFrame Reference to the current subtitle frame
@param aDirtyRegion The decoder outputs the region of the subtitle frame that has been updated.
i.e. the region that now contains new subtitle content.
@leave If an error occurs, the method will leave with a system-wide error code.
*/
void CSrtSubtitleDecoder::DecodeSubtitleContentL(const TDesC8& aContent, CFbsBitmap& aSubtitleFrame, TRect& aDirtyRegion)
{
TSize bmpSize = aSubtitleFrame.SizeInPixels();
TSize modifiedBmpSize;
// the following value of "2" means two side of margin.
modifiedBmpSize.iHeight = bmpSize.iHeight - KSrtBmpMarginHeight * 2;
modifiedBmpSize.iWidth = bmpSize.iWidth - KSrtBmpMarginWidth * 2;
// test if the size of the bitmap is valid after taking margin into account
if ((modifiedBmpSize.iHeight > 0) && (modifiedBmpSize.iWidth > 0))
{
// prepare and populate the bitmap
CFbsBitmapDevice* bmpDevice = CFbsBitmapDevice::NewL(&aSubtitleFrame);
CleanupStack::PushL(bmpDevice);
CFbsBitGc* bitmapContext = CFbsBitGc::NewL(); //Create a GC to draw to the bitmap
bitmapContext->Activate(bmpDevice);
CleanupStack::PushL(bitmapContext);
bitmapContext->SetDrawMode(CGraphicsContext::EDrawModeWriteAlpha);
// Use a white color (R=255, G=255, B=255) with alpha value of 0.
TRgb transparentWhite(255, 255, 255, 0);
bitmapContext->SetBrushColor(transparentWhite);
bitmapContext->Clear();
bitmapContext->SetDrawMode(CGraphicsContext::EDrawModePEN);
bitmapContext->SetPenStyle(CGraphicsContext::ESolidPen);
bitmapContext->SetPenColor(KRgbBlack);
HBufC* content = CnvUtfConverter::ConvertToUnicodeFromUtf8L(aContent);
CleanupStack::PushL(content);
// try to set up font
CFont *font = NULL;
TFontSpec fontSpec(KSrtTargetTypefaceName, KSrtTargetTypefaceHeightInTwips);
User::LeaveIfError(bmpDevice->GetNearestFontToDesignHeightInTwips(font, fontSpec));
bitmapContext->UseFont(font);
TInt maxCharWidthInPixels = font->MaxCharWidthInPixels();
TInt heightInPixels = font->HeightInPixels();
TInt maxLines = modifiedBmpSize.iHeight / heightInPixels;
TInt maxCharsPerLine = modifiedBmpSize.iWidth / maxCharWidthInPixels;
TInt charsInLine = 0;
TInt currentRow = 0;
TPoint drawPosition;
drawPosition.iX = KSrtBmpMarginWidth;
drawPosition.iY = KSrtBmpMarginHeight + heightInPixels;
TInt unvisitedChars = content->Length();
TInt lineStartPos = 0;
TInt numOfCharsToSkip = 0;
if ((maxLines > 0) && (maxCharsPerLine > 0))
{
do
{
TPtrC ptrUnvisitedDesc = content->Right(unvisitedChars);
charsInLine = CalculateCharsInNextLine(*font, ptrUnvisitedDesc, modifiedBmpSize.iWidth, numOfCharsToSkip);
if (charsInLine > 0)
{
if (charsInLine > numOfCharsToSkip)
{
TPtrC ptrLine = content->Mid(lineStartPos, charsInLine - numOfCharsToSkip);
// try to draw text line by line
// Note that aPosition in DrawText indicates the bottom left of the first character.
bitmapContext->DrawText(ptrLine, drawPosition);
drawPosition.iY += heightInPixels;
currentRow++;
}
lineStartPos += charsInLine;
unvisitedChars -= charsInLine;
}
}
while(charsInLine > 0 && currentRow < maxLines);
// update the dirty region
aDirtyRegion.iTl.SetXY(KSrtBmpMarginWidth, KSrtBmpMarginHeight);
TInt actualMaxCharsPerLine = 0;
actualMaxCharsPerLine = Min(content->Length(), maxCharsPerLine);
aDirtyRegion.iBr.SetXY(
KSrtBmpMarginWidth + actualMaxCharsPerLine * maxCharWidthInPixels,
KSrtBmpMarginHeight + currentRow * heightInPixels + font->FontMaxDescent());
}
else
{
// bitmap too small for the font
User::Leave(KErrArgument);
}
bmpDevice->ReleaseFont(font);
bitmapContext->DiscardFont();
CleanupStack::PopAndDestroy(3, bmpDevice); // content, bitmapContext, bmpDevice
}
else
{
// bitmap too small or the length of content is 0
User::Leave(KErrArgument);
}
}
/** Try to get the next valid frame
@param aSearchByVideoPosition Indicate if search for the next valid frame matching aVideoPosition
@param aVideoPosition The video position in microsecond
@leave If an error occurs, the method will leave with a system-wide error code.
*/
void CSrtSubtitleDecoder::GetNextValidFrameL(TBool aSearchByVideoPosition, TInt64 aVideoPosition)
{
TInt decoderBufferMaxLen = iDecodeBuffer.MaxLength();
TInt errorFrame = KErrNone;
TInt error = KErrNone;
TInt sepLen = 0;
TInt i = 0;
TChar currentChar = 0;
TBool frameFound = EFalse;
if (iLastFrame)
{
User::Leave(KErrEof);
}
while (!frameFound)
{
// if a frame is not available in iDecodeBuffer, fill iBuffer until CRLFCRLF is available in iBuffer
TPtrC8 restOfData = iDecodeBuffer.Right(iDecodeBuffer.Length() - iDecodeBufferPosition);
TInt sepPosition = 0;
// try to extract a frame from iDecodeBuffer
errorFrame = GetFrameSeparatorInfo(restOfData, sepLen, sepPosition);
if (KErrNotFound != errorFrame)
{
User::LeaveIfError(errorFrame);
}
// if no frame can be extracted, read source until a frame is available or get an EOF error.
while (KErrNotFound == errorFrame)
{
// move the unvisited data in decode buffer to the head before read from the source
if (iDecodeBufferPosition != 0)
{
// move the unvisited data in decode buffer to the head
if (iDecodeBufferPosition <= iDecodeBuffer.Length() - 1)
{
TPtrC8 newSubtitleFrame = iDecodeBuffer.Right(iDecodeBuffer.Length() - (iDecodeBufferPosition));
// use iBuffer to hold the preserved data when rotating data in iDecodeBuffer
if (iBuffer.MaxLength() < newSubtitleFrame.Length())
{
// Some data in iBuffer make iDecodeBuffer hold at least one frame, and so
// iBuffer should always be larger than the preserved data in iDecodeBuffer
User::Panic(KSrtSubtitleDecoderPanicCategory, ESrtUnexpected);
}
iBuffer.Copy(newSubtitleFrame);
iDecodeBuffer.Copy(iBuffer);
}
iDecodeBufferPosition = 0;
}
error = iSource.GetBuffer(iBuffer);
if ((KErrNone != error) && (KErrEof != error))
{
User::Leave(error);
}
if (iBuffer.Length() > 0)
{
TInt bufLen = iDecodeBuffer.Length();
TInt newBufLen = iBuffer.Length();
if (decoderBufferMaxLen < newBufLen + bufLen)
{
iDecodeBuffer.ReAllocL(newBufLen + bufLen);
decoderBufferMaxLen = iDecodeBuffer.MaxLength();
}
iDecodeBuffer.Append(iBuffer);
}
if (KErrEof == error)
{
// reach the end of the subtitle source but cannot find new line separators. Frame
// ends at EOF marker then. No more frames can be read after this one.
sepLen = 0;
sepPosition = iDecodeBuffer.Length();
errorFrame = KErrNone;
if (iLastFrame)
{
User::Leave(KErrEof);
}
iLastFrame = ETrue;
}
else
{
errorFrame = GetFrameSeparatorInfo(iDecodeBuffer, sepLen, sepPosition);
}
if (KErrNotFound != errorFrame)
{
User::LeaveIfError(errorFrame);
}
}
// extract a possible frame
TPtrC8 subtitleFrameDescriptor = iDecodeBuffer.Mid(iDecodeBufferPosition, sepPosition);
// Find the start of the next frame, and update the current position of the decode buffer by
// the following method: from the start of iDecodeBuffer, find the position of the first digit
// character after the first frame separator.
// Not the current position of the decode buffer is updated before parsing the frame, so the
// potential invalid frame will be skipped.
iDecodeBufferPosition += sepPosition + sepLen;
TInt firstDigitPosFromCurrentPos = KErrNotFound;
for (i = iDecodeBufferPosition; i < iDecodeBuffer.Length(); i++)
{
currentChar = iDecodeBuffer[i];
if (currentChar.IsDigit())
{
firstDigitPosFromCurrentPos = i;
break;
}
}
if (KErrNotFound == firstDigitPosFromCurrentPos)
{
iDecodeBufferPosition = iDecodeBuffer.Length() - 1;
}
else
{
iDecodeBufferPosition = firstDigitPosFromCurrentPos;
}
// parse the frame
TRAPD(parseError, iFrameParser->ParseL(subtitleFrameDescriptor, aSearchByVideoPosition, aVideoPosition));
if (KErrNone == parseError)
{
frameFound = ETrue;
if (!aSearchByVideoPosition)
{
// skip the past frames, such as
// 2\r\n00:00:02,000 --> 00:00:04,000\r\nhome\r\n\r\n
// 1\r\n00:00:00,000 --> 00:00:02,000\r\nwelcome\r\n\r\n
// 3\r\n00:00:04,000 --> 00:00:06,000\r\nagain
if (iFrameParser->EndTime() < iEndTimeOfLastValidFrame)
{
frameFound = EFalse;
}
else
{
iEndTimeOfLastValidFrame = iFrameParser->EndTime();
}
}
else
{
iEndTimeOfLastValidFrame = iFrameParser->EndTime();
}
}
else
{
if (KErrArgument != parseError)
{
User::Leave(parseError);
}
}
}
}
/** The decoder generates a single frame of subtitle animation for the current time.
The decoder maintains track of the video position. The video position can be updated
via a call to MSubtitleDecoder::SetVideoPosition().
@param aSubtitleFrame Reference to the current subtitle frame
@param aDirtyRegion The decoder outputs the region of the subtitle frame that has been updated.
i.e. the region that now contains new subtitle content.
@param aDisplayTime The time in microseconds from the current stream position that the subtitle is due.
DevSubtitle will draw aSubtitleFrame after aDisplayTime has elapsed.
@param aDisplayDuration The time in microseconds that this frame should be displayed for. The CRP will clear
this frame after aDisplayFor microseconds
@leave If an error occurs, the method will leave with a system-wide error code.
*/
EXPORT_C void CSrtSubtitleDecoder::GetNextFrameL(CFbsBitmap& aSubtitleFrame, TRect& aDirtyRegion, TTimeIntervalMicroSeconds& aDisplayTime, TTimeIntervalMicroSeconds& aDisplayDuration)
{
if (!iStarted)
{
User::Panic(KSrtSubtitleDecoderPanicCategory, ESrtNotStarted);
}
TTime currentTime;
currentTime.UniversalTime();
TTimeIntervalMicroSeconds elapsedTime = currentTime.MicroSecondsFrom(iStartSystemTime);
TInt64 currentVideoPositionValue = elapsedTime.Int64() + iStartVideoPosition;
if (iFirstSearch)
{
// skip the past frame
GetNextValidFrameL(ETrue, currentVideoPositionValue);
iFirstSearch = EFalse;
}
else
{
// get the next frame
GetNextValidFrameL(EFalse, 0);
}
TInt64 elapsedDuration = 0;
// if start time <= current video time, this frame needs to be started immediately.
if (iFrameParser->StartTime() <= currentVideoPositionValue)
{
aDisplayTime = 0;
elapsedDuration = currentVideoPositionValue - iFrameParser->StartTime();
}
else
{
aDisplayTime = iFrameParser->StartTime() - currentVideoPositionValue;
}
// if end time < current video time, this duration becomes 0.
if (iFrameParser->EndTime() <= currentVideoPositionValue)
{
aDisplayDuration = 0;
}
else
{
aDisplayDuration = iFrameParser->Duration() - elapsedDuration;
}
// decode the content into bitmap and get dirty region
TPtrC8 ptrContent = iFrameParser->Content();
DecodeSubtitleContentL(ptrContent, aSubtitleFrame, aDirtyRegion);
}
EXPORT_C TLanguage CSrtSubtitleDecoder::SubtitleLanguageL()
{
return iSource.SubtitleLanguageL();
}
EXPORT_C void CSrtSubtitleDecoder::GetSupportedSubtitleLanguagesL(RArray<TLanguage>& aLanguages)
{
iSource.GetSupportedSubtitleLanguagesL(aLanguages);
}
EXPORT_C void CSrtSubtitleDecoder::SetSubtitleLanguageL(TLanguage aLanguage)
{
iSource.SetSubtitleLanguageL(aLanguage);
}
EXPORT_C void CSrtSubtitleDecoder::SetVideoPosition(const TTimeIntervalMicroSeconds& aPosition)
{
if (aPosition < 0)
{
User::Panic(KSrtSubtitleDecoderPanicCategory, ESrtBadCall);
}
iStartVideoPosition = aPosition.Int64();
iLastFrame = EFalse;
}
EXPORT_C void CSrtSubtitleDecoder::Start()
{
if (iStarted)
{
User::Panic(KSrtSubtitleDecoderPanicCategory, ESrtAlreadyStarted);
}
iSource.SetPosition(0);
iStartSystemTime.UniversalTime();
iStarted = ETrue;
iDecodeBufferPosition = 0;
iFirstSearch = ETrue;
iEndTimeOfLastValidFrame = 0;
iLastFrame = EFalse;
}
EXPORT_C void CSrtSubtitleDecoder::Stop()
{
iSource.SetPosition(0);
iDecodeBuffer.SetLength(0);
iBuffer.SetLength(0);
iStartVideoPosition = 0;
iStartSystemTime = 0;
iStarted = EFalse;
iDecodeBufferPosition = 0;
iLastFrame = EFalse;
}
/** Searches for the first occurrence of the SRT frame separator (\r\n\r\n or \n\n) within this
descriptor and return the length and the position of the separator.
@param aDes The buffer being searched
@param aSeparatorLength Output the length of the SRT frame separator
@param aPosition The offset of the SRT frame separator from the beginning of this descriptor's data.
@return A system-wide error code, KErrNotFound if the SRT frame separator cannot be found.
*/
TInt CSrtSubtitleDecoder::GetFrameSeparatorInfo(const TDesC8& aDes, TInt& aSeparatorLength, TInt& aPosition)
{
TInt errCode = KErrNone;
aPosition = aDes.Find(KSrtFrameSeparator1);
if (KErrNotFound == aPosition)
{
aPosition = aDes.Find(KSrtFrameSeparator2);
if (KErrNotFound == aPosition)
{
errCode = KErrNotFound;
}
else
{
aSeparatorLength = KSrtFrameSeparator2Len;
}
}
else
{
aSeparatorLength = KSrtFrameSeparator1Len;
}
return errCode;
}