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