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