fixed: and back to int64_t for Edl as it's passing by pointer to dvdplayer. Also...
[xbmc:xbmc.git] / xbmc / Edl.cpp
1 /*
2  *      Copyright (C) 2005-2008 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "Edl.h"
23 #include "StringUtils.h"
24 #include "Util.h"
25 #include "FileSystem/File.h"
26 #include "FileSystem/CMythFile.h"
27 #include "AdvancedSettings.h"
28 #include "utils/log.h"
29 #include "tinyXML/tinyxml.h"
30
31 extern "C"
32 {
33 #include "lib/libcmyth/cmyth.h"
34 }
35
36 using namespace std;
37
38 #define MPLAYER_EDL_FILENAME "special://temp/xbmc.edl"
39 #define COMSKIP_HEADER "FILE PROCESSING COMPLETE"
40 #define VIDEOREDO_HEADER "<Version>2"
41 #define VIDEOREDO_TAG_CUT "<Cut>"
42 #define VIDEOREDO_TAG_SCENE "<SceneMarker "
43
44 using namespace XFILE;
45
46 CEdl::CEdl()
47 {
48   Clear();
49 }
50
51 CEdl::~CEdl()
52 {
53   Clear();
54 }
55
56 void CEdl::Clear()
57 {
58   if (CFile::Exists(MPLAYER_EDL_FILENAME))
59     CFile::Delete(MPLAYER_EDL_FILENAME);
60
61   m_vecCuts.clear();
62   m_vecSceneMarkers.clear();
63   m_iTotalCutTime = 0;
64 }
65
66 bool CEdl::ReadFiles(const CStdString& strMovie, const float fFramesPerSecond)
67 {
68   CLog::Log(LOGDEBUG, "%s - checking for any edit decision list (EDL) files for: %s", __FUNCTION__,
69             strMovie.c_str());
70
71   /*
72    * Read any available format until a valid EDL related file is found.
73    * 
74    * TODO: Surely there's a better way to do this bFound shenanigans.
75    */
76   bool bFound = false;
77
78   if (!bFound)
79     bFound = ReadVideoReDo(strMovie);
80
81   if (!bFound)
82     bFound = ReadEdl(strMovie);
83
84   if (!bFound)
85     bFound = ReadComskip(strMovie, fFramesPerSecond);
86
87   if (!bFound)
88     bFound = ReadBeyondTV(strMovie);
89
90   if (bFound)
91   {
92     MergeShortCommBreaks();
93     WriteMPlayerEdl();
94   }
95   return bFound;
96 }
97
98 bool CEdl::ReadEdl(const CStdString& strMovie)
99 {
100   Clear();
101
102   CStdString edlFilename;
103   CUtil::ReplaceExtension(strMovie, ".edl", edlFilename);
104   if (!CFile::Exists(edlFilename))
105     return false;
106
107   CFile edlFile;
108   if (!edlFile.Open(edlFilename))
109   {
110     CLog::Log(LOGERROR, "%s - Could not open EDL file: %s", __FUNCTION__, edlFilename.c_str());
111     return false;
112   }
113
114   bool bValid = true;
115   char szBuffer[1024];
116   int iLine = 0;
117   while (bValid && edlFile.ReadString(szBuffer, 1023))
118   {
119     iLine++;
120
121     double dStart, dEnd;
122     int iAction;
123     if (sscanf(szBuffer, "%lf %lf %i", &dStart, &dEnd, &iAction) == 3)
124     {
125       if (dStart == dEnd) // Ignore zero length cuts in generated EDL files
126         continue;
127
128       Cut cut;
129       cut.start = (int)dStart * 1000; // ms to s
130       cut.end = (int)dEnd * 1000; // ms to s
131
132       switch (iAction)
133       {
134       case 0:
135         cut.action = CUT;
136         bValid = AddCut(cut);
137         break;
138       case 1:
139         cut.action = MUTE;
140         bValid = AddCut(cut);
141         break;
142       case 2:
143         bValid = AddSceneMarker(cut.end);
144         break;
145       case 3:
146         cut.action = COMM_BREAK;
147         bValid = AddCut(cut);
148         break;
149       default:
150         bValid = false;
151         continue;
152       }
153     }
154     else
155       bValid = false;
156   }
157   edlFile.Close();
158
159   if (!bValid)
160   {
161     CLog::Log(LOGERROR, "%s - Invalid EDL file: %s. Error in line %i. Clearing any valid cuts or scenes found.",
162               __FUNCTION__, edlFilename.c_str(), iLine);
163     Clear();
164     return false;
165   }
166   else if (HasCut() || HasSceneMarker())
167   {
168     CLog::Log(LOGDEBUG, "%s - Read %i cuts and %i scene markers in EDL file: %s", __FUNCTION__,
169               m_vecCuts.size(), m_vecSceneMarkers.size(), edlFilename.c_str());
170     return true;
171   }
172   else
173   {
174     CLog::Log(LOGDEBUG, "%s - No cuts or scene markers found in EDL file: %s", __FUNCTION__,
175               edlFilename.c_str());
176     return false;
177   }
178 }
179
180 bool CEdl::ReadComskip(const CStdString& strMovie, const float fFramesPerSecond)
181 {
182   Clear();
183
184   CStdString comskipFilename;
185   CUtil::ReplaceExtension(strMovie, ".txt", comskipFilename);
186   if (!CFile::Exists(comskipFilename))
187     return false;
188
189   CFile comskipFile;
190   if (!comskipFile.Open(comskipFilename))
191   {
192     CLog::Log(LOGERROR, "%s - Could not open Comskip file: %s", __FUNCTION__, comskipFilename.c_str());
193     return false;
194   }
195
196   char szBuffer[1024];
197   if (comskipFile.ReadString(szBuffer, 1023)
198   &&  strncmp(szBuffer, COMSKIP_HEADER, strlen(COMSKIP_HEADER)) != 0) // Line 1.
199   {
200     CLog::Log(LOGERROR, "%s - Invalid Comskip file: %s. Error reading line 1 - expected '%s' at start.",
201               __FUNCTION__, comskipFilename.c_str(), COMSKIP_HEADER);
202     comskipFile.Close();
203     return false;
204   }
205
206   int iFrames;
207   float fFrameRate;
208   if (sscanf(szBuffer, "FILE PROCESSING COMPLETE %i FRAMES AT %f", &iFrames, &fFrameRate) != 2)
209   {
210     /*
211      * Not all generated Comskip files have the frame rate information.
212      */
213     fFrameRate = fFramesPerSecond;
214     CLog::Log(LOGWARNING, "%s - Frame rate not in Comskip file. Using detected frames per second: %.3f",
215               __FUNCTION__, fFrameRate);
216   }
217   else
218     fFrameRate /= 100; // Reduce by factor of 100 to get fps.
219
220   comskipFile.ReadString(szBuffer, 1023); // Line 2. Ignore "-------------"
221
222   bool bValid = true;
223   int iLine = 2;
224   while (bValid && comskipFile.ReadString(szBuffer, 1023)) // Line 3 and onwards.
225   {
226     iLine++;
227     double dStartFrame, dEndFrame;
228     if (sscanf(szBuffer, "%lf %lf", &dStartFrame, &dEndFrame) == 2)
229     {
230       Cut cut;
231       cut.start = (int64_t)(dStartFrame / fFrameRate * 1000);
232       cut.end = (int64_t)(dEndFrame / fFrameRate * 1000);
233       cut.action = COMM_BREAK;
234       bValid = AddCut(cut);
235     }
236     else
237       bValid = false;
238   }
239   comskipFile.Close();
240
241   if (!bValid)
242   {
243     CLog::Log(LOGERROR, "%s - Invalid Comskip file: %s. Error on line %i. Clearing any valid commercial breaks found.",
244               __FUNCTION__, comskipFilename.c_str(), iLine);
245     Clear();
246     return false;
247   }
248   else if (HasCut())
249   {
250     CLog::Log(LOGDEBUG, "%s - Read %i commercial breaks from Comskip file: %s", __FUNCTION__, m_vecCuts.size(),
251               comskipFilename.c_str());
252     return true;
253   }
254   else
255   {
256     CLog::Log(LOGDEBUG, "%s - No commercial breaks found in Comskip file: %s", __FUNCTION__, comskipFilename.c_str());
257     return false;
258   }
259 }
260
261 bool CEdl::ReadVideoReDo(const CStdString& strMovie)
262 {
263   /*
264    * VideoReDo file is strange. Tags are XML like, but it isn't an XML file.
265    *
266    * http://www.videoredo.com/
267    */
268
269   Clear();
270   CStdString videoReDoFilename;
271   CUtil::ReplaceExtension(strMovie, ".Vprj", videoReDoFilename);
272   if (!CFile::Exists(videoReDoFilename))
273     return false;
274
275   CFile videoReDoFile;
276   if (!videoReDoFile.Open(videoReDoFilename))
277   {
278     CLog::Log(LOGERROR, "%s - Could not open VideoReDo file: %s", __FUNCTION__, videoReDoFilename.c_str());
279     return false;
280   }
281
282   char szBuffer[1024];
283   if (videoReDoFile.ReadString(szBuffer, 1023)
284   &&  strncmp(szBuffer, VIDEOREDO_HEADER, strlen(VIDEOREDO_HEADER)) != 0)
285   {
286     CLog::Log(LOGERROR, "%s - Invalid VideoReDo file: %s. Error reading line 1 - expected %s. Only version 2 files are supported.",
287               __FUNCTION__, videoReDoFilename.c_str(), VIDEOREDO_HEADER);
288     videoReDoFile.Close();
289     return false;
290   }
291
292   int iLine = 1;
293   bool bValid = true;
294   while (bValid && videoReDoFile.ReadString(szBuffer, 1023))
295   {
296     iLine++;
297     if (strncmp(szBuffer, VIDEOREDO_TAG_CUT, strlen(VIDEOREDO_TAG_CUT)) == 0) // Found the <Cut> tag
298     {
299       /*
300        * double is used as 32 bit float would overflow.
301        */
302       double dStart, dEnd;
303       if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_CUT), "%lf:%lf", &dStart, &dEnd) == 2)
304       {
305         /*
306          *  Times need adjusting by 1/10,000 to get ms.
307          */
308         Cut cut;
309         cut.start = (int64_t)(dStart / 10000);
310         cut.end = (int64_t)(dEnd / 10000);
311         cut.action = CUT;
312         bValid = AddCut(cut);
313       }
314       else
315         bValid = false;
316     }
317     else if (strncmp(szBuffer, VIDEOREDO_TAG_SCENE, strlen(VIDEOREDO_TAG_SCENE)) == 0) // Found the <SceneMarker > tag
318     {
319       int iScene;
320       double dSceneMarker;
321       if (sscanf(szBuffer + strlen(VIDEOREDO_TAG_SCENE), " %i>%lf", &iScene, &dSceneMarker) == 2)
322         bValid = AddSceneMarker((int)dSceneMarker / 10000); // Times need adjusting by 1/10,000 to get ms.
323       else
324         bValid = false;
325     }
326     /*
327      * Ignore any other tags.
328      */
329   }
330   videoReDoFile.Close();
331
332   if (!bValid)
333   {
334     CLog::Log(LOGERROR, "%s - Invalid VideoReDo file: %s. Error in line %i. Clearing any valid cuts or scenes found.",
335               __FUNCTION__, videoReDoFilename.c_str(), iLine);
336     Clear();
337     return false;
338   }
339   else if (HasCut() || HasSceneMarker())
340   {
341     CLog::Log(LOGDEBUG, "%s - Read %i cuts and %i scene markers in VideoReDo file: %s", __FUNCTION__,
342               m_vecCuts.size(), m_vecSceneMarkers.size(), videoReDoFilename.c_str());
343     return true;
344   }
345   else
346   {
347     CLog::Log(LOGDEBUG, "%s - No cuts or scene markers found in VideoReDo file: %s", __FUNCTION__,
348               videoReDoFilename.c_str());
349     return false;
350   }
351 }
352
353 bool CEdl::ReadBeyondTV(const CStdString& strMovie)
354 {
355   Clear();
356
357   CStdString beyondTVFilename = strMovie + ".chapters.xml";
358   if (!CFile::Exists(beyondTVFilename))
359     return false;
360
361   TiXmlDocument xmlDoc;
362   if (!xmlDoc.LoadFile(beyondTVFilename))
363   {
364     CLog::Log(LOGERROR, "%s - Could not load Beyond TV file: %s. %s", __FUNCTION__, beyondTVFilename.c_str(),
365               xmlDoc.ErrorDesc());
366     return false;
367   }
368
369   if (xmlDoc.Error())
370   {
371     CLog::Log(LOGERROR, "%s - Could not parse Beyond TV file: %s. %s", __FUNCTION__, beyondTVFilename.c_str(),
372               xmlDoc.ErrorDesc());
373     return false;
374   }
375
376   TiXmlElement *pRoot = xmlDoc.RootElement();
377   if (!pRoot || strcmp(pRoot->Value(), "cutlist"))
378   {
379     CLog::Log(LOGERROR, "%s - Invalid Beyond TV file: %s. Expected root node to be <cutlist>", __FUNCTION__,
380               beyondTVFilename.c_str());
381     return false;
382   }
383
384   bool bValid = true;
385   TiXmlElement *pRegion = pRoot->FirstChildElement("Region");
386   while (bValid && pRegion)
387   {
388     TiXmlElement *pStart = pRegion->FirstChildElement("start");
389     TiXmlElement *pEnd = pRegion->FirstChildElement("end");
390     if (pStart && pEnd && pStart->FirstChild() && pEnd->FirstChild())
391     {
392       /*
393        * Need to divide the start and end times by a factor of 10,000 to get msec.
394        * E.g. <start comment="00:02:44.9980867">1649980867</start>
395        *
396        * Use atof so doesn't overflow 32 bit float or integer / long.
397        * E.g. <end comment="0:26:49.0000009">16090090000</end>
398        *
399        * Don't use atoll even though it is more correct as it isn't natively supported by
400        * Visual Studio.
401        *
402        * GetText() returns 0 if there were any problems and will subsequently be rejected in AddCut().
403        */
404       Cut cut;
405       cut.start = (int64_t)(atof(pStart->GetText()) / 10000);
406       cut.end = (int64_t)(atof(pEnd->GetText()) / 10000);
407       cut.action = COMM_BREAK;
408       bValid = AddCut(cut);
409     }
410     else
411       bValid = false;
412
413     pRegion = pRegion->NextSiblingElement("Region");
414   }
415   if (!bValid)
416   {
417     CLog::Log(LOGERROR, "%s - Invalid Beyond TV file: %s. Clearing any valid commercial breaks found.", __FUNCTION__,
418               beyondTVFilename.c_str());
419     Clear();
420     return false;
421   }
422   else if (HasCut())
423   {
424     CLog::Log(LOGDEBUG, "%s - Read %i commercial breaks from Beyond TV file: %s", __FUNCTION__, m_vecCuts.size(),
425               beyondTVFilename.c_str());
426     return true;
427   }
428   else
429   {
430     CLog::Log(LOGDEBUG, "%s - No commercial breaks found in Beyond TV file: %s", __FUNCTION__,
431               beyondTVFilename.c_str());
432     return false;
433   }
434 }
435
436 bool CEdl::AddCut(const Cut& cut)
437 {
438   if (cut.action != CUT && cut.action != MUTE && cut.action != COMM_BREAK)
439   {
440     CLog::Log(LOGERROR, "%s - Not a CUT, MUTE, or COMM_BREAK! [%s - %s], %d", __FUNCTION__,
441               MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
442               cut.action);
443     return false;
444   }
445
446   if (cut.start < 0)
447   {
448     CLog::Log(LOGERROR, "%s - Before start! [%s - %s], %d", __FUNCTION__,
449               MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
450               cut.action);
451     return false;
452   }
453
454   if (cut.start >= cut.end)
455   {
456     CLog::Log(LOGERROR, "%s - Times are around the wrong way or the same! [%s - %s], %d", __FUNCTION__,
457               MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
458               cut.action);
459     return false;
460   }
461
462   if (InCut(cut.start) || InCut(cut.end))
463   {
464     CLog::Log(LOGERROR, "%s - Start or end is in an existing cut! [%s - %s], %d", __FUNCTION__,
465               MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
466               cut.action);
467     return false;
468   }
469
470   for (int i = 0; i < (int)m_vecCuts.size(); i++)
471   {
472     if (cut.start < m_vecCuts[i].start && cut.end > m_vecCuts[i].end)
473     {
474       CLog::Log(LOGERROR, "%s - Cut surrounds an existing cut! [%s - %s], %d", __FUNCTION__,
475                 MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
476                 cut.action);
477       return false;
478     }
479   }
480
481   /*
482    * Insert cut in the list in the right position (ALL algorithms assume cuts are in ascending order)
483    */
484   if (m_vecCuts.empty() || cut.start > m_vecCuts.back().start)
485   {
486     CLog::Log(LOGDEBUG, "%s - Pushing new cut to back [%s - %s], %d", __FUNCTION__,
487               MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
488               cut.action);
489     m_vecCuts.push_back(cut);
490   }
491   else
492   {
493     vector<Cut>::iterator pCurrentCut;
494     for (pCurrentCut = m_vecCuts.begin(); pCurrentCut != m_vecCuts.end(); pCurrentCut++)
495     {
496       if (cut.start < pCurrentCut->start)
497       {
498         CLog::Log(LOGDEBUG, "%s - Inserting new cut [%s - %s], %d", __FUNCTION__,
499                   MillisecondsToTimeString(cut.start).c_str(), MillisecondsToTimeString(cut.end).c_str(),
500                   cut.action);
501         m_vecCuts.insert(pCurrentCut, cut);
502         break;
503       }
504     }
505   }
506
507   if (cut.action == CUT)
508     m_iTotalCutTime += cut.end - cut.start;
509
510   return true;
511 }
512
513 bool CEdl::AddSceneMarker(const int64_t iSceneMarker)
514 {
515   Cut cut;
516
517   if (InCut(iSceneMarker, &cut) && cut.action == CUT) // Only works for current cuts.
518     return false;
519
520   CLog::Log(LOGDEBUG, "%s - Inserting new scene marker: %s", __FUNCTION__,
521             MillisecondsToTimeString(iSceneMarker).c_str());
522   m_vecSceneMarkers.push_back(iSceneMarker); // Unsorted
523
524   return true;
525 }
526
527 bool CEdl::WriteMPlayerEdl()
528 {
529   if (!HasCut())
530     return false;
531
532   CFile mplayerEdlFile;
533   if (!mplayerEdlFile.OpenForWrite(MPLAYER_EDL_FILENAME, true))
534   {
535     CLog::Log(LOGERROR, "%s - Error opening MPlayer EDL file for writing: %s", __FUNCTION__,
536               MPLAYER_EDL_FILENAME);
537     return false;
538   }
539
540   CStdString strBuffer;
541   for (int i = 0; i < (int)m_vecCuts.size(); i++)
542   {
543     /* 
544      * MPlayer doesn't understand the scene marker (2) or commercial break (3) identifiers that XBMC
545      * supports in EDL files.
546      *
547      * http://www.mplayerhq.hu/DOCS/HTML/en/edl.html
548      *
549      * Write out mutes (1) directly. Treat commercial breaks as cuts (everything other than MUTES = 0).
550      */
551     strBuffer.AppendFormat("%.3f\t%.3f\t%i\n", (float)(m_vecCuts[i].start / 1000),
552                                                (float)(m_vecCuts[i].end / 1000),
553                                                m_vecCuts[i].action == MUTE ? 1 : 0);
554   }
555   mplayerEdlFile.Write(strBuffer.c_str(), strBuffer.size());
556   mplayerEdlFile.Close();
557
558   CLog::Log(LOGDEBUG, "%s - MPlayer EDL file written to: %s", __FUNCTION__, MPLAYER_EDL_FILENAME);
559
560   return true;
561 }
562
563 bool CEdl::HasCut()
564 {
565   return !m_vecCuts.empty();
566 }
567
568 int64_t CEdl::GetTotalCutTime()
569 {
570   return m_iTotalCutTime; // ms
571 }
572
573 int64_t CEdl::RemoveCutTime(int64_t iSeek)
574 {
575   if (!HasCut())
576     return iSeek;
577
578   int64_t iCutTime = 0;
579   for (int i = 0; i < (int)m_vecCuts.size(); i++)
580   {
581     if (m_vecCuts[i].action == CUT)
582     {
583       if (iSeek >= m_vecCuts[i].start && iSeek <= m_vecCuts[i].end) // Inside cut
584         iCutTime += iSeek - m_vecCuts[i].start - 1; // Decrease cut length by 1ms to jump over end boundary.
585       else if (iSeek >= m_vecCuts[i].start) // Cut has already been passed over.
586         iCutTime += m_vecCuts[i].end - m_vecCuts[i].start;
587     }
588   }
589   return iSeek - iCutTime;
590 }
591
592 int64_t CEdl::RestoreCutTime(int64_t iClock)
593 {
594   if (!HasCut())
595     return iClock;
596
597   int64_t iSeek = iClock;
598   for (int i = 0; i < (int)m_vecCuts.size(); i++)
599   {
600     if (m_vecCuts[i].action == CUT && iSeek >= m_vecCuts[i].start)
601       iSeek += m_vecCuts[i].end - m_vecCuts[i].start;
602   }
603
604   return iSeek;
605 }
606
607 bool CEdl::HasSceneMarker()
608 {
609   return !m_vecSceneMarkers.empty();
610 }
611
612 CStdString CEdl::GetInfo()
613 {
614   /*
615    * TODO: Update wiki (http://xbmc.org/wiki/?title=EDL_(commercial_skipping)_and_SceneMarker_support)
616    * if these different status strings are used.
617    */
618   CStdString strInfo = "";
619   if (HasCut())
620   {
621     int cutCount = 0, muteCount = 0, commBreakCount = 0;
622     for (int i = 0; i < (int)m_vecCuts.size(); i++)
623     {
624       switch (m_vecCuts[i].action)
625       {
626       case CUT:
627         cutCount++;
628         break;
629       case MUTE:
630         muteCount++;
631         break;
632       case COMM_BREAK:
633         commBreakCount++;
634         break;
635       }
636     }
637     if (cutCount > 0)
638       strInfo.AppendFormat("c%i", cutCount);
639     if (muteCount > 0)
640       strInfo.AppendFormat("m%i", muteCount);
641     if (commBreakCount > 0)
642       strInfo.AppendFormat("b%i", commBreakCount);
643   }
644   if (HasSceneMarker())
645     strInfo.AppendFormat("s%i", m_vecSceneMarkers.size());
646
647   return strInfo.IsEmpty() ? "-" : strInfo;
648 }
649
650 bool CEdl::InCut(const int64_t iSeek, Cut *pCut)
651 {
652   for (int i = 0; i < (int)m_vecCuts.size(); i++)
653   {
654     if (iSeek < m_vecCuts[i].start) // Early exit if not even up to the cut start time.
655       return false;
656
657     if (iSeek >= m_vecCuts[i].start && iSeek <= m_vecCuts[i].end) // Inside cut.
658     {
659       if (pCut)
660         *pCut = m_vecCuts[i];
661       return true;
662     }
663   }
664
665   return false;
666 }
667
668 bool CEdl::GetNextSceneMarker(bool bPlus, const int64_t iClock, int64_t *iSceneMarker)
669 {
670   if (!HasSceneMarker())
671     return false;
672
673   int64_t iSeek = RestoreCutTime(iClock);
674
675   int64_t iDiff = 10 * 60 * 60 * 1000; // 10 hours to ms.
676   bool bFound = false;
677
678   if (bPlus) // Find closest scene forwards
679   {
680     for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
681     {
682       if ((m_vecSceneMarkers[i] > iSeek) && ((m_vecSceneMarkers[i] - iSeek) < iDiff))
683       {
684         iDiff = m_vecSceneMarkers[i] - iSeek;
685         *iSceneMarker = m_vecSceneMarkers[i];
686         bFound = true;
687       }
688     }
689   }
690   else // Find closest scene backwards
691   {
692     for (int i = 0; i < (int)m_vecSceneMarkers.size(); i++)
693     {
694       if ((m_vecSceneMarkers[i] < iSeek) && ((iSeek - m_vecSceneMarkers[i]) < iDiff))
695       {
696         iDiff = iSeek - m_vecSceneMarkers[i];
697         *iSceneMarker = m_vecSceneMarkers[i];
698         bFound = true;
699       }
700     }
701   }
702
703   /*
704    * If the scene marker is in a cut then return the end of the cut. Can't guarantee that this is
705    * picked up when scene markers are added.
706    */
707   Cut cut;
708   if (bFound && InCut(*iSceneMarker, &cut) && cut.action == CUT)
709     *iSceneMarker = cut.end;
710
711   return bFound;
712 }
713
714 CStdString CEdl::MillisecondsToTimeString(const int64_t iMilliseconds)
715 {
716   CStdString strTimeString = "";
717   StringUtils::SecondsToTimeString((long)(iMilliseconds / 1000), strTimeString, TIME_FORMAT_HH_MM_SS); // milliseconds to seconds
718   strTimeString.AppendFormat(".%03i", iMilliseconds % 1000);
719   return strTimeString;
720 }
721
722 bool CEdl::ReadMythCommBreaks(const CURL& url, const float fFramesPerSecond)
723 {
724   Clear();
725
726   /*
727    * Exists() sets up all the internal bits needed for GetCommBreakList().
728    */
729   CCMythFile mythFile;
730   if (!mythFile.Exists(url))
731     return false;
732
733   CLog::Log(LOGDEBUG, "%s - Reading commercial breaks from MythTV for: %s", __FUNCTION__,
734             url.GetFileName().c_str());
735
736   cmyth_commbreaklist_t commbreaklist;
737   if (!mythFile.GetCommBreakList(commbreaklist))
738   {
739     CLog::Log(LOGERROR, "%s - Error getting commercial breaks from MythTV for: %s", __FUNCTION__,
740               url.GetFileName().c_str());
741     return false;
742   }
743
744   for (int i = 0; i < (int)commbreaklist->commbreak_count; i++)
745   {
746     cmyth_commbreak_t commbreak = commbreaklist->commbreak_list[i];
747
748     Cut cut;
749     cut.action = COMM_BREAK;
750     cut.start = (int)(commbreak->start_mark / fFramesPerSecond * 1000);
751     cut.end = (int)(commbreak->end_mark / fFramesPerSecond * 1000);
752
753     /*
754      * Detection isn't perfect near the edges so autowind by a small amount into each end of the
755      * detected commercial break.
756      *
757      * TODO: Advanced setting for the autowind amount. Perhaps one for fowards and one for backwards?
758      */
759     int autowind = 2 * 1000; // 2 seconds in ms
760     if (cut.start > 0) // Only autowind forwards if not at the start.
761       cut.start += autowind;
762     if (cut.end > cut.start + autowind) // Only autowind if it won't go back past the start (should never happen).
763       cut.end -= autowind;
764
765     if (!AddCut(cut)) // Log and continue with errors while still testing.
766       CLog::Log(LOGERROR, "%s - Invalid commercial break [%s - %s] found in MythTV for: %s. autowind: %d. Continuing anyway.",
767                 __FUNCTION__, MillisecondsToTimeString(cut.start).c_str(),
768                 MillisecondsToTimeString(cut.end).c_str(), url.GetFileName().c_str(), autowind);
769   }
770
771   if (HasCut())
772   {
773     CLog::Log(LOGDEBUG, "%s - Added %i commercial breaks from MythTV for: %s. Used detected frame rate of %.3f fps to calculate times from the frame markers.",
774               __FUNCTION__, m_vecCuts.size(), url.GetFileName().c_str(), fFramesPerSecond);
775     MergeShortCommBreaks();
776     /*
777      * No point writing the MPlayer EDL file as it won't be able to play the MythTV files anyway.
778      */
779     return true;
780   }
781   else
782   {
783     CLog::Log(LOGDEBUG, "%s - No commercial breaks found in MythTV for: %s", __FUNCTION__,
784               url.GetFileName().c_str());
785     return false;
786   }
787 }
788
789 void CEdl::MergeShortCommBreaks()
790 {
791   if (g_advancedSettings.m_bEdlMergeShortCommBreaks)
792   {
793     for (int i = 0; i < (int)m_vecCuts.size() - 1; i++)
794     {
795       if ((m_vecCuts[i].action == COMM_BREAK && m_vecCuts[i + 1].action == COMM_BREAK)
796       &&  (m_vecCuts[i + 1].end - m_vecCuts[i].start < g_advancedSettings.m_iEdlMaxCommBreakLength * 1000) // s to ms
797       &&  (m_vecCuts[i + 1].start - m_vecCuts[i].end < g_advancedSettings.m_iEdlMaxCommBreakGap * 1000)) // s to ms
798       {
799         Cut commBreak;
800         commBreak.action = COMM_BREAK;
801         commBreak.start = m_vecCuts[i].start;
802         commBreak.end = m_vecCuts[i + 1].end;
803
804         CLog::Log(LOGDEBUG, "%s - Consolidating commercial break [%s - %s] and [%s - %s] to: [%s - %s]", __FUNCTION__,
805                   MillisecondsToTimeString(m_vecCuts[i].start).c_str(), MillisecondsToTimeString(m_vecCuts[i].end).c_str(),
806                   MillisecondsToTimeString(m_vecCuts[i + 1].start).c_str(), MillisecondsToTimeString(m_vecCuts[i + 1].end).c_str(),
807                   MillisecondsToTimeString(commBreak.start).c_str(), MillisecondsToTimeString(commBreak.end).c_str());
808
809         /*
810          * Erase old cuts and insert the new merged one.
811          */
812         m_vecCuts.erase(m_vecCuts.begin() + i, m_vecCuts.begin() + i + 2);
813         m_vecCuts.insert(m_vecCuts.begin() + i, commBreak);
814
815         i--; // Reduce i to see if the next break is also within the max commercial break length.
816       }
817     }
818
819     /*
820      * Remove any commercial breaks shorter than the minimum (unless at the start)
821      */
822     for (int i = 0; i < (int)m_vecCuts.size(); i++)
823     {
824       if (m_vecCuts[i].action == COMM_BREAK
825       &&  m_vecCuts[i].start > 0
826       && (m_vecCuts[i].end - m_vecCuts[i].start) < g_advancedSettings.m_iEdlMinCommBreakLength * 1000)
827       {
828         CLog::Log(LOGDEBUG, "%s - Removing short commercial break [%s - %s]. Minimum length: %i seconds", __FUNCTION__,
829                   MillisecondsToTimeString(m_vecCuts[i].start).c_str(), MillisecondsToTimeString(m_vecCuts[i].end).c_str(),
830                   g_advancedSettings.m_iEdlMinCommBreakLength);
831         m_vecCuts.erase(m_vecCuts.begin() + i);
832
833         i--;
834       }
835     }
836   }
837
838   /*
839    * Add in scene markers at the start and end of the commercial breaks.
840    */
841   for (int i = 0; i < (int)m_vecCuts.size(); i++)
842   {
843     if (m_vecCuts[i].action == COMM_BREAK)
844     {
845       if (m_vecCuts[i].start > 0) // Don't add a scene marker at the start.
846         AddSceneMarker(m_vecCuts[i].start);
847       AddSceneMarker(m_vecCuts[i].end);
848     }
849   }
850   return;
851 }