1 #include "tiio_ffmpeg.h"
2 #include "../toonz/tapp.h"
3 #include "tsystem.h"
4 #include "tsound.h"
5 
6 #include <QProcess>
7 #include <QDir>
8 #include <QtGui/QImage>
9 #include <QRegExp>
10 #include "toonz/preferences.h"
11 #include "toonz/toonzfolders.h"
12 #include "tmsgcore.h"
13 
Ffmpeg()14 Ffmpeg::Ffmpeg() {
15   m_ffmpegPath         = Preferences::instance()->getFfmpegPath();
16   m_ffmpegTimeout      = Preferences::instance()->getFfmpegTimeout() * 1000;
17   std::string strPath  = m_ffmpegPath.toStdString();
18   m_intermediateFormat = "png";
19 }
~Ffmpeg()20 Ffmpeg::~Ffmpeg() {}
21 
checkFfmpeg()22 bool Ffmpeg::checkFfmpeg() {
23   // check the user defined path in preferences first
24   QString path = Preferences::instance()->getFfmpegPath() + "/ffmpeg";
25 #if defined(_WIN32)
26   path = path + ".exe";
27 #endif
28   if (TSystem::doesExistFileOrLevel(TFilePath(path))) return true;
29 
30   // check the OpenToonz root directory next
31   path = QDir::currentPath() + "/ffmpeg";
32 #if defined(_WIN32)
33   path = path + ".exe";
34 #endif
35   if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
36     Preferences::instance()->setValue(ffmpegPath, QDir::currentPath());
37     return true;
38   }
39 
40   // give up
41   return false;
42 }
43 
checkFfprobe()44 bool Ffmpeg::checkFfprobe() {
45   // check the user defined path in preferences first
46   QString path = Preferences::instance()->getFfmpegPath() + "/ffprobe";
47 #if defined(_WIN32)
48   path = path + ".exe";
49 #endif
50   if (TSystem::doesExistFileOrLevel(TFilePath(path))) return true;
51 
52   // check the OpenToonz root directory next
53   path = QDir::currentPath() + "/ffprobe";
54 #if defined(_WIN32)
55   path = path + ".exe";
56 #endif
57   if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
58     Preferences::instance()->setValue(ffmpegPath, QDir::currentPath());
59     return true;
60   }
61 
62   // give up
63   return false;
64 }
65 
checkFormat(std::string format)66 bool Ffmpeg::checkFormat(std::string format) {
67   QString path = Preferences::instance()->getFfmpegPath() + "/ffmpeg";
68 #if defined(_WIN32)
69   path = path + ".exe";
70 #endif
71   QStringList args;
72   args << "-formats";
73   QProcess ffmpeg;
74   ffmpeg.start(path, args);
75   ffmpeg.waitForFinished();
76   QString results = ffmpeg.readAllStandardError();
77   results += ffmpeg.readAllStandardOutput();
78   ffmpeg.close();
79   std::string strResults = results.toStdString();
80   std::string::size_type n;
81   n = strResults.find(format);
82   if (n != std::string::npos)
83     return true;
84   else
85     return false;
86 }
87 
getFfmpegCache()88 TFilePath Ffmpeg::getFfmpegCache() {
89   QString cacheRoot = ToonzFolder::getCacheRootFolder().getQString();
90   if (!TSystem::doesExistFileOrLevel(TFilePath(cacheRoot + "/ffmpeg"))) {
91     TSystem::mkDir(TFilePath(cacheRoot + "/ffmpeg"));
92   }
93   std::string ffmpegPath =
94       TFilePath(cacheRoot + "/ffmpeg").getQString().toStdString();
95   return TFilePath(cacheRoot + "/ffmpeg");
96 }
97 
setFrameRate(double fps)98 void Ffmpeg::setFrameRate(double fps) { m_frameRate = fps; }
99 
setPath(TFilePath path)100 void Ffmpeg::setPath(TFilePath path) { m_path = path; }
101 
createIntermediateImage(const TImageP & img,int frameIndex)102 void Ffmpeg::createIntermediateImage(const TImageP &img, int frameIndex) {
103   m_frameCount++;
104   if (m_frameNumberOffset == -1) m_frameNumberOffset = frameIndex - 1;
105   QString tempPath = getFfmpegCache().getQString() + "//" +
106                      QString::fromStdString(m_path.getName()) + "tempOut" +
107                      QString::number(frameIndex - m_frameNumberOffset) + "." +
108                      m_intermediateFormat;
109   std::string saveStatus = "";
110   TRasterImageP tempImage(img);
111   TRasterImage *image = (TRasterImage *)tempImage->cloneImage();
112 
113   m_lx           = image->getRaster()->getLx();
114   m_ly           = image->getRaster()->getLy();
115   m_bpp          = image->getRaster()->getPixelSize();
116   int totalBytes = m_lx * m_ly * m_bpp;
117   image->getRaster()->yMirror();
118 
119   // lock raster to get data
120   image->getRaster()->lock();
121   void *buffin = image->getRaster()->getRawData();
122   assert(buffin);
123   void *buffer = malloc(totalBytes);
124   memcpy(buffer, buffin, totalBytes);
125 
126   image->getRaster()->unlock();
127 
128   // create QImage save format
129   QByteArray ba      = m_intermediateFormat.toUpper().toLatin1();
130   const char *format = ba.data();
131 
132   QImage *qi = new QImage((uint8_t *)buffer, m_lx, m_ly, QImage::Format_ARGB32);
133   qi->save(tempPath, format, -1);
134   free(buffer);
135   m_cleanUpList.push_back(tempPath);
136 
137   delete qi;
138   delete image;
139 }
140 
runFfmpeg(QStringList preIArgs,QStringList postIArgs,bool includesInPath,bool includesOutPath,bool overWriteFiles)141 void Ffmpeg::runFfmpeg(QStringList preIArgs, QStringList postIArgs,
142                        bool includesInPath, bool includesOutPath,
143                        bool overWriteFiles) {
144   QString tempName = "//" + QString::fromStdString(m_path.getName()) +
145                      "tempOut%d." + m_intermediateFormat;
146   tempName = getFfmpegCache().getQString() + tempName;
147 
148   QStringList args;
149   args = args + preIArgs;
150   if (!includesInPath) {  // NOTE:  if including the in path, it needs to be in
151                           // the preIArgs argument.
152     args << "-i";
153     args << tempName;
154   }
155   if (m_hasSoundTrack) args = args + m_audioArgs;
156   args = args + postIArgs;
157   if (overWriteFiles && !includesOutPath) {  // if includesOutPath is true, you
158                                              // need to include the overwrite in
159                                              // your postIArgs.
160     args << "-y";
161   }
162   if (!includesOutPath) {
163     args << m_path.getQString();
164   }
165 
166   // write the file
167   QProcess ffmpeg;
168   ffmpeg.start(m_ffmpegPath + "/ffmpeg", args);
169   if (ffmpeg.waitForFinished(m_ffmpegTimeout)) {
170     QString results = ffmpeg.readAllStandardError();
171     results += ffmpeg.readAllStandardOutput();
172     int exitCode = ffmpeg.exitCode();
173     ffmpeg.close();
174     std::string strResults = results.toStdString();
175   } else {
176     DVGui::warning(
177         QObject::tr("FFmpeg timed out.\n"
178                     "Please check the file for errors.\n"
179                     "If the file doesn't play or is incomplete, \n"
180                     "Please try raising the FFmpeg timeout in Preferences."));
181   }
182 }
183 
runFfprobe(QStringList args)184 QString Ffmpeg::runFfprobe(QStringList args) {
185   QProcess ffmpeg;
186   ffmpeg.start(m_ffmpegPath + "/ffprobe", args);
187   ffmpeg.waitForFinished(m_ffmpegTimeout);
188   QString results = ffmpeg.readAllStandardError();
189   results += ffmpeg.readAllStandardOutput();
190   int exitCode = ffmpeg.exitCode();
191   ffmpeg.close();
192   // If the url cannot be opened or recognized as a multimedia file, ffprobe
193   // returns a positive exit code.
194   if (exitCode > 0) throw TImageException(m_path, "error reading info.");
195   std::string strResults = results.toStdString();
196   return results;
197 }
198 
saveSoundTrack(TSoundTrack * st)199 void Ffmpeg::saveSoundTrack(TSoundTrack *st) {
200   m_sampleRate    = st->getSampleRate();
201   m_channelCount  = st->getChannelCount();
202   m_bitsPerSample = st->getBitPerSample();
203   // LONG count = st->getSampleCount();
204   int bufSize         = st->getSampleCount() * st->getSampleSize();
205   const UCHAR *buffer = st->getRawData();
206 
207   m_audioPath = getFfmpegCache().getQString() + "//" +
208                 QString::fromStdString(m_path.getName()) + "tempOut.raw";
209   m_audioFormat = "s" + QString::number(m_bitsPerSample);
210   if (m_bitsPerSample > 8) m_audioFormat = m_audioFormat + "le";
211   std::string strPath = m_audioPath.toStdString();
212 
213   QByteArray data;
214   data.insert(0, (char *)buffer, bufSize);
215 
216   QFile file(m_audioPath);
217   file.open(QIODevice::WriteOnly);
218   file.write(data);
219   file.close();
220   m_hasSoundTrack = true;
221 
222   m_audioArgs << "-f";
223   m_audioArgs << m_audioFormat;
224   m_audioArgs << "-ar";
225   m_audioArgs << QString::number(m_sampleRate);
226   m_audioArgs << "-ac";
227   m_audioArgs << QString::number(m_channelCount);
228   m_audioArgs << "-i";
229   m_audioArgs << m_audioPath;
230 
231   // add file to framesWritten for cleanup
232   m_cleanUpList.push_back(m_audioPath);
233 }
234 
checkFilesExist()235 bool Ffmpeg::checkFilesExist() {
236   QString ffmpegCachePath = getFfmpegCache().getQString();
237   QString tempPath = ffmpegCachePath + "//" + cleanPathSymbols() + "In0001." +
238                      m_intermediateFormat;
239   if (TSystem::doesExistFileOrLevel(TFilePath(tempPath))) {
240     return true;
241   } else
242     return false;
243 }
244 
getInfo()245 ffmpegFileInfo Ffmpeg::getInfo() {
246   QString ffmpegCachePath = getFfmpegCache().getQString();
247   QString tempPath = ffmpegCachePath + "//" + cleanPathSymbols() + ".txt";
248   if (QFile::exists(tempPath)) {
249     QFile infoText(tempPath);
250     infoText.open(QIODevice::ReadOnly);
251     QByteArray ba = infoText.readAll();
252     infoText.close();
253     QString text = QString::fromStdString(ba.toStdString());
254     m_lx         = text.split(" ")[0].toInt();
255     m_ly         = text.split(" ")[1].toInt();
256     m_frameRate  = text.split(" ")[2].toDouble();
257     m_frameCount = text.split(" ")[3].toInt();
258   } else {
259     QFile infoText(tempPath);
260     getSize();
261     getFrameCount();
262     getFrameRate();
263     infoText.open(QIODevice::WriteOnly);
264     std::string infoToWrite =
265         std::to_string(m_lx) + " " + std::to_string(m_ly) + " " +
266         std::to_string(m_frameRate) + " " + std::to_string(m_frameCount);
267     int infoLength = infoToWrite.length();
268     infoText.write(infoToWrite.c_str(), infoLength);
269     infoText.close();
270   }
271   ffmpegFileInfo info;
272   info.m_lx         = m_lx;
273   info.m_ly         = m_ly;
274   info.m_frameRate  = m_frameRate;
275   info.m_frameCount = m_frameCount;
276   return info;
277 }
getImage(int frameIndex)278 TRasterImageP Ffmpeg::getImage(int frameIndex) {
279   QString ffmpegCachePath = getFfmpegCache().getQString();
280   QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
281   std::string tmpPath     = tempPath.toStdString();
282   // QString tempPath= m_path.getQString();
283   QString number   = QString("%1").arg(frameIndex, 4, 10, QChar('0'));
284   QString tempName = "In" + number + ".png";
285   tempName         = tempPath + tempName;
286 
287   // for debugging
288   std::string strPath = tempName.toStdString();
289   if (TSystem::doesExistFileOrLevel(TFilePath(tempName))) {
290     QImage *temp = new QImage(tempName, "PNG");
291     if (temp) {
292       QImage tempToo = temp->convertToFormat(QImage::Format_ARGB32);
293       delete temp;
294       const UCHAR *bits = tempToo.bits();
295 
296       TRasterPT<TPixelRGBM32> ret;
297       ret.create(m_lx, m_ly);
298       ret->lock();
299       memcpy(ret->getRawData(), bits, m_lx * m_ly * 4);
300       ret->unlock();
301       ret->yMirror();
302       return TRasterImageP(ret);
303     }
304   }
305   return TRasterImageP();
306 }
307 
getFrameRate()308 double Ffmpeg::getFrameRate() {
309   QStringList fpsArgs;
310   int fpsNum = 0, fpsDen = 0;
311   fpsArgs << "-v";
312   fpsArgs << "error";
313   fpsArgs << "-select_streams";
314   fpsArgs << "v:0";
315   fpsArgs << "-show_entries";
316   fpsArgs << "stream=r_frame_rate";
317   fpsArgs << "-of";
318   fpsArgs << "default=noprint_wrappers=1:nokey=1";
319   fpsArgs << m_path.getQString();
320   QString fpsResults = runFfprobe(fpsArgs);
321 
322   QStringList fpsResultsList = fpsResults.split("/");
323   if (fpsResultsList.size() > 1) {
324     fpsNum = fpsResultsList[0].toInt();
325     fpsDen = fpsResultsList[1].toInt();
326   }
327 
328   // if for some reason we don't have enough info to calculate it. Use the
329   // avg_frame_rate
330   if (!fpsDen) {
331     fpsArgs.clear();
332     fpsArgs << "-v";
333     fpsArgs << "error";
334     fpsArgs << "-select_streams";
335     fpsArgs << "v:0";
336     fpsArgs << "-show_entries";
337     fpsArgs << "stream=avg_frame_rate";
338     fpsArgs << "-of";
339     fpsArgs << "default=noprint_wrappers=1:nokey=1";
340     fpsArgs << m_path.getQString();
341     QString fpsResults = runFfprobe(fpsArgs);
342 
343     fpsResultsList = fpsResults.split("/");
344     if (fpsResultsList.size() > 1) {
345       fpsNum = fpsResultsList[0].toInt();
346       fpsDen = fpsResultsList[1].toInt();
347     }
348   }
349 
350   if (fpsDen > 0) {
351     m_frameRate = (double)fpsNum / (double)fpsDen;
352   }
353   return m_frameRate;
354 }
355 
getSize()356 TDimension Ffmpeg::getSize() {
357   QStringList sizeArgs;
358   sizeArgs << "-v";
359   sizeArgs << "error";
360   sizeArgs << "-of";
361   sizeArgs << "flat=s=_";
362   sizeArgs << "-select_streams";
363   sizeArgs << "v:0";
364   sizeArgs << "-show_entries";
365   sizeArgs << "stream=height,width";
366   sizeArgs << m_path.getQString();
367 
368   QString sizeResults = runFfprobe(sizeArgs);
369   QStringList split   = sizeResults.split("\n");
370   m_lx                = split[0].split("=")[1].toInt();
371   m_ly                = split[1].split("=")[1].toInt();
372   return TDimension(m_lx, m_ly);
373 }
374 
getFrameCount()375 int Ffmpeg::getFrameCount() {
376   // nb_read_frames from files may not be accurate. Let's calculate it based on
377   // r_frame_rate * duration
378   QStringList frameCountArgs;
379   frameCountArgs << "-v";
380   frameCountArgs << "error";
381   frameCountArgs << "-count_frames";
382   frameCountArgs << "-select_streams";
383   frameCountArgs << "v:0";
384   frameCountArgs << "-show_entries";
385   frameCountArgs << "stream=duration";
386   frameCountArgs << "-of";
387   frameCountArgs << "default=nokey=1:noprint_wrappers=1";
388   frameCountArgs << m_path.getQString();
389 
390   QString frameResults = runFfprobe(frameCountArgs);
391   m_frameCount         = frameResults.toDouble() * getFrameRate();
392 
393   // if for some reason we don't have enough info to calculate it. Use the
394   // nb_read_frames
395   if (!m_frameCount) {
396     frameCountArgs.clear();
397     frameCountArgs << "-v";
398     frameCountArgs << "error";
399     frameCountArgs << "-count_frames";
400     frameCountArgs << "-select_streams";
401     frameCountArgs << "v:0";
402     frameCountArgs << "-show_entries";
403     frameCountArgs << "stream=nb_read_frames";
404     frameCountArgs << "-of";
405     frameCountArgs << "default=nokey=1:noprint_wrappers=1";
406     frameCountArgs << m_path.getQString();
407 
408     frameResults = runFfprobe(frameCountArgs);
409     m_frameCount = frameResults.toInt();
410   }
411 
412   return m_frameCount;
413 }
414 
getFramesFromMovie(int frame)415 void Ffmpeg::getFramesFromMovie(int frame) {
416   QString ffmpegCachePath = getFfmpegCache().getQString();
417   QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
418   std::string tmpPath     = tempPath.toStdString();
419   QString tempName        = "In%04d." + m_intermediateFormat;
420   tempName                = tempPath + tempName;
421   QString tempStart;
422   if (frame == -1) {
423     tempStart = "In0001." + m_intermediateFormat;
424     tempStart = tempPath + tempStart;
425   } else {
426     QString number = QString("%1").arg(frame, 4, 10, QChar('0'));
427     tempStart      = tempPath + "In" + number + "." + m_intermediateFormat;
428   }
429   QString tempBase = tempPath + "In";
430   QString addToDelete;
431   if (!TSystem::doesExistFileOrLevel(TFilePath(tempStart))) {
432     // for debugging
433     std::string strPath = tempName.toStdString();
434 
435     QStringList preIFrameArgs;
436     QStringList postIFrameArgs;
437     // frameArgs << "-accurate_seek";
438     // frameArgs << "-ss";
439     // frameArgs << "0" + QString::number(frameIndex / m_info->m_frameRate);
440     preIFrameArgs << "-i";
441     preIFrameArgs << m_path.getQString();
442     postIFrameArgs << "-y";
443     postIFrameArgs << "-f";
444     postIFrameArgs << "image2";
445 
446     postIFrameArgs << tempName;
447 
448     runFfmpeg(preIFrameArgs, postIFrameArgs, true, true, true);
449 
450     for (int i = 1; i <= m_frameCount; i++) {
451       QString number      = QString("%1").arg(i, 4, 10, QChar('0'));
452       addToDelete         = tempBase + number + "." + m_intermediateFormat;
453       std::string delPath = addToDelete.toStdString();
454       // addToCleanUp(addToDelete);
455     }
456   }
457 }
458 
cleanPathSymbols()459 QString Ffmpeg::cleanPathSymbols() {
460   return m_path.getQString().remove(QRegExp(
461       QString::fromUtf8("[-`~!@#$%^&*()_—+=|:;<>«»,.?/{}\'\"\\[\\]\\\\]")));
462 }
463 
getGifFrameCount()464 int Ffmpeg::getGifFrameCount() {
465   int frame               = 1;
466   QString ffmpegCachePath = getFfmpegCache().getQString();
467   QString tempPath        = ffmpegCachePath + "//" + cleanPathSymbols();
468   std::string tmpPath     = tempPath.toStdString();
469   QString tempName        = "In%04d." + m_intermediateFormat;
470   tempName                = tempPath + tempName;
471   QString tempStart;
472   tempStart = "In0001." + m_intermediateFormat;
473   tempStart = tempPath + tempStart;
474   while (TSystem::doesExistFileOrLevel(TFilePath(tempStart))) {
475     frame++;
476     QString number = QString("%1").arg(frame, 4, 10, QChar('0'));
477     tempStart      = tempPath + "In" + number + "." + m_intermediateFormat;
478   }
479   return frame - 1;
480 }
481 
addToCleanUp(QString path)482 void Ffmpeg::addToCleanUp(QString path) {
483   if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
484     m_cleanUpList.push_back(path);
485   }
486 }
487 
cleanUpFiles()488 void Ffmpeg::cleanUpFiles() {
489   for (QString path : m_cleanUpList) {
490     if (TSystem::doesExistFileOrLevel(TFilePath(path))) {
491       TSystem::deleteFile(TFilePath(path));
492     }
493   }
494 }
495 
disablePrecompute()496 void Ffmpeg::disablePrecompute() {
497   Preferences::instance()->setPrecompute(false);
498 }