Skip to content

Instantly share code, notes, and snippets.

@ozmartian
Last active January 22, 2017 13:35
Show Gist options
  • Save ozmartian/2c9a3bf50f3a948d6230726847fe7e7e to your computer and use it in GitHub Desktop.
Save ozmartian/2c9a3bf50f3a948d6230726847fe7e7e to your computer and use it in GitHub Desktop.
taken from the official Kodi repo 01/2017
/*
* Copyright (C) 2005-2013 Team XBMC
* http://xbmc.org
*
* This Program 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 2, or (at your option)
* any later version.
*
* This Program 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 XBMC; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*
*/
#include "Edl.h"
#include "utils/StringUtils.h"
#include "utils/URIUtils.h"
#include "filesystem/File.h"
#include "settings/AdvancedSettings.h"
#include "utils/log.h"
#include "utils/XBMCTinyXML.h"
#include "PlatformDefs.h"
#include "pvr/recordings/PVRRecordings.h"
#include "pvr/PVRManager.h"
#define COMSKIP_HEADER "FILE PROCESSING COMPLETE"
#define VIDEOREDO_HEADER "<Version>2"
#define VIDEOREDO_TAG_CUT "<Cut>"
#define VIDEOREDO_TAG_SCENE "<SceneMarker "
using namespace XFILE;
CEdl::CEdl()
{
Clear();
}
void CEdl::Clear()
{
m_vecCuts.clear();
m_vecSceneMarkers.clear();
m_iTotalCutTime = 0;
}
bool CEdl::ReadEditDecisionLists(const std::string& strMovie, const float fFrameRate, const int iHeight)
{
/*
* The frame rate hints returned from ffmpeg for the video stream do not appear to take into
* account whether the content is interlaced. This affects the calculation to time offsets based
* on frames per second as most commercial detection programs use full frames, which need two
* interlaced fields to calculate a single frame so the actual frame rate is half.
*
* Adjust the frame rate using the detected frame rate or height to determine typical interlaced
* content (obtained from http://en.wikipedia.org/wiki/Frame_rate)
*
* Note that this is a HACK and we should be able to get the frame rate from the source sending
* back frame markers. However, this doesn't seem possible for MythTV.
*/
float fFramesPerSecond;
if (iHeight <= 480 && int(fFrameRate * 100) == 5994) // 59.940 fps = NTSC or 60i content except for 1280x720/60
{
fFramesPerSecond = fFrameRate / 2; // ~29.97f - division used to retain accuracy of original.
CLog::Log(LOGDEBUG, "%s - Assuming NTSC or 60i interlaced content. Adjusted frames per second from %.3f (~59.940 fps) to %.3f",
__FUNCTION__, fFrameRate, fFramesPerSecond);
}
else if (int(fFrameRate * 100) == 4795) // 47.952 fps = 24p -> NTSC conversion
{
fFramesPerSecond = fFrameRate / 2; // ~23.976f - division used to retain accuracy of original.
CLog::Log(LOGDEBUG, "%s - Assuming 24p -> NTSC conversion interlaced content. Adjusted frames per second from %.3f (~47.952 fps) to %.3f",
__FUNCTION__, fFrameRate, fFramesPerSecond);
}
else if (iHeight == 576 && fFrameRate > 30.0) // PAL @ 50.0fps rather than PAL @ 25.0 fps. Can't use direct fps check of 50.0 as this is valid for 720p
{
fFramesPerSecond = fFrameRate / 2; // ~25.0f - division used to retain accuracy of original.
CLog::Log(LOGDEBUG, "%s - Assuming PAL interlaced content. Adjusted frames per second from %.3f (~50.00 fps) to %.3f",
__FUNCTION__, fFrameRate, fFramesPerSecond);
}
else if (iHeight == 1080 && fFrameRate > 30.0) // Don't know of any 1080p content being broadcast at higher than 30.0 fps so assume 1080i
{
fFramesPerSecond = fFrameRate / 2;
CLog::Log(LOGDEBUG, "%s - Assuming 1080i interlaced content. Adjusted frames per second from %.3f to %.3f",
__FUNCTION__, fFrameRate, fFramesPerSecond);
}
else // Assume everything else is not interlaced, e.g. 720p.
fFramesPerSecond = fFrameRate;
bool bFound = false;
/*
* Only check for edit decision lists if the movie is on the local hard drive, or accessed over a
* network share.
*/
if ((URIUtils::IsHD(strMovie) ||
URIUtils::IsSmb(strMovie) ||
URIUtils::IsNfs(strMovie)) &&
!URIUtils::IsPVRRecording(strMovie) &&
!URIUtils::IsInternetStream(strMovie))
{
CLog::Log(LOGDEBUG, "%s - Checking for edit decision lists (EDL) on local drive or remote share for: %s",
__FUNCTION__, strMovie.c_str());
/*
* Read any available file format until a valid EDL related file is found.
*/
if (!bFound)
bFound = ReadVideoReDo(strMovie);
if (!bFound)
bFound = ReadEdl(strMovie, fFramesPerSecond);
if (!bFound)
bFound = ReadComskip(strMovie, fFramesPerSecond);
if (!bFound)
bFound = ReadBeyondTV(strMovie);
}
/*
* PVR Recordings
*/
else if (URIUtils::IsPVRRecording(strMovie))
{
CLog::Log(LOGDEBUG, "%s - Checking for edit decision list (EDL) for PVR recording: %s",
__FUNCTION__, strMovie.c_str());
bFound = ReadPvr(strMovie);
}
if (bFound)
MergeShortCommBreaks();
return bFound;
}
bool CEdl::ReadEdl(const std::string& strMovie, const float fFramesPerSecond)
{
Clear();
std::string edlFilename(URIUtils::ReplaceExtension(strMovie, ".edl"));
if (!CFile::Exists(edlFilename))
return false;
CFile edlFile;
if (!edlFile.Open(edlFilename))
{
CLog::Log(LOGERROR, "%s - Could not open EDL file: %s", __FUNCTION__, edlFilename.c_str());
return false;
}
bool bError = false;
int iLine = 0;
std::string strBuffer;
strBuffer.resize(1024);
while (edlFile.ReadString(&strBuffer[0], 1024))
{
// Log any errors from previous run in the loop
if (bError)
CLog::Log(LOGWARNING, "%s - Error on line %i in EDL file: %s", __FUNCTION__, iLine, edlFilename.c_str());
bError = false;
iLine++;
char buffer1[513];
char buffer2[513];
int iAction;
int iFieldsRead = sscanf(strBuffer.c_str(), "%512s %512s %i", buffer1,
buffer2, &iAction);
if (iFieldsRead != 2 && iFieldsRead != 3) // Make sure we read the right number of fields
{
bError = true;
continue;
}
std::vector<std::string> strFields(2);
strFields[0] = buffer1;
strFields[1] = buffer2;
if (iFieldsRead == 2) // If only 2 fields read, then assume it's a scene marker.
{
iAction = atoi(strFields[1].c_str());
strFields[1] = strFields[0];
}
/*
* For each of the first two fields read, parse based on whether it is a time string
* (HH:MM:SS.sss), frame marker (#12345), or normal seconds string (123.45).
*/
int64_t iCutStartEnd[2];
for (int i = 0; i < 2; i++)
{
if (strFields[i].find(":") != std::string::npos) // HH:MM:SS.sss format
{
std::vector<std::string> fieldParts = StringUtils::Split(strFields[i], '.');
if (fieldParts.size() == 1) // No ms
{
iCutStartEnd[i] = StringUtils::TimeStringToSeconds(fieldParts[0]) * (int64_t)1000; // seconds to ms
}
else if (fieldParts.size() == 2) // Has ms. Everything after the dot (.) is ms
{
/*
* Have to pad or truncate the ms portion to 3 characters before converting to ms.
*/
if (fieldParts[1].length() == 1)
{
fieldParts[1] = fieldParts[1] + "00";
}
else if (fieldParts[1].length() == 2)
{
fieldParts[1] = fieldParts[1] + "0";
}
else if (fieldParts[1].length() > 3)
{
fieldParts[1] = fieldParts[1].substr(0, 3);
}
iCutStartEnd[i] = (int64_t)StringUtils::TimeStringToSeconds(fieldParts[0]) * 1000 + atoi(fieldParts[1].c_str()); // seconds to ms
}
else
{
bError = true;
continue;
}
}
else if (strFields[i][0] == '#') // #12345 format for frame number
{
iCutStartEnd[i] = (int64_t)(atol(strFields[i].substr(1).c_str()) / fFramesPerSecond * 1000); // frame number to ms
}
else // Plain old seconds in float format, e.g. 123.45
{
iCutStartEnd[i] = (int64_t)(atof(strFields[i].c_str()) * 1000); // seconds to ms
}
}
if (bError) // If there was an error in the for loop, ignore and continue with the next line
continue;
Cut cut;
cut.start = iCutStartEnd[0];
cut.end = iCutStartEnd[1];
switch (iAction)
{
case 0:
cut.action = CUT;
if (!AddCut(cut))
{
CLog::Log(LOGWARNING, "%s - Error adding cut from line %i in EDL file: %s", __FUNCTION__,
iLine, edlFilename.c_str());
continue;
}
break;
case 1:
cut.action = MUTE;
if (!AddCut(cut))
{
CLog::Log(LOGWARNING, "%s - Error adding mute from line %i in EDL file: %s", __FUNCTION__,
iLine, edlFilename.c_str());
continue;
}
break;
case 2:
if (!AddSceneMarker(cut.end))
{
CLog::Log(LOGWARNING, "%s - Error adding scene marker from line %i in EDL file: %s",
__FUNCTION__, iLine, edlFilename.c_str());
continue;
}
break;
case 3:
cut.action = COMM_BREAK;
if (!AddCut(cut))
{
CLog::Log(LOGWARNING, "%s - Error adding commercial break from line %i in EDL file: %s",
__FUNCTION__, iLine, edlFilename.c_str());
continue;
}
break;
default:
CLog::Log(LOGWARNING, "%s - Invalid action on line %i in EDL file: %s", __FUNCTION__, iLine,
edlFilename.c_str());
continue;
}
}
if (bError) // Log last line warning, if there was one, since while loop will have terminated.
CLog::Log(LOGWARNING, "%s - Error on line %i in EDL file: %s", __FUNCTION__, iLine, edlFilename.c_str());
edlFile.Close();
if (HasCut() || HasSceneMarker())
{
CLog::Log(LOGDEBUG, "%s - Read %" PRIuS" cuts and %" PRIuS" scene markers in EDL file: %s", __FUNCTION__,
m_vecCuts.size(), m_vecSceneMarkers.size(), edlFilename.c_str());
return true;
}
else
{
CLog::Log(LOGDEBUG, "%s - No cuts or scene markers found in EDL file: %s", __FUNCTION__,
edlFilename.c_str());
return false;
}
}
bool CEdl::ReadComskip(const std::string& strMovie, const float fFramesPerSecond)
{
Clear();
std::string comskipFilename(URIUtils::ReplaceExtension(strMovie, ".txt"));
if (!CFile::Exists(comskipFilename))
return false;
CFile comskipFile;
if (!comskipFile.Open(comskipFilename))
{
CLog::Log(LOGERROR, "%s - Could not open Comskip file: %s", __FUNCTION__, comskipFilename.c_str());
return false;
}
char szBuffer[1024];
if (comskipFile.ReadString(szBuffer, 1023)
&& strncmp(szBuffer, COMSKIP_HEADER, strlen(COMSKIP_HEADER)) != 0) // Line 1.
{
CLog::Log(LOGERROR, "%s - Invalid Comskip file: %s. Error reading line 1 - expected '%s' at start.",
__FUNCTION__, comskipFilename.c_str(), COMSKIP_HEADER);
comskipFile.Close();
return false;
}
int iFrames;
float fFrameRate;
if (sscanf(szBuffer, "FILE PROCESSING COMPLETE %i FRAMES AT %f", &iFrames, &fFrameRate) != 2)
{
/*
* Not all generated Comskip files have the frame rate information.
*/
fFrameRate = fFramesPerSecond;
CLog::Log(LOGWARNING, "%s - Frame rate not in Comskip file. Using detected frames per second: %.3f",
__FUNCTION__, fFrameRate);
}
else
fFrameRate /= 100; // Reduce by factor of 100 to get fps.
(void)comskipFile.ReadString(szBuffer, 1023); // Line 2. Ignore "-------------"
bool bValid = true;
int iLine = 2;
while (bValid && comskipFile.ReadString(szBuffer, 1023)) // Line 3 and onwards.
{
iLine++;
double dStartFrame, dEndFrame;
if (sscanf(szBuffer, "%lf %lf", &dStartFrame, &dEndFrame) == 2)
{
Cut cut;
cut.start = (int64_t)(dStartFrame / fFrameRate * 1000);
cut.end = (int64_t)(dEndFrame / fFrameRate * 1000);
cut.action = COMM_BREAK;
bValid = AddCut(cut);
}
else
bValid = false;
}
comskipFile.Close();
if (!bValid)
{
CLog::Log(LOGERROR, "%s - Invalid Comskip file: %s. Error on line %i. Clearing any valid commercial breaks found.",
__FUNCTION__, comskipFilename.c_str(), iLine);
Clear();
return false;
}
else if (HasCut())
{
CLog::Log(LOGDEBUG, "%s - Read %" PRIuS" commercial breaks from Comskip file: %s", __FUNCTION__, m_vecCuts.size(),
comskipFilename.c_str());
return true;
}
else
{
CLog::Log(LOGDEBUG, "%s - No commercial breaks found in Comskip file: %s", __FUNCTION__, comskipFilename.c_str());
return false;
}
}
bool CEdl::ReadVideoReDo(const std::string& strMovie)
{
/*
* VideoReDo file is strange. Tags are XML like, but it isn't an XML file.
*
* http://www.videoredo.com/
*/
Clear();
std::string videoReDoFilename(URIUtils::ReplaceExtension(strMovie, ".Vprj"));
if (!CFile::Exists(videoReDoFilename))
return false;
CFile videoReDoFile;
if (!videoReDoFile.Open(videoReDoFilename))
{
CLog::Log(LOGERROR, "%s - Could not open VideoReDo file: %s", __FUNCTION__, videoReDoFilename.c_str());
return false;
}
char szBuffer[1024];
if (videoReDoFile.ReadString(szBuffer, 1023)
&& strncmp(szBuffer, VIDEOREDO_HEADER, strlen(VIDEOREDO_HEADER)) != 0)
{
CLog::Log(LOGERROR, "%s - Invalid VideoReDo file: %s. Error reading line 1 - expected %s. Only version 2 files are supported.",
__FUNCTION__, videoReDoFilename.c_str(), VIDEOREDO_HEADER);
videoReDoFile.Close();
return false;
}
int iLine = 1;
bool bValid = true;
while (bValid && videoReDoFile.ReadString(szBuffer, 1023))
{
iLine++;
if (strncmp(szBuffer, VIDEOREDO_TAG_CUT, strlen(VIDEOREDO_TAG_CUT)) == 0) // Found the <Cut> tag
{
/*
* double is used as 32 bit float would overflow.
*/
double dStart, dEnd;
if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_CUT), "%lf:%lf", &dStart, &dEnd) == 2)
{
/*
* Times need adjusting by 1/10,000 to get ms.
*/
Cut cut;
cut.start = (int64_t)(dStart / 10000);
cut.end = (int64_t)(dEnd / 10000);
cut.action = CUT;
bValid = AddCut(cut);
}
else
bValid = false;
}
else if (strncmp(szBuffer, VIDEOREDO_TAG_SCENE, strlen(VIDEOREDO_TAG_SCENE)) == 0) // Found the <SceneMarker > tag
{
int iScene;
double dSceneMarker;
if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_SCENE), " %i>%lf", &iScene, &dSceneMarker) == 2)
bValid = AddSceneMarker((int64_t)(dSceneMarker / 10000)); // Times need adjusting by 1/10,000 to get ms.
else
bValid = false;
}
/*
* Ignore any other tags.
*/
}
videoReDoFile.Close();
if (!bValid)
{
CLog::Log(LOGERROR, "%s - Invalid VideoReDo file: %s. Error in line %i. Clearing any valid cuts or scenes found.",
__FUNCTION__, videoReDoFilename.c_str(), iLine);
Clear();
return false;
}
else if (HasCut() || HasSceneMarker())
{
CLog::Log(LOGDEBUG, "%s - Read %" PRIuS" cuts and %" PRIuS" scene markers in VideoReDo file: %s", __FUNCTION__,
m_vecCuts.size(), m_vecSceneMarkers.size(), videoReDoFilename.c_str());
return true;
}
else
{
CLog::Log(LOGDEBUG, "%s - No cuts or scene markers found in VideoReDo file: %s", __FUNCTION__,
videoReDoFilename.c_str());
return false;
}
}
bool CEdl::ReadBeyondTV(const std::string& strMovie)
{
Clear();
std::string beyondTVFilename(URIUtils::ReplaceExtension(strMovie, URIUtils::GetExtension(strMovie) + ".chapters.xml"));
if (!CFile::Exists(beyondTVFilename))
return false;
CXBMCTinyXML xmlDoc;
if (!xmlDoc.LoadFile(beyondTVFilename))
{
CLog::Log(LOGERROR, "%s - Could not load Beyond TV file: %s. %s", __FUNCTION__, beyondTVFilename.c_str(),
xmlDoc.ErrorDesc());
return false;
}
if (xmlDoc.Error())
{
CLog::Log(LOGERROR, "%s - Could not parse Beyond TV file: %s. %s", __FUNCTION__, beyondTVFilename.c_str(),
xmlDoc.ErrorDesc());
return false;
}
TiXmlElement *pRoot = xmlDoc.RootElement();
if (!pRoot || strcmp(pRoot->Value(), "cutlist"))
{
CLog::Log(LOGERROR, "%s - Invalid Beyond TV file: %s. Expected root node to be <cutlist>", __FUNCTION__,
beyondTVFilename.c_str());
return false;
}
bool bValid = true;
TiXmlElement *pRegion = pRoot->FirstChildElement("Region");
while (bValid && pRegion)
{
TiXmlElement *pStart = pRegion->FirstChildElement("start");
TiXmlElement *pEnd = pRegion->FirstChildElement("end");
if (pStart && pEnd && pStart->FirstChild() && pEnd->FirstChild())
{
/*
* Need to divide the start and end times by a factor of 10,000 to get msec.
* E.g. <start comment="00:02:44.9980867">1649980867</start>
*
* Use atof so doesn't overflow 32 bit float or integer / long.
* E.g. <end comment="0:26:49.0000009">16090090000</end>
*
* Don't use atoll even though it is more correct as it isn't natively supported by
* Visual Studio.
*
* atof() returns 0 if there were any problems and will subsequently be rejected in AddCut().
*/
Cut cut;
cut.start = (int64_t)(atof(pStart->FirstChild()->Value()) / 10000);
cut.end = (int64_t)(atof(pEnd->FirstChild()->Value()) / 10000);
cut.action = COMM_BREAK;
bValid = AddCut(cut);
}
else
bValid = false;
pRegion = pRegion->NextSiblingElement("Region");
}
if (!bValid)
{
CLog::Log(LOGERROR, "%s - Invalid Beyond TV file: %s. Clearing any valid commercial breaks found.", __FUNCTION__,
beyondTVFilename.c_str());
Clear();
return false;
}
else if (HasCut())
{
CLog::Log(LOGDEBUG, "%s - Read %" PRIuS" commercial breaks from Beyond TV file: %s", __FUNCTION__, m_vecCuts.size(),
beyondTVFilename.c_str());
return true;
}
else
{
CLog::Log(LOGDEBUG, "%s - No commercial breaks found in Beyond TV file: %s", __FUNCTION__,
beyondTVFilename.c_str());
return false;
}
}
bool CEdl::ReadPvr(const std::string &strMovie)
{
if (!PVR::g_PVRManager.IsStarted())
{
CLog::Log(LOGERROR, "%s - PVR Manager not started, cannot read Edl for %s", __FUNCTION__, strMovie.c_str());
return false;
}
CFileItemPtr tag = PVR::g_PVRRecordings->GetByPath(strMovie);
if (tag && tag->HasPVRRecordingInfoTag())
{
CLog::Log(LOGDEBUG, "%s - Reading Edl for recording: %s", __FUNCTION__, tag->GetPVRRecordingInfoTag()->m_strTitle.c_str());
}
else
{
CLog::Log(LOGERROR, "%s - Unable to find PVR recording: %s", __FUNCTION__, strMovie.c_str());
return false;
}
std::vector<PVR_EDL_ENTRY> edl = tag->GetPVRRecordingInfoTag()->GetEdl();
std::vector<PVR_EDL_ENTRY>::const_iterator it;
for (it = edl.begin(); it != edl.end(); ++it)
{
Cut cut;
cut.start = it->start;
cut.end = it->end;
switch (it->type)
{
case PVR_EDL_TYPE_CUT:
cut.action = CUT;
break;
case PVR_EDL_TYPE_MUTE:
cut.action = MUTE;
break;
case PVR_EDL_TYPE_SCENE:
if (!AddSceneMarker(cut.end))
{
CLog::Log(LOGWARNING, "%s - Error adding scene marker for pvr recording", __FUNCTION__);
}
continue;
case PVR_EDL_TYPE_COMBREAK:
cut.action = COMM_BREAK;
break;
default:
CLog::Log(LOGINFO, "%s - Ignoring entry of unknown type: %d", __FUNCTION__, it->type);
continue;
}
if (AddCut(cut))
{
CLog::Log(LOGDEBUG, "%s - Added break [%s - %s] found in PVRRecording for: %s.",
__FUNCTION__, MillisecondsToTimeString(cut.start).c_str(),
MillisecondsToTimeString(cut.end).c_str(), strMovie.c_str());
}
else
{
CLog::Log(LOGERROR, "%s - Invalid break [%s - %s] found in PVRRecording for: %s. Continuing anyway.",
__FUNCTION__, MillisecondsToTimeString(cut.start).c_str(),
MillisecondsToTimeString(cut.end).c_str(), strMovie.c_str());
}
}
return !edl.empty();
}
bool CEdl::AddCut(Cut& cut)
{
if (cut.action != CUT && cut.action != MUTE && cut.action != COMM_BREAK)
{
CLog::Log(LOGERROR, "%s - Not a CUT, MUTE, or COMM_BREAK! [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
return false;
}
if (cut.start < 0)
{
CLog::Log(LOGERROR, "%s - Before start! [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
return false;
}
if (cut.start >= cut.end)
{
CLog::Log(LOGERROR, "%s - Times are around the wrong way or the same! [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
return false;
}
if (InCut(cut.start) || InCut(cut.end))
{
CLog::Log(LOGERROR, "%s - Start or end is in an existing cut! [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
return false;
}
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (cut.start < m_vecCuts[i].start && cut.end > m_vecCuts[i].end)
{
CLog::Log(LOGERROR, "%s - Cut surrounds an existing cut! [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
return false;
}
}
if (cut.action == COMM_BREAK)
{
/*
* Detection isn't perfect near the edges of commercial breaks so automatically wait for a bit at
* the start (autowait) and automatically rewind by a bit (autowind) at the end of the commercial
* break.
*/
int autowait = g_advancedSettings.m_iEdlCommBreakAutowait * 1000; // seconds -> ms
int autowind = g_advancedSettings.m_iEdlCommBreakAutowind * 1000; // seconds -> ms
if (cut.start > 0) // Only autowait if not at the start.
cut.start += autowait;
if (cut.end > cut.start + autowind) // Only autowind if it won't go back past the start (should never happen).
cut.end -= autowind;
}
/*
* Insert cut in the list in the right position (ALL algorithms assume cuts are in ascending order)
*/
if (m_vecCuts.empty() || cut.start > m_vecCuts.back().start)
{
CLog::Log(LOGDEBUG, "%s - Pushing new cut to back [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
m_vecCuts.push_back(cut);
}
else
{
std::vector<Cut>::iterator pCurrentCut;
for (pCurrentCut = m_vecCuts.begin(); pCurrentCut != m_vecCuts.end(); ++pCurrentCut)
{
if (cut.start < pCurrentCut->start)
{
CLog::Log(LOGDEBUG, "%s - Inserting new cut [%s - %s], %d", __FUNCTION__,
MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
cut.action);
m_vecCuts.insert(pCurrentCut, cut);
break;
}
}
}
if (cut.action == CUT)
m_iTotalCutTime += cut.end - cut.start;
return true;
}
bool CEdl::AddSceneMarker(const int iSceneMarker)
{
Cut cut;
if (InCut(iSceneMarker, &cut) && cut.action == CUT) // Only works for current cuts.
return false;
CLog::Log(LOGDEBUG, "%s - Inserting new scene marker: %s", __FUNCTION__,
MillisecondsToTimeString(iSceneMarker).c_str());
m_vecSceneMarkers.push_back(iSceneMarker); // Unsorted
return true;
}
bool CEdl::HasCut() const
{
return !m_vecCuts.empty();
}
int CEdl::GetTotalCutTime() const
{
return m_iTotalCutTime; // ms
}
int CEdl::RemoveCutTime(int iSeek) const
{
if (!HasCut())
return iSeek;
/**
* @todo Consider an optimization of using the (now unused) total cut time if the seek time
* requested is later than the end of the last recorded cut. For example, when calculating the
* total duration for display.
*/
int iCutTime = 0;
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (m_vecCuts[i].action == CUT)
{
if (iSeek >= m_vecCuts[i].start && iSeek <= m_vecCuts[i].end) // Inside cut
iCutTime += iSeek - m_vecCuts[i].start - 1; // Decrease cut length by 1ms to jump over end boundary.
else if (iSeek >= m_vecCuts[i].start) // Cut has already been passed over.
iCutTime += m_vecCuts[i].end - m_vecCuts[i].start;
}
}
return iSeek - iCutTime;
}
int CEdl::RestoreCutTime(int iClock) const
{
if (!HasCut())
return iClock;
int iSeek = iClock;
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (m_vecCuts[i].action == CUT && iSeek >= m_vecCuts[i].start)
iSeek += m_vecCuts[i].end - m_vecCuts[i].start;
}
return iSeek;
}
bool CEdl::HasSceneMarker() const
{
return !m_vecSceneMarkers.empty();
}
std::string CEdl::GetInfo() const
{
std::string strInfo;
if (HasCut())
{
int cutCount = 0, muteCount = 0, commBreakCount = 0;
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
switch (m_vecCuts[i].action)
{
case CUT:
cutCount++;
break;
case MUTE:
muteCount++;
break;
case COMM_BREAK:
commBreakCount++;
break;
}
}
if (cutCount > 0)
strInfo += StringUtils::Format("c%i", cutCount);
if (muteCount > 0)
strInfo += StringUtils::Format("m%i", muteCount);
if (commBreakCount > 0)
strInfo += StringUtils::Format("b%i", commBreakCount);
}
if (HasSceneMarker())
strInfo += StringUtils::Format("s%" PRIuS, m_vecSceneMarkers.size());
return strInfo.empty() ? "-" : strInfo;
}
bool CEdl::InCut(const int iSeek, Cut *pCut) const
{
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (iSeek < m_vecCuts[i].start) // Early exit if not even up to the cut start time.
return false;
if (iSeek >= m_vecCuts[i].start && iSeek <= m_vecCuts[i].end) // Inside cut.
{
if (pCut)
*pCut = m_vecCuts[i];
return true;
}
}
return false;
}
bool CEdl::GetNearestCut(bool bPlus, const int iSeek, Cut *pCut) const
{
if (bPlus)
{
// Searching forwards
for (auto &cut : m_vecCuts)
{
if (iSeek >= cut.start && iSeek <= cut.end) // Inside cut.
{
if (pCut)
*pCut = cut;
return true;
}
else if (iSeek < cut.start) // before this cut
{
if (pCut)
*pCut = cut;
return true;
}
}
return false;
}
else
{
// Searching backwards
for (int i = (int)m_vecCuts.size() - 1; i >= 0; i--)
{
if (iSeek - 20000 >= m_vecCuts[i].start && iSeek <= m_vecCuts[i].end)
// Inside cut. We ignore if we're closer to 20 seconds inside
{
if (pCut)
*pCut = m_vecCuts[i];
return true;
}
else if (iSeek > m_vecCuts[i].end) // after this cut
{
if (pCut)
*pCut = m_vecCuts[i];
return true;
}
}
return false;
}
}
bool CEdl::GetNextSceneMarker(bool bPlus, const int iClock, int *iSceneMarker) const
{
if (!HasSceneMarker())
return false;
int iSeek = RestoreCutTime(iClock);
int iDiff = 10 * 60 * 60 * 1000; // 10 hours to ms.
bool bFound = false;
if (bPlus) // Find closest scene forwards
{
for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
{
if ((m_vecSceneMarkers[i] > iSeek) && ((m_vecSceneMarkers[i] - iSeek) < iDiff))
{
iDiff = m_vecSceneMarkers[i] - iSeek;
*iSceneMarker = m_vecSceneMarkers[i];
bFound = true;
}
}
}
else // Find closest scene backwards
{
for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
{
if ((m_vecSceneMarkers[i] < iSeek) && ((iSeek - m_vecSceneMarkers[i]) < iDiff))
{
iDiff = iSeek - m_vecSceneMarkers[i];
*iSceneMarker = m_vecSceneMarkers[i];
bFound = true;
}
}
}
/*
* If the scene marker is in a cut then return the end of the cut. Can't guarantee that this is
* picked up when scene markers are added.
*/
Cut cut;
if (bFound && InCut(*iSceneMarker, &cut) && cut.action == CUT)
*iSceneMarker = cut.end;
return bFound;
}
std::string CEdl::MillisecondsToTimeString(const int iMilliseconds)
{
std::string strTimeString = StringUtils::SecondsToTimeString((long)(iMilliseconds / 1000), TIME_FORMAT_HH_MM_SS); // milliseconds to seconds
strTimeString += StringUtils::Format(".%03i", iMilliseconds % 1000);
return strTimeString;
}
void CEdl::MergeShortCommBreaks()
{
/*
* mythcommflag routinely seems to put a 20-40ms commercial break at the start of the recording.
*
* Remove any spurious short commercial breaks at the very start so they don't interfere with
* the algorithms below.
*/
if (!m_vecCuts.empty()
&& m_vecCuts[0].action == COMM_BREAK
&& (m_vecCuts[0].end - m_vecCuts[0].start) < 5 * 1000) // 5 seconds
{
CLog::Log(LOGDEBUG, "%s - Removing short commercial break at start [%s - %s]. <5 seconds", __FUNCTION__,
MillisecondsToTimeString(m_vecCuts[0].start).c_str(), MillisecondsToTimeString(m_vecCuts[0].end).c_str());
m_vecCuts.erase(m_vecCuts.begin());
}
if (g_advancedSettings.m_bEdlMergeShortCommBreaks)
{
for (int i = 0; i < (int)m_vecCuts.size() - 1; i++)
{
if ((m_vecCuts[i].action == COMM_BREAK && m_vecCuts[i + 1].action == COMM_BREAK)
&& (m_vecCuts[i + 1].end - m_vecCuts[i].start < g_advancedSettings.m_iEdlMaxCommBreakLength * 1000) // s to ms
&& (m_vecCuts[i + 1].start - m_vecCuts[i].end < g_advancedSettings.m_iEdlMaxCommBreakGap * 1000)) // s to ms
{
Cut commBreak;
commBreak.action = COMM_BREAK;
commBreak.start = m_vecCuts[i].start;
commBreak.end = m_vecCuts[i + 1].end;
CLog::Log(LOGDEBUG, "%s - Consolidating commercial break [%s - %s] and [%s - %s] to: [%s - %s]", __FUNCTION__,
MillisecondsToTimeString(m_vecCuts[i].start).c_str(), MillisecondsToTimeString(m_vecCuts[i].end).c_str(),
MillisecondsToTimeString(m_vecCuts[i + 1].start).c_str(), MillisecondsToTimeString(m_vecCuts[i + 1].end).c_str(),
MillisecondsToTimeString(commBreak.start).c_str(), MillisecondsToTimeString(commBreak.end).c_str());
/*
* Erase old cuts and insert the new merged one.
*/
m_vecCuts.erase(m_vecCuts.begin() + i, m_vecCuts.begin() + i + 2);
m_vecCuts.insert(m_vecCuts.begin() + i, commBreak);
i--; // Reduce i to see if the next break is also within the max commercial break length.
}
}
/*
* To cater for recordings that are started early and then have a commercial break identified
* before the TV show starts, expand the first commercial break to the very beginning if it
* starts within the maximum start gap. This is done outside of the consolidation to prevent
* the maximum commercial break length being triggered.
*/
if (!m_vecCuts.empty()
&& m_vecCuts[0].action == COMM_BREAK
&& m_vecCuts[0].start < g_advancedSettings.m_iEdlMaxStartGap * 1000)
{
CLog::Log(LOGDEBUG, "%s - Expanding first commercial break back to start [%s - %s].", __FUNCTION__,
MillisecondsToTimeString(m_vecCuts[0].start).c_str(), MillisecondsToTimeString(m_vecCuts[0].end).c_str());
m_vecCuts[0].start = 0;
}
/*
* Remove any commercial breaks shorter than the minimum (unless at the start)
*/
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (m_vecCuts[i].action == COMM_BREAK
&& m_vecCuts[i].start > 0
&& (m_vecCuts[i].end - m_vecCuts[i].start) < g_advancedSettings.m_iEdlMinCommBreakLength * 1000)
{
CLog::Log(LOGDEBUG, "%s - Removing short commercial break [%s - %s]. Minimum length: %i seconds", __FUNCTION__,
MillisecondsToTimeString(m_vecCuts[i].start).c_str(), MillisecondsToTimeString(m_vecCuts[i].end).c_str(),
g_advancedSettings.m_iEdlMinCommBreakLength);
m_vecCuts.erase(m_vecCuts.begin() + i);
i--;
}
}
}
/*
* Add in scene markers at the start and end of the commercial breaks.
*/
for (int i = 0; i < (int)m_vecCuts.size(); i++)
{
if (m_vecCuts[i].action == COMM_BREAK)
{
if (m_vecCuts[i].start > 0) // Don't add a scene marker at the start.
AddSceneMarker(m_vecCuts[i].start);
AddSceneMarker(m_vecCuts[i].end);
}
}
return;
}
#pragma once
/*
* Copyright (C) 2005-2013 Team XBMC
* http://xbmc.org
*
* This Program 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 2, or (at your option)
* any later version.
*
* This Program 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 XBMC; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*
*/
#include <string>
#include <vector>
class CEdl
{
public:
CEdl();
typedef enum
{
CUT = 0,
MUTE = 1,
// SCENE = 2,
COMM_BREAK = 3
} Action;
struct Cut
{
int start; // ms
int end; // ms
Action action;
};
bool ReadEditDecisionLists(const std::string& strMovie, const float fFramesPerSecond, const int iHeight);
void Clear();
bool HasCut() const;
bool HasSceneMarker() const;
std::string GetInfo() const;
int GetTotalCutTime() const;
int RemoveCutTime(int iSeek) const;
int RestoreCutTime(int iClock) const;
bool InCut(int iSeek, Cut *pCut = NULL) const;
bool GetNearestCut(bool bPlus, const int iSeek, Cut *pCut) const;
bool GetNextSceneMarker(bool bPlus, const int iClock, int *iSceneMarker) const;
static std::string MillisecondsToTimeString(const int iMilliseconds);
protected:
private:
int m_iTotalCutTime; // ms
std::vector<Cut> m_vecCuts;
std::vector<int> m_vecSceneMarkers;
bool ReadEdl(const std::string& strMovie, const float fFramesPerSecond);
bool ReadComskip(const std::string& strMovie, const float fFramesPerSecond);
bool ReadVideoReDo(const std::string& strMovie);
bool ReadBeyondTV(const std::string& strMovie);
bool ReadPvr(const std::string& strMovie);
bool AddCut(Cut& NewCut);
bool AddSceneMarker(const int sceneMarker);
void MergeShortCommBreaks();
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment