1 /*
2  * Copyright (c) 2020-2021 Meltytech, LLC
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "proxymanager.h"
19 #include "mltcontroller.h"
20 #include "settings.h"
21 #include "shotcut_mlt_properties.h"
22 #include "jobqueue.h"
23 #include "jobs/ffmpegjob.h"
24 #include "jobs/qimagejob.h"
25 #include "util.h"
26 
27 #include <QObject>
28 #include <QVector>
29 #include <QXmlStreamReader>
30 #include <QXmlStreamWriter>
31 #include <QFile>
32 #include <QImageReader>
33 #include <Logger.h>
34 #include <utime.h>
35 
36 static const char* kProxySubfolder = "proxies";
37 static const char* kProxyVideoExtension = ".mp4";
38 static const char* kProxyPendingVideoExtension = ".pending.mp4";
39 static const char* kProxyImageExtension = ".jpg";
40 static const char* kProxyPendingImageExtension = ".pending.jpg";
41 static const float kProxyResolutionRatio = 1.3f;
42 static const int   kFallbackProxyResolution = 540;
43 static const QStringList kPixFmtsWithAlpha = {"pal8", "argb", "rgba", "abgr",
44     "bgra", "yuva420p", "yuva422p", "yuva444p", "yuva420p9be", "yuva420p9le",
45     "yuva422p9be", "yuva422p9le", "yuva444p9be", "yuva444p9le", "yuva420p10be",
46     "yuva420p10le", "yuva422p10be", "yuva422p10le", "yuva444p10be", "yuva444p10le",
47     "yuva420p16be", "yuva420p16le", "yuva422p16be", "yuva422p16le", "yuva444p16be",
48     "yuva444p16le", "rgba64be", "rgba64le", "bgra64be", "bgra64le", "ya8",
49     "ya16le", "ya16be", "gbrap", "gbrap16le", "gbrap16be", "ayuv64le", "ayuv64be",
50     "gbrap12le", "gbrap12be", "gbrap10le", "gbrap10be", "gbrapf32be",
51     "gbrapf32le", "yuva422p12be", "yuva422p12le", "yuva444p12be", "yuva444p12le"};
52 
dir()53 QDir ProxyManager::dir()
54 {
55     // Use project folder + "/proxies" if using project folder and enabled
56     QDir dir(MLT.projectFolder());
57     if (!MLT.projectFolder().isEmpty() && dir.exists() && Settings.proxyUseProjectFolder()) {
58         if (!dir.cd(kProxySubfolder)) {
59             if (dir.mkdir(kProxySubfolder))
60                 dir.cd(kProxySubfolder);
61         }
62     } else {
63         // Otherwise, use app setting
64         dir = QDir(Settings.proxyFolder());
65     }
66     return dir;
67 }
68 
resource(Mlt::Service & producer)69 QString ProxyManager::resource(Mlt::Service& producer)
70 {
71     QString resource = QString::fromUtf8(producer.get("resource"));
72     if (producer.get_int(kIsProxyProperty) && producer.get(kOriginalResourceProperty)) {
73         resource = QString::fromUtf8(producer.get(kOriginalResourceProperty));
74     } else if (!::qstrcmp(producer.get("mlt_service"), "timewarp")) {
75         resource = QString::fromUtf8(producer.get("warp_resource"));
76     }
77     return resource;
78 }
79 
generateVideoProxy(Mlt::Producer & producer,bool fullRange,ScanMode scanMode,const QPoint & aspectRatio,bool replace)80 void ProxyManager::generateVideoProxy(Mlt::Producer& producer, bool fullRange, ScanMode scanMode, const QPoint& aspectRatio, bool replace)
81 {
82     // Always regenerate per preview scaling or 540 if not specified
83     QString resource = ProxyManager::resource(producer);
84     QStringList args;
85     QString hash = Util::getHash(producer);
86     QString fileName = ProxyManager::dir().filePath(hash + kProxyPendingVideoExtension);
87     QString filters;
88     auto hwCodecs = Settings.encodeHardware();
89     QString hwFilters;
90 
91     // Touch file to make it in progress
92     QFile file(fileName);
93     file.open(QIODevice::WriteOnly);
94     file.resize(0);
95     file.close();
96 
97     args << "-loglevel" << "verbose";
98     args << "-i" << resource;
99     args << "-max_muxing_queue_size" << "9999";
100     // transcode all streams except data, subtitles, and attachments
101     auto audioIndex = producer.property_exists(kDefaultAudioIndexProperty)? producer.get_int(kDefaultAudioIndexProperty) : producer.get_int("audio_index");
102     if (producer.get_int("video_index") < audioIndex) {
103         args << "-map" << "0:V?" << "-map" << "0:a?";
104     } else {
105         args << "-map" << "0:a?" << "-map" << "0:V?";
106     }
107     args << "-map_metadata" << "0" << "-ignore_unknown";
108     args << "-vf";
109 
110     if (scanMode == Automatic) {
111         filters = QString("yadif=deint=interlaced,");
112     } else if (scanMode != Progressive) {
113         filters = QString("yadif=parity=%1,").arg(scanMode == InterlacedTopFieldFirst? "tff" : "bff");
114     }
115     filters += QString("scale=width=-2:height=%1").arg(resolution());
116     if (Settings.proxyUseHardware() && (hwCodecs.contains("hevc_vaapi") || hwCodecs.contains("h264_vaapi"))) {
117         hwFilters = ",format=nv12,hwupload";
118     }
119     if (fullRange) {
120         args << filters + ":in_range=full:out_range=full" + hwFilters;
121         args << "-color_range" << "jpeg";
122     } else {
123         args << filters + ":in_range=mpeg:out_range=mpeg" + hwFilters;
124         args << "-color_range" << "mpeg";
125     }
126     switch (producer.get_int("meta.media.colorspace")) {
127     case 601:
128         if (producer.get_int("meta.media.height") == 576) {
129             args << "-color_primaries" << "bt470bg";
130             args << "-color_trc" << "smpte170m";
131             args << "-colorspace" << "bt470bg";
132         } else {
133             args << "-color_primaries" << "smpte170m";
134             args << "-color_trc" << "smpte170m";
135             args << "-colorspace" << "smpte170m";
136         }
137         break;
138     case 170:
139         args << "-color_primaries" << "smpte170m";
140         args << "-color_trc" << "smpte170m";
141         args << "-colorspace" << "smpte170m";
142         break;
143     case 240:
144         args << "-color_primaries" << "smpte240m";
145         args << "-color_trc" << "smpte240m";
146         args << "-colorspace" << "smpte240m";
147         break;
148     case 470:
149         args << "-color_primaries" << "bt470bg";
150         args << "-color_trc" << "bt470bg";
151         args << "-colorspace" << "bt470bg";
152         break;
153     default:
154         args << "-color_primaries" << "bt709";
155         args << "-color_trc" << "bt709";
156         args << "-colorspace" << "bt709";
157         break;
158     }
159     if (!aspectRatio.isNull()) {
160         args << "-aspect" << QString("%1:%2").arg(aspectRatio.x()).arg(aspectRatio.y());
161     }
162     args << "-f" << "mp4" << "-codec:a" << "ac3" << "-b:a" << "256k";
163     args << "-pix_fmt" << "yuv420p";
164     if (Settings.proxyUseHardware()) {
165         if (hwCodecs.contains("hevc_nvenc")) {
166             args << "-codec:v" << "hevc_nvenc";
167             args << "-rc" << "constqp";
168             args << "-vglobal_quality" << "37";
169         } else if (hwCodecs.contains("hevc_qsv")) {
170             args << "-load_plugin" << "hevc_hw";
171             args << "-codec:v" << "hevc_qsv";
172             args << "-q:v" << "36";
173         } else if (hwCodecs.contains("hevc_amf")) {
174             args << "-codec:v" << "hevc_amf";
175             args << "-rc" << "1";
176             args << "-qp_i" << "32" << "-qp_p" << "32";
177         } else if (hwCodecs.contains("hevc_vaapi")) {
178             args << "-init_hw_device" << "vaapi=vaapi0:,connection_type=x11" << "-filter_hw_device" << "vaapi0";
179             args << "-codec:v" << "hevc_vaapi";
180             args << "-qp" << "37";
181         } else if (hwCodecs.contains("h264_nvenc")) {
182             args << "-codec:v" << "h264_nvenc";
183             args << "-rc" << "constqp";
184             args << "-vglobal_quality" << "37";
185         } else if (hwCodecs.contains("h264_vaapi")) {
186             args << "-init_hw_device" << "vaapi=vaapi0:,connection_type=x11" << "-filter_hw_device" << "vaapi0";
187             args << "-codec:v" << "h264_vaapi";
188             args << "-qp" << "30";
189         } else if (hwCodecs.contains("hevc_videotoolbox")) {
190             args << "-codec:v" << "hevc_videotoolbox";
191             args << "-b:v" << "2M";
192         } else if (hwCodecs.contains("h264_videotoolbox")) {
193             args << "-codec:v" << "h264_videotoolbox";
194             args << "-b:v" << "2M";
195         } else if (hwCodecs.contains("h264_qsv")) {
196             args << "-codec:v" << "h264_qsv";
197             args << "-q:v" << "36";
198         } else if (hwCodecs.contains("h264_amf")) {
199             args << "-codec:v" << "h264_amf";
200             args << "-rc" << "1";
201             args << "-qp_i" << "32" << "-qp_p" << "32";
202         }
203     }
204     if (!args.contains("-codec:v")) {
205         args << "-codec:v" << "libx264";
206         args << "-preset" << "veryfast";
207         args << "-crf" << "23";
208     }
209     args << "-g" << "1" << "-bf" << "0";
210     args << "-y" << fileName;
211 
212     FfmpegJob* job = new FfmpegJob(fileName, args, true);
213     job->setLabel(QObject::tr("Make proxy for %1").arg(Util::baseName(resource)));
214     if (replace) {
215         job->setPostJobAction(new ProxyReplacePostJobAction(resource, fileName, hash));
216     } else {
217         job->setPostJobAction(new ProxyFinalizePostJobAction(fileName));
218     }
219     JOBS.add(job);
220 }
221 
generateImageProxy(Mlt::Producer & producer,bool replace)222 void ProxyManager::generateImageProxy(Mlt::Producer& producer, bool replace)
223 {
224     // Always regenerate per preview scaling or 540 if not specified
225     QString resource = ProxyManager::resource(producer);
226     QStringList args;
227     QString hash = Util::getHash(producer);
228     QString fileName = ProxyManager::dir().filePath(hash + kProxyPendingImageExtension);
229     QString filters;
230 
231     // Touch file to make it in progress
232     QFile file(fileName);
233     file.open(QIODevice::WriteOnly);
234     file.resize(0);
235     file.close();
236 
237     AbstractJob* job = new QImageJob(fileName, resource, resolution());
238     if (replace) {
239         job->setPostJobAction(new ProxyReplacePostJobAction(resource, fileName, hash));
240     } else {
241         job->setPostJobAction(new ProxyFinalizePostJobAction(fileName));
242     }
243     JOBS.add(job);
244 }
245 
246 typedef QPair<QString, QString> MltProperty;
247 
processProperties(QXmlStreamWriter & newXml,QVector<MltProperty> & properties,const QString & root)248 static void processProperties(QXmlStreamWriter& newXml, QVector<MltProperty>& properties, const QString& root)
249 {
250     // Determine if this is a proxy resource
251     bool isProxy = false;
252     QString newResource;
253     QString service;
254     QString speed = "1";
255     for (const auto& p: properties) {
256         if (p.first == kIsProxyProperty) {
257             isProxy = true;
258         } else if (p.first == kOriginalResourceProperty) {
259             newResource = p.second;
260         } else if (newResource.isEmpty() && p.first == "resource") {
261             newResource = p.second;
262         } else if (p.first == "mlt_service") {
263             service = p.second;
264         } else if (p.first == "warp_speed") {
265             speed = p.second;
266         }
267     }
268     QVector<MltProperty> newProperties;
269     QVector<MltProperty>& propertiesRef = properties;
270     if (isProxy) {
271         // Filter the properties
272         for (const auto& p: properties) {
273             // Replace the resource property if proxy
274             if (p.first == "resource") {
275                 // Convert to relative
276                 if (!root.isEmpty() && newResource.startsWith(root)) {
277                     newResource = newResource.mid(root.size());
278                 }
279                 if (service == "timewarp") {
280                     newProperties << MltProperty(p.first, QString("%1:%2").arg(speed).arg(newResource));
281                 } else {
282                     newProperties << MltProperty(p.first, newResource);
283                 }
284             } else if (p.first == "warp_resource") {
285                 newProperties << MltProperty(p.first, newResource);
286             // Remove special proxy and original resource properties
287             } else if (p.first != kIsProxyProperty && p.first != kOriginalResourceProperty) {
288                 newProperties << MltProperty(p.first, p.second);
289             }
290         }
291         propertiesRef = newProperties;
292     }
293     // Write all of the property elements
294     for (const auto& p : propertiesRef) {
295         newXml.writeStartElement("property");
296         newXml.writeAttribute("name", p.first);
297         newXml.writeCharacters(p.second);
298         newXml.writeEndElement();
299     }
300     // Reset the saved properties
301     properties.clear();
302 }
303 
filterXML(QString & xmlString,QString root)304 bool ProxyManager::filterXML(QString& xmlString, QString root)
305 {
306     QString output;
307     QXmlStreamReader xml(xmlString);
308     QXmlStreamWriter newXml(&output);
309     bool isPropertyElement = false;
310     QVector<MltProperty> properties;
311 
312     // This prevents processProperties() from mis-matching a resource path that begins with root
313     // when it is converting to relative paths.
314     if (!root.isEmpty() && root.endsWith('/')) {
315         root.append('/');
316     }
317 
318     newXml.setAutoFormatting(true);
319     newXml.setAutoFormattingIndent(2);
320 
321     while (!xml.atEnd()) {
322         switch (xml.readNext()) {
323         case QXmlStreamReader::Characters:
324             if (!isPropertyElement)
325                 newXml.writeCharacters(xml.text().toString());
326             break;
327         case QXmlStreamReader::Comment:
328             newXml.writeComment(xml.text().toString());
329             break;
330         case QXmlStreamReader::DTD:
331             newXml.writeDTD(xml.text().toString());
332             break;
333         case QXmlStreamReader::EntityReference:
334             newXml.writeEntityReference(xml.name().toString());
335             break;
336         case QXmlStreamReader::ProcessingInstruction:
337             newXml.writeProcessingInstruction(xml.processingInstructionTarget().toString(), xml.processingInstructionData().toString());
338             break;
339         case QXmlStreamReader::StartDocument:
340             newXml.writeStartDocument(xml.documentVersion().toString(), xml.isStandaloneDocument());
341             break;
342         case QXmlStreamReader::EndDocument:
343             newXml.writeEndDocument();
344             break;
345         case QXmlStreamReader::StartElement: {
346             const QString element = xml.name().toString();
347             if (element == "property") {
348                 // Save each property element but do not output yet
349                 const QString name = xml.attributes().value("name").toString();
350                 properties << MltProperty(name, xml.readElementText());
351                 isPropertyElement = true;
352             } else {
353                 // At the start of a non-property element
354                 isPropertyElement = false;
355                 processProperties(newXml, properties, root);
356                 // Write the new start element
357                 newXml.writeStartElement(xml.namespaceUri().toString(), element);
358                 for (const auto& a : xml.attributes()) {
359                     newXml.writeAttribute(a);
360                 }
361             }
362             break;
363         }
364         case QXmlStreamReader::EndElement:
365             // At the end of a non-property element
366             if (xml.name() != "property") {
367                 processProperties(newXml, properties, root);
368                 newXml.writeEndElement();
369             }
370             break;
371         default:
372             break;
373         }
374     }
375 
376     // Useful for debugging
377 //        tempFile.open();
378 //        LOG_DEBUG() << tempFile.readAll().constData();
379 //        tempFile.close();
380 
381     if (!xml.hasError()) {
382         xmlString = output;
383         return true;
384     }
385     return false;
386 }
387 
fileExists(Mlt::Producer & producer)388 bool ProxyManager::fileExists(Mlt::Producer& producer)
389 {
390     QDir proxyDir(Settings.proxyFolder());
391     QDir projectDir(MLT.projectFolder());
392     QString service = QString::fromLatin1(producer.get("mlt_service"));
393     QString fileName;
394     if (service.startsWith("avformat")) {
395         fileName = Util::getHash(producer) + kProxyVideoExtension;
396     } else if (isValidImage(producer)) {
397         fileName = Util::getHash(producer) + kProxyImageExtension;
398     } else {
399         return false;
400     }
401     return (projectDir.cd(kProxySubfolder) && projectDir.exists(fileName)) || proxyDir.exists(fileName);
402 }
403 
filePending(Mlt::Producer & producer)404 bool ProxyManager::filePending(Mlt::Producer& producer)
405 {
406     QDir proxyDir(Settings.proxyFolder());
407     QDir projectDir(MLT.projectFolder());
408     QString service = QString::fromLatin1(producer.get("mlt_service"));
409     QString fileName;
410     if (service.startsWith("avformat")) {
411         fileName = Util::getHash(producer) + kProxyPendingVideoExtension;
412     } else if (isValidImage(producer)) {
413         fileName = Util::getHash(producer) + kProxyPendingImageExtension;
414     } else {
415         return false;
416     }
417     return (projectDir.cd(kProxySubfolder) && projectDir.exists(fileName)) || proxyDir.exists(fileName);
418 }
419 
isValidImage(Mlt::Producer & producer)420 bool ProxyManager::isValidImage(Mlt::Producer& producer)
421 {
422     QString service = QString::fromLatin1(producer.get("mlt_service"));
423     if ((service == "qimage" || service == "pixbuf") && !producer.get_int(kShotcutSequenceProperty)) {
424         QImageReader reader;
425         reader.setDecideFormatFromContent(true);
426         reader.setFileName(ProxyManager::resource(producer));
427         return reader.imageCount() == 1 && !reader.read().hasAlphaChannel();
428     }
429     return false;
430 }
431 
isValidVideo(Mlt::Producer producer)432 bool ProxyManager::isValidVideo(Mlt::Producer producer)
433 {
434     QString service = QString::fromLatin1(producer.get("mlt_service"));
435     int video_index = producer.get_int("video_index");
436     // video_index -1 means no video
437     if (video_index < 0)
438         return false;
439     if (service == "avformat-novalidate") {
440         producer = Mlt::Producer(MLT.profile(), resource(producer).toUtf8().constData());
441         service = QString::fromLatin1(producer.get("mlt_service"));
442         producer.set("video_index", video_index);
443     }
444     if (service == "avformat") {
445         QString key = QString("meta.media.%1.codec.pix_fmt").arg(video_index);
446         QString pix_fmt = QString::fromLatin1(producer.get(key.toLatin1().constData()));
447         // Cover art is usually 90000 fps and should not be proxied
448         key = QString("meta.media.%1.codec.frame_rate").arg(video_index);
449         QString frame_rate = producer.get(key.toLatin1().constData());
450         key = QString("meta.media.%1.codec.name").arg(video_index);
451         QString codec_name = producer.get(key.toLatin1().constData());
452         bool coverArt = codec_name == "mjpeg" && frame_rate == "90000";
453         key = QString("meta.attr.%1.stream.alpha_mode.markup").arg(video_index);
454         bool alpha_mode = producer.get_int(key.toLatin1().constData());
455         LOG_DEBUG() << "pix_fmt =" << pix_fmt << " codec.frame_rate =" << frame_rate << " alpha_mode =" << alpha_mode;
456         return !kPixFmtsWithAlpha.contains(pix_fmt) && !alpha_mode && !coverArt;
457     }
458     return false;
459 }
460 
461 // Returns true if the producer exists and was updated with proxy info
generateIfNotExists(Mlt::Producer & producer,bool replace)462 bool ProxyManager::generateIfNotExists(Mlt::Producer& producer, bool replace)
463 {
464     if (Settings.proxyEnabled() && producer.is_valid() && !producer.get_int(kDisableProxyProperty) && !producer.get_int(kIsProxyProperty)) {
465         if (ProxyManager::fileExists(producer)) {
466             QString service = QString::fromLatin1(producer.get("mlt_service"));
467             QDir projectDir(MLT.projectFolder());
468             QString fileName;
469             if (service.startsWith("avformat")) {
470                 fileName = Util::getHash(producer) + kProxyVideoExtension;
471             } else if (isValidImage(producer)) {
472                 fileName = Util::getHash(producer) + kProxyImageExtension;
473             } else {
474                 return false;
475             }
476             producer.set(kIsProxyProperty, 1);
477             producer.set(kOriginalResourceProperty, producer.get("resource"));
478             if (projectDir.exists(fileName)) {
479                 ::utime(projectDir.filePath(fileName).toUtf8().constData(), nullptr);
480                 producer.set("resource", projectDir.filePath(fileName).toUtf8().constData());
481             } else {
482                 QDir proxyDir(Settings.proxyFolder());
483                 ::utime(proxyDir.filePath(fileName).toUtf8().constData(), nullptr);
484                 producer.set("resource", proxyDir.filePath(fileName).toUtf8().constData());
485             }
486             return true;
487         } else if (!filePending(producer)) {
488             if (isValidVideo(producer)) {
489                 // Tag this producer so we do not try to generate proxy again in this session
490                 delete producer.get_frame();
491                 auto threshold = qRound(kProxyResolutionRatio * resolution());
492                 LOG_DEBUG() << producer.get_int("meta.media.width") << "x" << producer.get_int("meta.media.height") << "threshold" << threshold;
493                 if (producer.get_int("meta.media.width") > threshold && producer.get_int("meta.media.height") > threshold) {
494                     ProxyManager::generateVideoProxy(producer, MLT.fullRange(producer), Automatic, QPoint(), replace);
495                 }
496             } else if (isValidImage(producer)) {
497                 // Tag this producer so we do not try to generate proxy again in this session
498                 delete producer.get_frame();
499                 auto threshold = qRound(kProxyResolutionRatio * resolution());
500                 LOG_DEBUG() << producer.get_int("meta.media.width") << "x" << producer.get_int("meta.media.height") << "threshold" << threshold;
501                 if (producer.get_int("meta.media.width") > threshold && producer.get_int("meta.media.height") > threshold) {
502                     ProxyManager::generateImageProxy(producer, replace);
503                 }
504             }
505         }
506     }
507     return false;
508 }
509 
videoFilenameExtension()510 const char* ProxyManager::videoFilenameExtension()
511 {
512     return kProxyVideoExtension;
513 }
514 
pendingVideoExtension()515 const char* ProxyManager::pendingVideoExtension()
516 {
517     return kProxyPendingVideoExtension;
518 }
519 
imageFilenameExtension()520 const char* ProxyManager::imageFilenameExtension()
521 {
522     return kProxyImageExtension;
523 }
524 
pendingImageExtension()525 const char* ProxyManager::pendingImageExtension()
526 {
527     return kProxyImageExtension;
528 }
529 
resolution()530 int ProxyManager::resolution()
531 {
532     return Settings.playerPreviewScale()? Settings.playerPreviewScale() : kFallbackProxyResolution;
533 }
534 
535 class FindNonProxyProducersParser : public Mlt::Parser
536 {
537 private:
538     QString m_hash;
539     QList<Mlt::Producer> m_producers;
540 
541 public:
FindNonProxyProducersParser()542     FindNonProxyProducersParser() : Mlt::Parser() {}
543 
producers()544     QList<Mlt::Producer>& producers() { return m_producers; }
545 
on_start_filter(Mlt::Filter *)546     int on_start_filter(Mlt::Filter*) { return 0; }
on_start_producer(Mlt::Producer * producer)547     int on_start_producer(Mlt::Producer* producer) {
548         if (!producer->parent().get_int(kIsProxyProperty))
549             m_producers << Mlt::Producer(producer);
550         return 0;
551     }
on_end_producer(Mlt::Producer *)552     int on_end_producer(Mlt::Producer*) { return 0; }
on_start_playlist(Mlt::Playlist *)553     int on_start_playlist(Mlt::Playlist*) { return 0; }
on_end_playlist(Mlt::Playlist *)554     int on_end_playlist(Mlt::Playlist*) { return 0; }
on_start_tractor(Mlt::Tractor *)555     int on_start_tractor(Mlt::Tractor*) { return 0; }
on_end_tractor(Mlt::Tractor *)556     int on_end_tractor(Mlt::Tractor*) { return 0; }
on_start_multitrack(Mlt::Multitrack *)557     int on_start_multitrack(Mlt::Multitrack*) { return 0; }
on_end_multitrack(Mlt::Multitrack *)558     int on_end_multitrack(Mlt::Multitrack*) { return 0; }
on_start_track()559     int on_start_track() { return 0; }
on_end_track()560     int on_end_track() { return 0; }
on_end_filter(Mlt::Filter *)561     int on_end_filter(Mlt::Filter*) { return 0; }
on_start_transition(Mlt::Transition *)562     int on_start_transition(Mlt::Transition*) { return 0; }
on_end_transition(Mlt::Transition *)563     int on_end_transition(Mlt::Transition*) { return 0; }
564 };
565 
generateIfNotExistsAll(Mlt::Producer & producer)566 void ProxyManager::generateIfNotExistsAll(Mlt::Producer& producer)
567 {
568     FindNonProxyProducersParser parser;
569     parser.start(producer);
570     for (auto& clip : parser.producers()) {
571         generateIfNotExists(clip, false /* replace */);
572     }
573 }
574 
removePending()575 bool ProxyManager::removePending()
576 {
577     bool foundAny = false;
578     QDir dir(MLT.projectFolder());
579     if (!MLT.projectFolder().isEmpty() && dir.exists()) {
580         dir.cd(kProxySubfolder);
581     } else {
582         dir = QDir(Settings.proxyFolder());
583     }
584     if (dir.exists()) {
585         dir.setNameFilters(QStringList() << "*.pending.*");
586         dir.setFilter(QDir::Files | QDir::NoDotAndDotDot | QDir::Writable);
587         for (const auto& s : dir.entryList()) {
588             LOG_INFO() << "removing" << dir.filePath(s);
589             foundAny |= QFile::remove(dir.filePath(s));
590         }
591     }
592     //TODO if any pending remove, let user know and offer to regenerate?
593     return foundAny;
594 }
595