1 /*
2  * Copyright (c) 2012-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 "avformatproducerwidget.h"
19 #include "ui_avformatproducerwidget.h"
20 #include "util.h"
21 #include "mltcontroller.h"
22 #include "shotcut_mlt_properties.h"
23 #include "dialogs/filedatedialog.h"
24 #include "jobqueue.h"
25 #include "jobs/ffprobejob.h"
26 #include "jobs/ffmpegjob.h"
27 #include "jobs/meltjob.h"
28 #include "jobs/postjobaction.h"
29 #include "settings.h"
30 #include "mainwindow.h"
31 #include "Logger.h"
32 #include "qmltypes/qmlapplication.h"
33 #include "proxymanager.h"
34 #include "dialogs/longuitask.h"
35 #include "spatialmedia/spatialmedia.h"
36 
37 #include <QtWidgets>
38 
ProducerIsTimewarp(Mlt::Producer * producer)39 static bool ProducerIsTimewarp( Mlt::Producer* producer )
40 {
41     return QString::fromUtf8(producer->get("mlt_service")) == "timewarp";
42 }
43 
GetFilenameFromProducer(Mlt::Producer * producer,bool useOriginal=true)44 static QString GetFilenameFromProducer(Mlt::Producer* producer, bool useOriginal = true)
45 {
46     QString resource;
47     if (useOriginal && producer->get(kOriginalResourceProperty)) {
48         resource = QString::fromUtf8(producer->get(kOriginalResourceProperty));
49     } else if (ProducerIsTimewarp(producer)) {
50         resource = QString::fromUtf8(producer->get("warp_resource"));
51     } else {
52         resource = QString::fromUtf8(producer->get("resource"));
53     }
54     if (QFileInfo(resource).isRelative()) {
55         QString basePath = QFileInfo(MAIN.fileName()).canonicalPath();
56         QFileInfo fi(basePath, resource);
57         resource = fi.filePath();
58     }
59     return resource;
60 }
61 
GetSpeedFromProducer(Mlt::Producer * producer)62 static double GetSpeedFromProducer( Mlt::Producer* producer )
63 {
64     double speed = 1.0;
65     if (ProducerIsTimewarp(producer)) {
66         speed = fabs(producer->get_double("warp_speed"));
67     }
68     return speed;
69 }
70 
DecodeTask(AvformatProducerWidget * widget)71 DecodeTask::DecodeTask(AvformatProducerWidget* widget)
72     : QObject(0)
73     , QRunnable()
74     , m_frame(widget->producer()->get_frame())
75 {
76     connect(this, SIGNAL(frameDecoded()), widget, SLOT(onFrameDecoded()));
77 }
78 
run()79 void DecodeTask::run()
80 {
81     mlt_image_format format = mlt_image_none;
82     int w = MLT.profile().width();
83     int h = MLT.profile().height();
84     m_frame->get_image(format, w, h);
85     emit frameDecoded();
86 }
87 
AvformatProducerWidget(QWidget * parent)88 AvformatProducerWidget::AvformatProducerWidget(QWidget *parent)
89     : QWidget(parent)
90     , ui(new Ui::AvformatProducerWidget)
91     , m_defaultDuration(-1)
92     , m_recalcDuration(true)
93 {
94     ui->setupUi(this);
95     ui->timelineDurationText->setFixedWidth(ui->durationSpinBox->width());
96     ui->filenameLabel->setFrame(true);
97     Util::setColorsToHighlight(ui->filenameLabel, QPalette::Base);
98     if (Settings.playerGPU())
99         connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, SLOT(onFrameDisplayed(const SharedFrame&)));
100     else
101         connect(this, SIGNAL(producerChanged(Mlt::Producer*)), SLOT(onProducerChanged()));
102 }
103 
~AvformatProducerWidget()104 AvformatProducerWidget::~AvformatProducerWidget()
105 {
106     delete ui;
107 }
108 
newProducer(Mlt::Profile & profile)109 Mlt::Producer* AvformatProducerWidget::newProducer(Mlt::Profile& profile)
110 {
111     Mlt::Producer* p = 0;
112     if ( ui->speedSpinBox->value() == 1.0 )
113     {
114         p = new Mlt::Producer(profile, GetFilenameFromProducer(producer(), false).toUtf8().constData());
115     }
116     else
117     {
118         // If the system language's numeric format and region's numeric format differ, then MLT
119         // uses the language's numeric format while Qt is using the region's. Thus, to
120         // supply a proper numeric format in string form to MLT, we must use MLT instead of
121         // letting Qt convert it.
122         Mlt::Properties tempProps;
123         tempProps.set("speed", ui->speedSpinBox->value());
124         QString warpspeed = QString::fromLatin1(tempProps.get("speed"));
125 
126         QString filename = GetFilenameFromProducer(producer(), false);
127         QString s = QString("%1:%2:%3").arg("timewarp").arg(warpspeed).arg(filename);
128         p = new Mlt::Producer(profile, s.toUtf8().constData());
129         p->set(kShotcutProducerProperty, "avformat");
130     }
131     if (p->is_valid()) {
132         p->set("video_delay", double(ui->syncSlider->value()) / 1000);
133         if (ui->pitchCheckBox->checkState() == Qt::Checked) {
134             m_producer->set("warp_pitch", 1);
135         }
136     }
137     return p;
138 }
139 
setProducer(Mlt::Producer * p)140 void AvformatProducerWidget::setProducer(Mlt::Producer* p)
141 {
142     AbstractProducerWidget::setProducer(p);
143     emit producerChanged(p);
144 }
145 
updateDuration()146 void AvformatProducerWidget::updateDuration()
147 {
148     if (m_producer->get(kFilterInProperty) && m_producer->get(kFilterOutProperty)) {
149         auto duration = m_producer->get_int(kFilterOutProperty) - m_producer->get_int(kFilterInProperty) + 1;
150         ui->timelineDurationLabel->show();
151         ui->timelineDurationText->setText(m_producer->frames_to_time(duration));
152         ui->timelineDurationText->show();
153     } else {
154         ui->timelineDurationLabel->hide();
155         ui->timelineDurationLabel->setText(QString());
156         ui->timelineDurationText->hide();
157     }
158 }
159 
rename()160 void AvformatProducerWidget::rename()
161 {
162     ui->filenameLabel->setFocus();
163     ui->filenameLabel->selectAll();
164 }
165 
keyPressEvent(QKeyEvent * event)166 void AvformatProducerWidget::keyPressEvent(QKeyEvent* event)
167 {
168     if (ui->speedSpinBox->hasFocus() &&
169             (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)) {
170         ui->speedSpinBox->clearFocus();
171     } else {
172         QWidget::keyPressEvent(event);
173     }
174 }
175 
onFrameDisplayed(const SharedFrame &)176 void AvformatProducerWidget::onFrameDisplayed(const SharedFrame&)
177 {
178     // This forces avformat-novalidate or unloaded avformat to load and get
179     // media information.
180     delete m_producer->get_frame();
181     onFrameDecoded();
182     // We can stop listening to this signal if this is audio-only or if we have
183     // received the video resolution.
184     if (m_producer->get_int("audio_index") == -1 || m_producer->get_int("meta.media.width") || m_producer->get_int("meta.media.height"))
185         disconnect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, 0);
186 }
187 
onProducerChanged()188 void AvformatProducerWidget::onProducerChanged()
189 {
190     QThreadPool::globalInstance()->start(new DecodeTask(this), 10);
191 }
192 
reopen(Mlt::Producer * p)193 void AvformatProducerWidget::reopen(Mlt::Producer* p)
194 {
195     int length = ui->durationSpinBox->value();
196     int out = m_producer->get_out();
197     int position = m_producer->position();
198     double speed = m_producer->get_speed();
199 
200     if( m_recalcDuration )
201     {
202         double oldSpeed = GetSpeedFromProducer(producer());
203         double newSpeed = ui->speedSpinBox->value();
204         double speedRatio = oldSpeed / newSpeed;
205         int in = m_producer->get_in();
206 
207         length = qRound(length * speedRatio);
208         in = qMin(qRound(in * speedRatio), length - 1);
209         out = qMin(qRound(out * speedRatio), length - 1);
210         p->set("length", p->frames_to_time(length, mlt_time_clock));
211         p->set_in_and_out(in, out);
212         position = qRound(position * speedRatio);
213 
214         // Adjust filters.
215         int n = p->filter_count();
216         for (int j = 0; j < n; j++) {
217             QScopedPointer<Mlt::Filter> filter(p->filter(j));
218             if (filter && filter->is_valid() && !filter->get_int("_loader")) {
219                 in = qMin(qRound(filter->get_in() * speedRatio), length - 1);
220                 out = qMin(qRound(filter->get_out() * speedRatio), length - 1);
221                 filter->set_in_and_out(in, out);
222                 //TODO: keyframes
223             }
224         }
225     }
226     else
227     {
228         p->set("length", p->frames_to_time(length, mlt_time_clock));
229         if (out + 1 >= m_producer->get_length())
230             p->set("out", length - 1);
231         else if (out >= length)
232             p->set("out", length - 1);
233         else
234             p->set("out", out);
235         if (position > p->get_out())
236             position = p->get_out();
237         p->set("in", m_producer->get_in());
238     }
239     if (MLT.setProducer(p)) {
240         AbstractProducerWidget::setProducer(0);
241         return;
242     }
243     MLT.stop();
244     emit producerReopened(false);
245     emit producerChanged(p);
246     MLT.seek(position);
247     MLT.play(speed);
248     setProducer(p);
249 }
250 
recreateProducer()251 void AvformatProducerWidget::recreateProducer()
252 {
253     Mlt::Producer* p = newProducer(MLT.profile());
254     p->pass_list(*m_producer, "audio_index, video_index, force_aspect_ratio,"
255                  "video_delay, force_progressive, force_tff, set.force_full_luma, color_range, warp_pitch,"
256                  kAspectRatioNumerator ","
257                  kAspectRatioDenominator ","
258                  kShotcutHashProperty ","
259                  kPlaylistIndexProperty ","
260                  kShotcutSkipConvertProperty ","
261                  kCommentProperty ","
262                  kDefaultAudioIndexProperty ","
263                  kShotcutCaptionProperty ","
264                  kOriginalResourceProperty ","
265                  kDisableProxyProperty ","
266                  kIsProxyProperty);
267     Mlt::Controller::copyFilters(*m_producer, *p);
268     if (m_producer->get(kMultitrackItemProperty)) {
269         emit producerChanged(p);
270         delete p;
271     } else {
272         reopen(p);
273     }
274 }
275 
onFrameDecoded()276 void AvformatProducerWidget::onFrameDecoded()
277 {
278     int tabIndex = ui->tabWidget->currentIndex();
279     ui->tabWidget->setTabEnabled(0, false);
280     ui->tabWidget->setTabEnabled(1, false);
281     ui->tabWidget->setTabEnabled(2, false);
282     if (m_defaultDuration == -1)
283         m_defaultDuration = m_producer->get_length();
284 
285     double warpSpeed = GetSpeedFromProducer(producer());
286     QString resource = GetFilenameFromProducer(producer());
287     QString name = Util::baseName(resource);
288     QString caption = m_producer->get(kShotcutCaptionProperty);
289     if (caption.isEmpty() || caption.startsWith(name)) {
290         // compute the caption
291         if (warpSpeed != 1.0)
292             caption = QString("%1 (%2x)").arg(name).arg(warpSpeed);
293         else
294             caption = name;
295         m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
296     }
297     ui->filenameLabel->setText(caption);
298     ui->filenameLabel->setCursorPosition(caption.length());
299     ui->filenameLabel->setToolTip(resource);
300     ui->notesTextEdit->setPlainText(QString::fromUtf8(m_producer->get(kCommentProperty)));
301     ui->durationSpinBox->setValue(m_producer->get_length());
302     updateDuration();
303     m_recalcDuration = false;
304     ui->speedSpinBox->setValue(warpSpeed);
305     if (warpSpeed == 1.0) {
306         ui->pitchCheckBox->setEnabled(false);
307     } else {
308         ui->pitchCheckBox->setEnabled(true);
309     }
310     if (m_producer->get_int("warp_pitch") == 1) {
311         ui->pitchCheckBox->setCheckState(Qt::Checked);
312     } else {
313         ui->pitchCheckBox->setCheckState(Qt::Unchecked);
314     }
315     ui->rangeComboBox->setEnabled(true);
316 
317     // populate the track combos
318     int n = m_producer->get_int("meta.media.nb_streams");
319     int videoIndex = 1;
320     int audioIndex = 1;
321     int totalAudioChannels = 0;
322     bool populateTrackCombos = (ui->videoTrackComboBox->count() == 0 &&
323                                 ui->audioTrackComboBox->count() == 0);
324     int color_range = !qstrcmp(m_producer->get("meta.media.color_range"), "full");
325 
326     for (int i = 0; i < n; i++) {
327         QString key = QString("meta.media.%1.stream.type").arg(i);
328         QString streamType(m_producer->get(key.toLatin1().constData()));
329         if (streamType == "video") {
330             key = QString("meta.media.%1.codec.name").arg(i);
331             QString codec(m_producer->get(key.toLatin1().constData()));
332             key = QString("meta.media.%1.codec.width").arg(i);
333             QString width(m_producer->get(key.toLatin1().constData()));
334             key = QString("meta.media.%1.codec.height").arg(i);
335             QString height(m_producer->get(key.toLatin1().constData()));
336             QString name = QString("%1: %2x%3 %4")
337                     .arg(videoIndex)
338                     .arg(width)
339                     .arg(height)
340                     .arg(codec);
341             if (populateTrackCombos) {
342                 if (ui->videoTrackComboBox->count() == 0)
343                     ui->videoTrackComboBox->addItem(tr("None"), -1);
344                 ui->videoTrackComboBox->addItem(name, i);
345             }
346             if (i == m_producer->get_int("video_index")) {
347                 key = QString("meta.media.%1.codec.long_name").arg(i);
348                 QString codec(m_producer->get(key.toLatin1().constData()));
349                 ui->videoTableWidget->setItem(0, 1, new QTableWidgetItem(codec));
350                 key = QString("meta.media.%1.codec.pix_fmt").arg(i);
351                 QString pix_fmt = QString::fromLatin1(m_producer->get(key.toLatin1().constData()));
352                 if (pix_fmt.startsWith("yuvj")) {
353                     color_range = 1;
354                 } else if (pix_fmt.contains("gbr") || pix_fmt.contains("rgb")) {
355                     color_range = 1;
356                     ui->rangeComboBox->setEnabled(false);
357                 }
358                 ui->videoTableWidget->setItem(3, 1, new QTableWidgetItem(pix_fmt));
359                 key = QString("meta.media.%1.codec.colorspace").arg(i);
360                 int colorspace = m_producer->get_int(key.toLatin1().constData());
361                 QString csString = tr("unknown (%1)").arg(colorspace);
362                 switch (colorspace) {
363                     case 240:
364                         csString = "SMPTE ST240";
365                         break;
366                     case 601:
367                         csString = "ITU-R BT.601";
368                         break;
369                     case 709:
370                         csString = "ITU-R BT.709";
371                         break;
372                     case 9:
373                     case 10:
374                         csString = "ITU-R BT.2020";
375                         break;
376                 }
377                 ui->videoTableWidget->setItem(4, 1, new QTableWidgetItem(csString));
378                 key = QString("meta.media.%1.codec.color_trc").arg(i);
379                 int trc = m_producer->get_int(key.toLatin1().constData());
380                 QString trcString = tr("unknown (%1)").arg(trc);
381                 switch (trc) {
382                     case 0: trcString = tr("NA"); break;
383                     case 1: trcString = "ITU-R BT.709"; break;
384                     case 6: trcString = "ITU-R BT.601"; break;
385                     case 7: trcString = "SMPTE ST240"; break;
386                     case 14: trcString = "ITU-R BT.2020"; break;
387                     case 15: trcString = "ITU-R BT.2020"; break;
388                     case 16: trcString = "SMPTE ST2084 (PQ)"; break;
389                     case 17: trcString = "SMPTE ST428"; break;
390                     case 18: trcString = "ARIB B67 (HLG)"; break;
391                 }
392                 QTableWidgetItem* trcItem = new QTableWidgetItem(trcString);
393                 trcItem->setData(Qt::UserRole, QVariant(trc));
394                 ui->videoTableWidget->setItem(5, 1, trcItem);
395                 ui->videoTrackComboBox->setCurrentIndex(videoIndex);
396             }
397             ui->tabWidget->setTabEnabled(0, true);
398             videoIndex++;
399         }
400         else if (streamType == "audio") {
401             key = QString("meta.media.%1.codec.name").arg(i);
402             QString codec(m_producer->get(key.toLatin1().constData()));
403             key = QString("meta.media.%1.codec.channels").arg(i);
404             int channels(m_producer->get_int(key.toLatin1().constData()));
405             totalAudioChannels += channels;
406             key = QString("meta.media.%1.codec.sample_rate").arg(i);
407             QString sampleRate(m_producer->get(key.toLatin1().constData()));
408             QString name = QString("%1: %2 ch %3 KHz %4")
409                     .arg(audioIndex)
410                     .arg(channels)
411                     .arg(sampleRate.toDouble()/1000)
412                     .arg(codec);
413             if (populateTrackCombos) {
414                 if (ui->audioTrackComboBox->count() == 0)
415                     ui->audioTrackComboBox->addItem(tr("None"), -1);
416                 ui->audioTrackComboBox->addItem(name, i);
417             }
418             if ( QString::number(i) == m_producer->get("audio_index")) {
419                 key = QString("meta.media.%1.codec.long_name").arg(i);
420                 QString codec(m_producer->get(key.toLatin1().constData()));
421                 ui->audioTableWidget->setItem(0, 1, new QTableWidgetItem(codec));
422                 const char* layout = mlt_channel_layout_name(mlt_channel_layout_default(channels));
423                 QString channelsStr = QString("%1 (%2)").arg(channels).arg(layout);
424                 ui->audioTableWidget->setItem(1, 1, new QTableWidgetItem(channelsStr));
425                 ui->audioTableWidget->setItem(2, 1, new QTableWidgetItem(sampleRate));
426                 key = QString("meta.media.%1.codec.sample_fmt").arg(i);
427                 ui->audioTableWidget->setItem(3, 1, new QTableWidgetItem(
428                     m_producer->get(key.toLatin1().constData())));
429                 ui->audioTrackComboBox->setCurrentIndex(audioIndex);
430             }
431             ui->tabWidget->setTabEnabled(1, true);
432             audioIndex++;
433         }
434     }
435     if (populateTrackCombos && ui->audioTrackComboBox->count() > 2)
436         ui->audioTrackComboBox->addItem(tr("All"), "all");
437 
438     if (m_producer->get("audio_index") == QString("-1")) {
439         ui->audioTrackComboBox->setCurrentIndex(0);
440         ui->audioTableWidget->setItem(0, 1, new QTableWidgetItem(""));
441         ui->audioTableWidget->setItem(1, 1, new QTableWidgetItem("0"));
442         ui->audioTableWidget->setItem(2, 1, new QTableWidgetItem(""));
443         ui->audioTableWidget->setItem(3, 1, new QTableWidgetItem(""));
444     }
445     else if (m_producer->get("audio_index") == QString("all")) {
446         ui->audioTrackComboBox->setCurrentIndex(ui->audioTrackComboBox->count()-1);
447         ui->audioTableWidget->setItem(0, 1, new QTableWidgetItem(""));
448         ui->audioTableWidget->setItem(1, 1, new QTableWidgetItem(QString::number(totalAudioChannels)));
449         ui->audioTableWidget->setItem(2, 1, new QTableWidgetItem(""));
450         ui->audioTableWidget->setItem(3, 1, new QTableWidgetItem(""));
451     }
452     if (m_producer->get("video_index") == QString("-1")) {
453         ui->videoTrackComboBox->setCurrentIndex(0);
454         ui->videoTableWidget->setItem(0, 1, new QTableWidgetItem(""));
455         ui->videoTableWidget->setItem(1, 1, new QTableWidgetItem(""));
456         ui->videoTableWidget->setItem(2, 1, new QTableWidgetItem(""));
457         ui->videoTableWidget->setItem(3, 1, new QTableWidgetItem(""));
458         ui->videoTableWidget->setItem(4, 1, new QTableWidgetItem(""));
459         ui->videoTableWidget->setItem(5, 1, new QTableWidgetItem(""));
460         ui->proxyButton->hide();
461     }
462 
463     // Restore the previous tab, or select the first enabled tab.
464     if (ui->tabWidget->isTabEnabled(tabIndex))
465         ui->tabWidget->setCurrentIndex(tabIndex);
466     else if (ui->tabWidget->isTabEnabled(0))
467         ui->tabWidget->setCurrentIndex(0);
468     else if (ui->tabWidget->isTabEnabled(1))
469         ui->tabWidget->setCurrentIndex(1);
470 
471     int width = m_producer->get_int("meta.media.width");
472     int height = m_producer->get_int("meta.media.height");
473     if (width || height) {
474         bool isProxy = m_producer->get_int(kIsProxyProperty) && m_producer->get(kOriginalResourceProperty);
475         ui->videoTableWidget->setItem(1, 1, new QTableWidgetItem(QString("%1x%2 %3").arg(width).arg(height)
476                                       .arg(isProxy? tr("(PROXY)") : "")));
477     }
478 
479     double sar = m_producer->get_double("meta.media.sample_aspect_num");
480     if (m_producer->get_double("meta.media.sample_aspect_den") > 0)
481         sar /= m_producer->get_double("meta.media.sample_aspect_den");
482     if (m_producer->get("force_aspect_ratio"))
483         sar = m_producer->get_double("force_aspect_ratio");
484     int dar_numerator = width * sar;
485     int dar_denominator = height;
486     if (height > 0) {
487         switch (int(sar * width / height * 100)) {
488         case 133:
489             dar_numerator = 4;
490             dar_denominator = 3;
491             break;
492         case 177:
493             dar_numerator = 16;
494             dar_denominator = 9;
495             break;
496         case 56:
497             dar_numerator = 9;
498             dar_denominator = 16;
499         }
500     }
501     if (m_producer->get(kAspectRatioNumerator))
502         dar_numerator = m_producer->get_int(kAspectRatioNumerator);
503     if (m_producer->get(kAspectRatioDenominator))
504         dar_denominator = m_producer->get_int(kAspectRatioDenominator);
505     ui->aspectNumSpinBox->blockSignals(true);
506     ui->aspectNumSpinBox->setValue(dar_numerator);
507     ui->aspectNumSpinBox->blockSignals(false);
508     ui->aspectDenSpinBox->blockSignals(true);
509     ui->aspectDenSpinBox->setValue(dar_denominator);
510     ui->aspectDenSpinBox->blockSignals(false);
511 
512     double fps = m_producer->get_double("meta.media.frame_rate_num");
513     if (m_producer->get_double("meta.media.frame_rate_den") > 0)
514         fps /= m_producer->get_double("meta.media.frame_rate_den");
515     if (m_producer->get("force_fps"))
516         fps = m_producer->get_double("fps");
517     bool isVariableFrameRate = m_producer->get_int("meta.media.variable_frame_rate");
518     if (fps != 0.0 ) {
519         ui->videoTableWidget->setItem(2, 1, new QTableWidgetItem(QString("%L1 %2").arg(fps, 0, 'f', 6)
520                                       .arg(isVariableFrameRate? tr("(variable)") : "")));
521     }
522 
523     int progressive = m_producer->get_int("meta.media.progressive");
524     if (m_producer->get("force_progressive"))
525         progressive = m_producer->get_int("force_progressive");
526     ui->scanComboBox->setCurrentIndex(progressive);
527 
528     int tff = m_producer->get_int("meta.media.top_field_first");
529     if (m_producer->get("force_tff"))
530         tff = m_producer->get_int("force_tff");
531     ui->fieldOrderComboBox->setCurrentIndex(tff);
532     ui->fieldOrderComboBox->setEnabled(!progressive);
533     if (m_producer->get("color_range"))
534         color_range = m_producer->get_int("color_range") == 2;
535     else if (m_producer->get("set.force_full_luma"))
536         color_range = m_producer->get_int("set.force_full_luma");
537     ui->rangeComboBox->setCurrentIndex(color_range);
538 
539     if (populateTrackCombos) {
540         for (int i = 0; i < m_producer->count(); i++) {
541             QString name(m_producer->get_name(i));
542             if (name.startsWith("meta.attr.") && name.endsWith(".markup")) {
543                 int row = ui->metadataTable->rowCount();
544                 ui->metadataTable->setRowCount(row + 1);
545                 ui->metadataTable->setItem(row, 0, new QTableWidgetItem(name.section('.', -2, -2)));
546                 ui->metadataTable->setItem(row, 1, new QTableWidgetItem(m_producer->get(i)));
547                 ui->tabWidget->setTabEnabled(2, true);
548             }
549         }
550     }
551     ui->syncSlider->setValue(qRound(m_producer->get_double("video_delay") * 1000.0));
552 
553     if (Settings.showConvertClipDialog() && !m_producer->get_int(kShotcutSkipConvertProperty)) {
554         auto transferItem = ui->videoTableWidget->item(5, 1);
555         if (transferItem && transferItem->data(Qt::UserRole).toInt() > 7) {
556             // Transfer characteristics > SMPTE240M Probably need conversion
557             QString trcString = ui->videoTableWidget->item(5, 1)->text();
558             m_producer->set(kShotcutSkipConvertProperty, true);
559             LongUiTask::cancel();
560             MLT.pause();
561             LOG_INFO() << resource << "Probable HDR" << ui->videoTableWidget->item(5, 1)->text();
562             TranscodeDialog dialog(tr("This file uses color transfer characteristics %1, which may result in incorrect colors or brightness in Shotcut. "
563                                       "Do you want to convert it to an edit-friendly format?\n\n"
564                                       "If yes, choose a format below and then click OK to choose a file name. "
565                                       "After choosing a file name, a job is created. "
566                                       "When it is done, double-click the job to open it.\n").arg(trcString),
567                                       ui->scanComboBox->currentIndex(), this);
568             dialog.set709Convert(true);
569             dialog.setWindowModality(QmlApplication::dialogModality());
570             dialog.showCheckBox();
571             convert(dialog);
572         } else if (isVariableFrameRate) {
573             m_producer->set(kShotcutSkipConvertProperty, true);
574             LongUiTask::cancel();
575             MLT.pause();
576             LOG_INFO() << resource << "is variable frame rate";
577             TranscodeDialog dialog(tr("This file is variable frame rate, which is not reliable for editing. "
578                                       "Do you want to convert it to an edit-friendly format?\n\n"
579                                       "If yes, choose a format below and then click OK to choose a file name. "
580                                       "After choosing a file name, a job is created. "
581                                       "When it is done, double-click the job to open it.\n"),
582                                    ui->scanComboBox->currentIndex(), this);
583             dialog.setWindowModality(QmlApplication::dialogModality());
584             dialog.showCheckBox();
585             convert(dialog);
586         } else if (QFile::exists(resource) && !MLT.isSeekable(m_producer.data())) {
587             m_producer->set(kShotcutSkipConvertProperty, true);
588             LongUiTask::cancel();
589             MLT.pause();
590             LOG_INFO() << resource << "is not seekable";
591             TranscodeDialog dialog(tr("This file does not support seeking and cannot be used for editing. "
592                                       "Do you want to convert it to an edit-friendly format?\n\n"
593                                       "If yes, choose a format below and then click OK to choose a file name. "
594                                       "After choosing a file name, a job is created. "
595                                       "When it is done, double-click the job to open it.\n"),
596                                    ui->scanComboBox->currentIndex(), this);
597             dialog.setWindowModality(QmlApplication::dialogModality());
598             dialog.showCheckBox();
599             convert(dialog);
600         }
601     }
602 }
603 
on_videoTrackComboBox_activated(int index)604 void AvformatProducerWidget::on_videoTrackComboBox_activated(int index)
605 {
606     if (m_producer) {
607         m_producer->set("video_index", ui->videoTrackComboBox->itemData(index).toInt());
608         recreateProducer();
609     }
610 }
611 
on_audioTrackComboBox_activated(int index)612 void AvformatProducerWidget::on_audioTrackComboBox_activated(int index)
613 {
614     if (m_producer) {
615         // Save the default audio index for AudioLevelsTask.
616         if (!m_producer->get(kDefaultAudioIndexProperty)) {
617             m_producer->set(kDefaultAudioIndexProperty, m_producer->get_int("audio_index"));
618         }
619         m_producer->set("audio_index", ui->audioTrackComboBox->itemData(index).toString().toUtf8().constData());
620         recreateProducer();
621     }
622 }
623 
on_scanComboBox_activated(int index)624 void AvformatProducerWidget::on_scanComboBox_activated(int index)
625 {
626     if (m_producer) {
627         int progressive = m_producer->get_int("meta.media.progressive");
628         ui->fieldOrderComboBox->setEnabled(!progressive);
629         if (m_producer->get("force_progressive") || progressive != index)
630             // We need to set these force_ properties as a string so they can be properly removed
631             // by setting them NULL.
632             m_producer->set("force_progressive", QString::number(index).toLatin1().constData());
633         emit producerChanged(producer());
634         if (Settings.playerGPU())
635             connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, SLOT(onFrameDisplayed(const SharedFrame&)));
636     }
637 }
638 
on_fieldOrderComboBox_activated(int index)639 void AvformatProducerWidget::on_fieldOrderComboBox_activated(int index)
640 {
641     if (m_producer) {
642         int tff = m_producer->get_int("meta.media.top_field_first");
643         if (m_producer->get("force_tff") || tff != index)
644             m_producer->set("force_tff", QString::number(index).toLatin1().constData());
645         emit producerChanged(producer());
646         if (Settings.playerGPU())
647             connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, SLOT(onFrameDisplayed(const SharedFrame&)));
648     }
649 }
650 
on_aspectNumSpinBox_valueChanged(int)651 void AvformatProducerWidget::on_aspectNumSpinBox_valueChanged(int)
652 {
653     if (m_producer) {
654         double new_sar = double(ui->aspectNumSpinBox->value() * m_producer->get_int("meta.media.height")) /
655             double(ui->aspectDenSpinBox->value() * m_producer->get_int("meta.media.width"));
656         double sar = m_producer->get_double("meta.media.sample_aspect_num");
657         if (m_producer->get_double("meta.media.sample_aspect_den") > 0)
658             sar /= m_producer->get_double("meta.media.sample_aspect_den");
659         if (m_producer->get("force_aspect_ratio") || new_sar != sar) {
660             m_producer->set("force_aspect_ratio", QString::number(new_sar).toLatin1().constData());
661             m_producer->set(kAspectRatioNumerator, ui->aspectNumSpinBox->text().toLatin1().constData());
662             m_producer->set(kAspectRatioDenominator, ui->aspectDenSpinBox->text().toLatin1().constData());
663         }
664         emit producerChanged(producer());
665         if (Settings.playerGPU())
666             connect(MLT.videoWidget(), SIGNAL(frameDisplayed(const SharedFrame&)), this, SLOT(onFrameDisplayed(const SharedFrame&)));
667     }
668 }
669 
on_aspectDenSpinBox_valueChanged(int i)670 void AvformatProducerWidget::on_aspectDenSpinBox_valueChanged(int i)
671 {
672     on_aspectNumSpinBox_valueChanged(i);
673 }
674 
on_durationSpinBox_editingFinished()675 void AvformatProducerWidget::on_durationSpinBox_editingFinished()
676 {
677     if (!m_producer)
678         return;
679     if (ui->durationSpinBox->value() == m_producer->get_length())
680         return;
681     recreateProducer();
682 }
683 
684 
on_speedSpinBox_editingFinished()685 void AvformatProducerWidget::on_speedSpinBox_editingFinished()
686 {
687     if (!m_producer)
688         return;
689     if (ui->speedSpinBox->value() == GetSpeedFromProducer(producer()))
690         return;
691     if (ui->speedSpinBox->value() == 1.0) {
692         ui->pitchCheckBox->setEnabled(false);
693     } else {
694         ui->pitchCheckBox->setEnabled(true);
695     }
696     m_recalcDuration = true;
697     recreateProducer();
698 }
699 
on_pitchCheckBox_stateChanged(int state)700 void AvformatProducerWidget::on_pitchCheckBox_stateChanged(int state)
701 {
702     if (!m_producer)
703         return;
704     if (state == Qt::Unchecked) {
705         m_producer->set("warp_pitch", 0);
706     } else {
707         m_producer->set("warp_pitch", 1);
708     }
709     emit modified();
710 }
711 
on_syncSlider_valueChanged(int value)712 void AvformatProducerWidget::on_syncSlider_valueChanged(int value)
713 {
714     double delay = double(value) / 1000.0;
715     if (m_producer && m_producer->get_double("video_delay") != delay) {
716         m_producer->set("video_delay", delay);
717         emit modified();
718     }
719 }
720 
on_actionOpenFolder_triggered()721 void AvformatProducerWidget::on_actionOpenFolder_triggered()
722 {
723     Util::showInFolder(GetFilenameFromProducer(producer()));
724 }
725 
on_menuButton_clicked()726 void AvformatProducerWidget::on_menuButton_clicked()
727 {
728     QMenu menu;
729     menu.addAction(ui->actionReset);
730     if (!MLT.resource().contains("://")) // not a network stream
731         menu.addAction(ui->actionOpenFolder);
732     menu.addAction(ui->actionCopyFullFilePath);
733     menu.addAction(ui->actionFFmpegInfo);
734     menu.addAction(ui->actionFFmpegIntegrityCheck);
735     menu.addAction(ui->actionFFmpegConvert);
736     menu.addAction(ui->actionExtractSubclip);
737     menu.addAction(ui->actionSetFileDate);
738     if (GetFilenameFromProducer(producer()).toLower().endsWith(".mp4")) {
739         menu.addAction(ui->actionSetEquirectangular);
740     }
741     menu.exec(ui->menuButton->mapToGlobal(QPoint(0, 0)));
742 }
743 
on_actionCopyFullFilePath_triggered()744 void AvformatProducerWidget::on_actionCopyFullFilePath_triggered()
745 {
746     qApp->clipboard()->setText(GetFilenameFromProducer(producer()));
747 }
748 
on_notesTextEdit_textChanged()749 void AvformatProducerWidget::on_notesTextEdit_textChanged()
750 {
751     QString existing = QString::fromUtf8(m_producer->get(kCommentProperty));
752     if (ui->notesTextEdit->toPlainText() != existing) {
753         m_producer->set(kCommentProperty, ui->notesTextEdit->toPlainText().toUtf8().constData());
754         emit modified();
755     }
756 }
757 
on_actionFFmpegInfo_triggered()758 void AvformatProducerWidget::on_actionFFmpegInfo_triggered()
759 {
760     QStringList args;
761     args << "-v" << "quiet";
762     args << "-print_format" << "ini";
763     args << "-pretty";
764     args << "-show_format" << "-show_programs" << "-show_streams" << "-find_stream_info";
765     args << GetFilenameFromProducer(producer());
766     AbstractJob* job = new FfprobeJob(args.last(), args);
767     job->start();
768 }
769 
on_actionFFmpegIntegrityCheck_triggered()770 void AvformatProducerWidget::on_actionFFmpegIntegrityCheck_triggered()
771 {
772     QString resource = GetFilenameFromProducer(producer());
773     QStringList args;
774     args << "-xerror";
775     args << "-err_detect" << "+explode";
776     args << "-v" << "info";
777     args << "-i" << resource;
778     args << "-map" << "0";
779     args << "-f" << "null" << "pipe:";
780     JOBS.add(new FfmpegJob(resource, args));
781 }
782 
on_actionFFmpegConvert_triggered()783 void AvformatProducerWidget::on_actionFFmpegConvert_triggered()
784 {
785     TranscodeDialog dialog(tr("Choose an edit-friendly format below and then click OK to choose a file name. "
786                               "After choosing a file name, a job is created. "
787                               "When it is done, double-click the job to open it.\n"),
788                            ui->scanComboBox->currentIndex(), this);
789     dialog.setWindowModality(QmlApplication::dialogModality());
790     dialog.set709Convert(ui->videoTableWidget->item(5, 1)->data(Qt::UserRole).toInt() > 7);
791     convert(dialog);
792 }
793 
convert(TranscodeDialog & dialog)794 void AvformatProducerWidget::convert(TranscodeDialog& dialog)
795 {
796     int result = dialog.exec();
797     if (dialog.isCheckBoxChecked()) {
798         Settings.setShowConvertClipDialog(false);
799     }
800     if (result == QDialog::Accepted) {
801         QString resource = GetFilenameFromProducer(producer());
802         QString path = Settings.savePath();
803         QStringList args;
804         QString nameFilter;
805 
806         args << "-loglevel" << "verbose";
807         args << "-i" << resource;
808         args << "-max_muxing_queue_size" << "9999";
809         // transcode all streams except data, subtitles, and attachments
810         auto audioIndex = m_producer->property_exists(kDefaultAudioIndexProperty)? m_producer->get_int(kDefaultAudioIndexProperty) : m_producer->get_int("audio_index");
811         if (m_producer->get_int("video_index") < audioIndex) {
812             args << "-map" << "0:V?" << "-map" << "0:a?";
813         } else {
814             args << "-map" << "0:a?" << "-map" << "0:V?";
815         }
816         args << "-map_metadata" << "0" << "-ignore_unknown";
817 
818         // Set video filters
819         args << "-vf";
820         QString filterString;
821         if (dialog.deinterlace()) {
822             QString deinterlaceFilter = QString("bwdif,");
823             filterString = filterString + deinterlaceFilter;
824         }
825         QString range;
826         if (ui->rangeComboBox->currentIndex())
827             range = "full";
828         else
829             range = "mpeg";
830         if (dialog.get709Convert()) {
831             QString convertFilter = QString("zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv422p,");
832             filterString = filterString + convertFilter;
833         }
834         filterString = filterString + QString("scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=%1:out_range=%2").arg(range).arg(range);
835         if (dialog.fpsOverride()) {
836             QString minterpFilter = QString(",minterpolate='mi_mode=%1:mc_mode=aobmc:me_mode=bidir:vsbmc=1:fps=%2'").arg(dialog.frc()).arg(dialog.fps());
837             filterString = filterString + minterpFilter;
838         }
839         args << filterString;
840 
841         // Specify color range
842         if (ui->rangeComboBox->currentIndex())
843             args << "-color_range" << "jpeg";
844         else
845             args << "-color_range" << "mpeg";
846 
847         if (!dialog.deinterlace() && !ui->scanComboBox->currentIndex())
848             args << "-flags" << "+ildct+ilme" << "-top" << QString::number(ui->fieldOrderComboBox->currentIndex());
849 
850         switch (dialog.format()) {
851         case 0:
852             path.append("/%1 - %2.mp4");
853             nameFilter = tr("MP4 (*.mp4);;All Files (*)");
854             args << "-f" << "mp4" << "-codec:a" << "ac3" << "-b:a" << "512k" << "-codec:v" << "libx264";
855             args << "-preset" << "medium" << "-g" << "1" << "-crf" << "15";
856             break;
857         case 1:
858             args << "-f" << "mov" << "-codec:a" << "alac";
859             if (dialog.deinterlace() || ui->scanComboBox->currentIndex()) { // progressive
860                 args << "-codec:v" << "dnxhd" << "-profile:v" << "dnxhr_hq" << "-pix_fmt" << "yuv422p";
861             } else { // interlaced
862                 args << "-codec:v" << "prores_ks" << "-profile:v" << "standard";
863             }
864             path.append("/%1 - %2.mov");
865             nameFilter = tr("MOV (*.mov);;All Files (*)");
866             break;
867         case 2:
868             args << "-f" << "matroska" << "-codec:a" << "pcm_f32le" << "-codec:v" << "utvideo";
869             args << "-pix_fmt" << "yuv422p";
870             path.append("/%1 - %2.mkv");
871             nameFilter = tr("MKV (*.mkv);;All Files (*)");
872             break;
873         }
874         if (dialog.get709Convert()) {
875             args << "-colorspace" << "bt709" << "-color_primaries" << "bt709" << "-color_trc" << "bt709";
876         }
877         QFileInfo fi(resource);
878         path = path.arg(fi.completeBaseName()).arg(tr("Converted"));
879         QString filename = QFileDialog::getSaveFileName(this, dialog.windowTitle(), path, nameFilter,
880             nullptr, Util::getFileDialogOptions());
881         if (!filename.isEmpty()) {
882             if (filename == QDir::toNativeSeparators(resource)) {
883                 QMessageBox::warning(this, dialog.windowTitle(),
884                                      QObject::tr("Unable to write file %1\n"
885                                         "Perhaps you do not have permission.\n"
886                                         "Try again with a different folder.")
887                                      .arg(fi.fileName()));
888                 return;
889             }
890             if (Util::warnIfNotWritable(filename, this, dialog.windowTitle()))
891                 return;
892 
893             Settings.setSavePath(QFileInfo(filename).path());
894             args << "-y" << filename;
895             m_producer->Mlt::Properties::clear(kOriginalResourceProperty);
896 
897             FfmpegJob* job = new FfmpegJob(filename, args, false);
898             job->setLabel(tr("Convert %1").arg(Util::baseName(filename)));
899             job->setPostJobAction(new ConvertReplacePostJobAction(resource, filename, Util::getHash(*m_producer)));
900             JOBS.add(job);
901         }
902     }
903 }
904 
revertToOriginalResource()905 bool AvformatProducerWidget::revertToOriginalResource()
906 {
907     QString resource = m_producer->get(kOriginalResourceProperty);
908     if (!resource.isEmpty() && !m_producer->get_int(kIsProxyProperty)) {
909         m_producer->Mlt::Properties::clear(kOriginalResourceProperty);
910         if (m_producer->get(kMultitrackItemProperty)) {
911             QString s = QString::fromLatin1(m_producer->get(kMultitrackItemProperty));
912             QVector<QStringRef> parts = s.splitRef(':');
913             if (parts.length() == 2) {
914                 int clipIndex = parts[0].toInt();
915                 int trackIndex = parts[1].toInt();
916                 QUuid uuid = MAIN.timelineClipUuid(trackIndex, clipIndex);
917                 if (!uuid.isNull()) {
918                     Mlt::Producer producer(MLT.profile(), resource.toUtf8().constData());
919                     if (producer.is_valid()) {
920                         if (!qstrcmp(producer.get("mlt_service"), "avformat")) {
921                             producer.set("mlt_service", "avformat-novalidate");
922                             producer.set("mute_on_pause", 0);
923                         }
924                         MLT.lockCreationTime(&producer);
925                         producer.set_in_and_out(m_producer->get_int(kOriginalInProperty), m_producer->get_int(kOriginalOutProperty));
926                         MAIN.replaceInTimeline(uuid, producer);
927                         return true;
928                     }
929                 }
930             }
931         } else {
932             MAIN.open(resource);
933             return true;
934         }
935     }
936     return false;
937 }
938 
on_reverseButton_clicked()939 void AvformatProducerWidget::on_reverseButton_clicked()
940 {
941     if (revertToOriginalResource())
942         return;
943 
944     TranscodeDialog dialog(tr("Choose an edit-friendly format below and then click OK to choose a file name. "
945                               "After choosing a file name, a job is created. "
946                               "When it is done, double-click the job to open it.\n"),
947                            ui->scanComboBox->currentIndex(), this);
948     dialog.setWindowTitle(tr("Reverse..."));
949     dialog.setWindowModality(QmlApplication::dialogModality());
950     int result = dialog.exec();
951     if (dialog.isCheckBoxChecked()) {
952         Settings.setShowConvertClipDialog(false);
953     }
954     if (result == QDialog::Accepted) {
955         QString resource = GetFilenameFromProducer(producer());
956         QString path = Settings.savePath();
957         QStringList meltArgs;
958         QStringList ffmpegArgs;
959         QString nameFilter;
960         QString ffmpegSuffix = "mov";
961         int in = -1;
962 
963         if (Settings.proxyEnabled()) {
964             m_producer->Mlt::Properties::clear(kOriginalResourceProperty);
965         } else {
966             // Save these properties for revertToOriginalResource()
967             m_producer->set(kOriginalResourceProperty, resource.toUtf8().constData());
968             m_producer->set(kOriginalInProperty, m_producer->get(kFilterInProperty)?
969                 m_producer->get_time(kFilterInProperty, mlt_time_clock) : m_producer->get_time("in", mlt_time_clock));
970             m_producer->set(kOriginalOutProperty, m_producer->get(kFilterOutProperty)?
971                 m_producer->get_time(kFilterOutProperty, mlt_time_clock) : m_producer->get_time("out", mlt_time_clock));
972         }
973 
974         ffmpegArgs << "-loglevel" << "verbose";
975         ffmpegArgs << "-i" << resource;
976         ffmpegArgs << "-max_muxing_queue_size" << "9999";
977         // set trim options
978         if (m_producer->get(kFilterInProperty)) {
979             in = m_producer->get_int(kFilterInProperty);
980             int ss = qMax(0, in - qRound(m_producer->get_fps() * 15.0));
981             auto s = QString::fromLatin1(m_producer->frames_to_time(ss, mlt_time_clock));
982             ffmpegArgs << "-ss" << s.replace(',', '.');
983         } else {
984             ffmpegArgs << "-ss" << QString::fromLatin1(m_producer->get_time("in", mlt_time_clock)).replace(',', '.').replace(',', '.');
985         }
986         if (m_producer->get(kFilterOutProperty)) {
987             int out = m_producer->get_int(kFilterOutProperty);
988             int to = qMin(m_producer->get_playtime() - 1, out + qRound(m_producer->get_fps() * 15.0));
989             in = to - out - 1;
990             auto s = QString::fromLatin1(m_producer->frames_to_time(to, mlt_time_clock));
991             ffmpegArgs << "-to" << s.replace(',', '.');
992         } else {
993             ffmpegArgs << "-to" << QString::fromLatin1(m_producer->get_time("out", mlt_time_clock)).replace(',', '.');
994         }
995         // transcode all streams except data, subtitles, and attachments
996         ffmpegArgs << "-map" << "0:V?" << "-map" << "0:a?" << "-map_metadata" << "0" << "-ignore_unknown";
997         if (ui->rangeComboBox->currentIndex())
998             ffmpegArgs << "-vf" << "scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=full:out_range=full" << "-color_range" << "jpeg";
999         else
1000             ffmpegArgs << "-vf" << "scale=flags=accurate_rnd+full_chroma_inp+full_chroma_int:in_range=mpeg:out_range=mpeg" << "-color_range" << "mpeg";
1001         if (!ui->scanComboBox->currentIndex())
1002             ffmpegArgs << "-flags" << "+ildct+ilme" << "-top" << QString::number(ui->fieldOrderComboBox->currentIndex());
1003 
1004         meltArgs << "-consumer" << "avformat";
1005         if (m_producer->get_int("audio_index") == -1) {
1006             meltArgs << "an=1" << "audio_off=1";
1007         } else if (qstrcmp(m_producer->get("audio_index"), "all")) {
1008             int index = m_producer->get_int("audio_index");
1009             QString key = QString("meta.media.%1.codec.channels").arg(index);
1010             const char* channels = m_producer->get(key.toLatin1().constData());
1011             meltArgs << QString("channels=").append(channels);
1012         }
1013         if (m_producer->get_int("video_index") == -1)
1014             meltArgs << "vn=1" << "video_off=1";
1015 
1016         switch (dialog.format()) {
1017         case 0:
1018             path.append("/%1 - %2.mp4");
1019             nameFilter = tr("MP4 (*.mp4);;All Files (*)");
1020             ffmpegArgs << "-f" << "mov" << "-codec:a" << "alac";
1021             if (ui->scanComboBox->currentIndex()) { // progressive
1022                 ffmpegArgs << "-codec:v" << "dnxhd" << "-profile:v" << "dnxhr_hq" << "-pix_fmt" << "yuv422p";
1023             } else { // interlaced
1024                 ffmpegArgs << "-codec:v" << "prores_ks" << "-profile:v" << "standard";
1025                 meltArgs << "top_field_first=" + QString::number(ui->fieldOrderComboBox->currentIndex());
1026             }
1027             meltArgs << "acodec=ac3" << "ab=512k" << "vcodec=libx264";
1028             meltArgs << "vpreset=medium" << "g=1" << "crf=11";
1029             break;
1030         case 1:
1031             ffmpegArgs << "-f" << "mov" << "-codec:a" << "alac";
1032             meltArgs << "acodec=alac";
1033             if (ui->scanComboBox->currentIndex()) { // progressive
1034                 ffmpegArgs << "-codec:v" << "dnxhd" << "-profile:v" << "dnxhr_hq" << "-pix_fmt" << "yuv422p";
1035                 meltArgs << "vcodec=dnxhd" << "vprofile=dnxhr_hq";
1036             } else { // interlaced
1037                 ffmpegArgs << "-codec:v" << "prores_ks" << "-profile:v" << "standard";
1038                 meltArgs << "top_field_first=" + QString::number(ui->fieldOrderComboBox->currentIndex());
1039                 meltArgs << "vcodec=prores_ks" << "vprofile=standard";
1040             }
1041             path.append("/%1 - %2.mov");
1042             nameFilter = tr("MOV (*.mov);;All Files (*)");
1043             break;
1044         case 2:
1045             ffmpegSuffix = "mkv";
1046             ffmpegArgs << "-f" << "matroska" << "-codec:a" << "pcm_s32le" << "-codec:v" << "utvideo";
1047             ffmpegArgs << "-pix_fmt" << "yuv422p";
1048             if (!ui->scanComboBox->currentIndex()) { // interlaced
1049                 meltArgs << "field_order=" + QString::fromLatin1(ui->fieldOrderComboBox->currentIndex()? "tt" : "bb");
1050             }
1051             meltArgs << "acodec=pcm_f32le" << "vcodec=utvideo" << "mlt_audio_format=f32le" << "pix_fmt=yuv422p";
1052             path.append("/%1 - %2.mkv");
1053             nameFilter = tr("MKV (*.mkv);;All Files (*)");
1054             break;
1055         }
1056         QFileInfo fi(resource);
1057         path = path.arg(fi.completeBaseName()).arg(tr("Reversed"));
1058         QString filename = QmlApplication::getNextProjectFile(path);
1059         if (filename.isEmpty()) {
1060             filename = QFileDialog::getSaveFileName(this, dialog.windowTitle(), path, nameFilter,
1061                 nullptr, Util::getFileDialogOptions());
1062         }
1063         if (!filename.isEmpty()) {
1064             if (filename == QDir::toNativeSeparators(resource)) {
1065                 QMessageBox::warning(this, dialog.windowTitle(),
1066                                      QObject::tr("Unable to write file %1\n"
1067                                         "Perhaps you do not have permission.\n"
1068                                         "Try again with a different folder.")
1069                                      .arg(fi.fileName()));
1070                 return;
1071             }
1072             if (Util::warnIfNotWritable(filename, this, dialog.windowTitle()))
1073                 return;
1074 
1075             Settings.setSavePath(QFileInfo(filename).path());
1076 
1077             // Make a temporary file name for the ffmpeg job.
1078             QFileInfo fi(filename);
1079             QString tmpFileName = QString("%1/%2 - XXXXXX.%3").arg(fi.path()).arg(fi.completeBaseName()).arg(ffmpegSuffix);
1080             QTemporaryFile tmp(tmpFileName);
1081             tmp.setAutoRemove(false);
1082             tmp.open();
1083             tmp.close();
1084             tmpFileName = tmp.fileName();
1085 
1086             // Run the ffmpeg job to convert a portion of the file to something edit-friendly.
1087             ffmpegArgs << "-y" << tmpFileName;
1088             FfmpegJob* ffmpegJob = new FfmpegJob(filename, ffmpegArgs, false);
1089             ffmpegJob->setLabel(tr("Convert %1").arg(Util::baseName(resource)));
1090             JOBS.add(ffmpegJob);
1091 
1092             // Run the melt job to convert the intermediate file to the reversed clip.
1093             meltArgs.prepend(QString("timewarp:-1.0:").append(tmpFileName));
1094             meltArgs << QString("target=").append(filename);
1095             MeltJob* meltJob = new MeltJob(filename, meltArgs,
1096                 m_producer->get_int("meta.media.frame_rate_num"), m_producer->get_int("meta.media.frame_rate_den"));
1097             meltJob->setLabel(tr("Reverse %1").arg(Util::baseName(resource)));
1098 
1099             if (m_producer->get(kMultitrackItemProperty)) {
1100                 QString s = QString::fromLatin1(m_producer->get(kMultitrackItemProperty));
1101                 QVector<QStringRef> parts = s.splitRef(':');
1102                 if (parts.length() == 2) {
1103                     int clipIndex = parts[0].toInt();
1104                     int trackIndex = parts[1].toInt();
1105                     QUuid uuid = MAIN.timelineClipUuid(trackIndex, clipIndex);
1106                     if (!uuid.isNull()) {
1107                         meltJob->setPostJobAction(new ReverseReplacePostJobAction(resource, filename, tmpFileName, uuid.toByteArray(), in));
1108                         JOBS.add(meltJob);
1109                         return;
1110                     }
1111                 }
1112             }
1113             meltJob->setPostJobAction(new ReverseOpenPostJobAction(resource, filename, tmpFileName));
1114             JOBS.add(meltJob);
1115         }
1116     }
1117 }
1118 
1119 
on_actionExtractSubclip_triggered()1120 void AvformatProducerWidget::on_actionExtractSubclip_triggered()
1121 {
1122     QString resource = GetFilenameFromProducer(producer());
1123     QString path = Settings.savePath();
1124     QFileInfo fi(resource);
1125 
1126     path.append("/%1 - %2.%3");
1127     path = path.arg(fi.completeBaseName()).arg(tr("Sub-clip")).arg(fi.suffix());
1128     QString caption = tr("Extract Sub-clip...");
1129     QString nameFilter = tr("%1 (*.%2);;All Files (*)").arg(fi.suffix()).arg(fi.suffix());
1130     QString filename = QFileDialog::getSaveFileName(this, caption, path, nameFilter,
1131         nullptr, Util::getFileDialogOptions());
1132 
1133     if (!filename.isEmpty()) {
1134         if (filename == QDir::toNativeSeparators(resource)) {
1135             QMessageBox::warning(this, caption,
1136                                  QObject::tr("Unable to write file %1\n"
1137                                     "Perhaps you do not have permission.\n"
1138                                     "Try again with a different folder.")
1139                                  .arg(fi.fileName()));
1140             return;
1141         }
1142         if (Util::warnIfNotWritable(filename, this, caption))
1143             return;
1144         Settings.setSavePath(QFileInfo(filename).path());
1145 
1146         QStringList ffmpegArgs;
1147 
1148         // Build the ffmpeg command line.
1149         ffmpegArgs << "-loglevel" << "verbose";
1150         ffmpegArgs << "-i" << resource;
1151         // set trim options
1152         if (m_producer->get_int(kFilterInProperty) || m_producer->get_int("in")) {
1153             if (m_producer->get(kFilterInProperty))
1154                 ffmpegArgs << "-ss" << QString::fromLatin1(m_producer->get_time(kFilterInProperty, mlt_time_clock)).replace(',', '.');
1155             else
1156                 ffmpegArgs << "-ss" << QString::fromLatin1(m_producer->get_time("in", mlt_time_clock)).replace(',', '.').replace(',', '.');
1157         }
1158         if (m_producer->get(kFilterOutProperty))
1159             ffmpegArgs << "-to" << QString::fromLatin1(m_producer->get_time(kFilterOutProperty, mlt_time_clock)).replace(',', '.');
1160         else
1161             ffmpegArgs << "-to" << QString::fromLatin1(m_producer->get_time("out", mlt_time_clock)).replace(',', '.');
1162         ffmpegArgs << "-avoid_negative_ts" << "make_zero"
1163                    << "-map" << "0:V?" << "-map" << "0:a?" << "-map" << "0:s?" << "-map" << "0:d?"
1164                    << "-map_metadata" << "0"
1165                    << "-codec" << "copy" << "-y" << filename;
1166 
1167         // Run the ffmpeg job.
1168         FfmpegJob* ffmpegJob = new FfmpegJob(filename, ffmpegArgs, false);
1169         ffmpegJob->setLabel(tr("Extract sub-clip %1").arg(Util::baseName(resource)));
1170         JOBS.add(ffmpegJob);
1171     }
1172 }
1173 
1174 
on_actionSetFileDate_triggered()1175 void AvformatProducerWidget::on_actionSetFileDate_triggered()
1176 {
1177     QString resource = GetFilenameFromProducer(producer());
1178     FileDateDialog dialog(resource, producer(), this);
1179     dialog.setModal(QmlApplication::dialogModality());
1180     dialog.exec();
1181 }
1182 
on_rangeComboBox_activated(int index)1183 void AvformatProducerWidget::on_rangeComboBox_activated(int index)
1184 {
1185     if (m_producer) {
1186         m_producer->set("color_range", index? 2 : 1);
1187         recreateProducer();
1188     }
1189 }
1190 
on_filenameLabel_editingFinished()1191 void AvformatProducerWidget::on_filenameLabel_editingFinished()
1192 {
1193     if (m_producer) {
1194         const auto caption = ui->filenameLabel->text();
1195         if (caption.isEmpty()) {
1196             double warpSpeed = GetSpeedFromProducer(producer());
1197             QString resource = GetFilenameFromProducer(producer());
1198             QString caption = Util::baseName(resource);
1199             if(warpSpeed != 1.0)
1200                 caption = QString("%1 (%2x)").arg(caption).arg(warpSpeed);
1201             m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
1202             ui->filenameLabel->setText(caption);
1203         } else {
1204             m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
1205         }
1206         emit modified();
1207     }
1208 }
1209 
on_convertButton_clicked()1210 void AvformatProducerWidget::on_convertButton_clicked()
1211 {
1212     on_actionFFmpegConvert_triggered();
1213 }
1214 
on_actionDisableProxy_triggered(bool checked)1215 void AvformatProducerWidget::on_actionDisableProxy_triggered(bool checked)
1216 {
1217     if (checked) {
1218         producer()->set(kDisableProxyProperty, 1);
1219 
1220         // Replace with original
1221         if (producer()->get_int(kIsProxyProperty) && producer()->get(kOriginalResourceProperty)) {
1222             Mlt::Producer original(MLT.profile(), producer()->get(kOriginalResourceProperty));
1223             if (original.is_valid()) {
1224                 if (!qstrcmp(original.get("mlt_service"), "avformat")) {
1225                     original.set("mlt_service", "avformat-novalidate");
1226                     original.set("mute_on_pause", 0);
1227                 }
1228                 original.set(kDisableProxyProperty, 1);
1229                 MAIN.replaceAllByHash(Util::getHash(original), original, true);
1230             }
1231         }
1232     } else {
1233         producer()->Mlt::Properties::clear(kDisableProxyProperty);
1234         ui->actionMakeProxy->setEnabled(true);
1235     }
1236 }
1237 
on_actionMakeProxy_triggered()1238 void AvformatProducerWidget::on_actionMakeProxy_triggered()
1239 {
1240     bool fullRange = ui->rangeComboBox->currentIndex() == 1;
1241     QPoint aspectRatio(ui->aspectNumSpinBox->value(), ui->aspectDenSpinBox->value());
1242     ProxyManager::ScanMode scan = ProxyManager::Progressive;
1243     if (!ui->scanComboBox->currentIndex())
1244         scan = ui->fieldOrderComboBox->currentIndex()? ProxyManager::InterlacedTopFieldFirst
1245                                                      : ProxyManager::InterlacedBottomFieldFirst;
1246 
1247     ProxyManager::generateVideoProxy(*producer(), fullRange, scan, aspectRatio);
1248 }
1249 
on_actionDeleteProxy_triggered()1250 void AvformatProducerWidget::on_actionDeleteProxy_triggered()
1251 {
1252     // Delete the file if it exists
1253     QString hash = Util::getHash(*producer());
1254     QString fileName = hash + ProxyManager::videoFilenameExtension();
1255     QDir dir = ProxyManager::dir();
1256     LOG_DEBUG() << "removing" << dir.filePath(fileName);
1257     dir.remove(dir.filePath(fileName));
1258 
1259     // Delete the pending file if it exists));
1260     fileName = hash + ProxyManager::pendingVideoExtension();
1261     dir.remove(dir.filePath(fileName));
1262 
1263     // Replace with original
1264     if (producer()->get_int(kIsProxyProperty) && producer()->get(kOriginalResourceProperty)) {
1265         Mlt::Producer original(MLT.profile(), producer()->get(kOriginalResourceProperty));
1266         if (original.is_valid()) {
1267             if (!qstrcmp(original.get("mlt_service"), "avformat")) {
1268                 original.set("mlt_service", "avformat-novalidate");
1269                 original.set("mute_on_pause", 0);
1270             }
1271             MAIN.replaceAllByHash(hash, original, true);
1272         }
1273     }
1274 }
1275 
on_actionCopyHashCode_triggered()1276 void AvformatProducerWidget::on_actionCopyHashCode_triggered()
1277 {
1278     qApp->clipboard()->setText(Util::getHash(*producer()));
1279     QMessageBox::information(this, qApp->applicationName(),
1280                              tr("The hash code below is already copied to your clipboard:\n\n") +
1281                              Util::getHash(*producer()),
1282                              QMessageBox::Ok);
1283 }
1284 
on_proxyButton_clicked()1285 void AvformatProducerWidget::on_proxyButton_clicked()
1286 {
1287     if (m_producer->get_int("video_index") >= 0) {
1288         QMenu menu;
1289         if (ProxyManager::isValidVideo(*producer())) {
1290             menu.addAction(ui->actionMakeProxy);
1291         }
1292 #ifndef Q_OS_WIN
1293         menu.addAction(ui->actionDeleteProxy);
1294 #endif
1295         menu.addAction(ui->actionDisableProxy);
1296         menu.addAction(ui->actionCopyHashCode);
1297         if (m_producer->get_int(kDisableProxyProperty)) {
1298             ui->actionMakeProxy->setDisabled(true);
1299             ui->actionDisableProxy->setChecked(true);
1300         }
1301         menu.exec(ui->proxyButton->mapToGlobal(QPoint(0, 0)));
1302     }
1303 }
1304 
on_actionReset_triggered()1305 void AvformatProducerWidget::on_actionReset_triggered()
1306 {
1307     ui->speedSpinBox->setValue(1.0);
1308     ui->pitchCheckBox->setCheckState(Qt::Unchecked);
1309     Mlt::Producer* p = newProducer(MLT.profile());
1310     ui->durationSpinBox->setValue(m_defaultDuration);
1311     ui->syncSlider->setValue(0);
1312     Mlt::Controller::copyFilters(*m_producer, *p);
1313     if (m_producer->get(kMultitrackItemProperty)) {
1314         emit producerChanged(p);
1315         delete p;
1316     } else {
1317         reopen(p);
1318     }
1319 }
1320 
on_actionSetEquirectangular_triggered()1321 void AvformatProducerWidget::on_actionSetEquirectangular_triggered()
1322 {
1323     // Get the location and file name for the report.
1324     QString caption = tr("Set Equirectangular Projection");
1325     QFileInfo info(GetFilenameFromProducer(producer()));
1326     QString directory = QString("%1/%2 - ERP.%3")
1327             .arg(info.path())
1328             .arg(info.completeBaseName())
1329             .arg(info.suffix());
1330     QString filePath = QFileDialog::getSaveFileName(&MAIN, caption, directory, QString(),
1331         nullptr, Util::getFileDialogOptions());
1332     if (!filePath.isEmpty()) {
1333         if (SpatialMedia::injectSpherical(objectName().toStdString(), filePath.toStdString())) {
1334             MAIN.showStatusMessage(tr("Successfully wrote %1").arg(QFileInfo(filePath).fileName()));
1335         } else {
1336             MAIN.showStatusMessage(tr("An error occurred saving the projection."));
1337         }
1338     }
1339 }
1340