src/platform/qt/VideoView.cpp (view raw)
1/* Copyright (c) 2013-2014 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include "VideoView.h"
7
8#ifdef USE_FFMPEG
9
10#include <QFileDialog>
11#include <QMap>
12
13using namespace QGBA;
14
15QMap<QString, QString> VideoView::s_acodecMap;
16QMap<QString, QString> VideoView::s_vcodecMap;
17QMap<QString, QString> VideoView::s_containerMap;
18
19bool VideoView::Preset::compatible(const Preset& other) const {
20 if (!other.container.isNull() && !container.isNull() && other.container != container) {
21 return false;
22 }
23 if (!other.acodec.isNull() && !acodec.isNull() && other.acodec != acodec) {
24 return false;
25 }
26 if (!other.vcodec.isNull() && !vcodec.isNull() && other.vcodec != vcodec) {
27 return false;
28 }
29 if (other.abr && abr && other.abr != abr) {
30 return false;
31 }
32 if (other.vbr && vbr && other.vbr != vbr) {
33 return false;
34 }
35 if (other.width && width && other.width != width) {
36 return false;
37 }
38 if (other.height && height && other.height != height) {
39 return false;
40 }
41 return true;
42}
43
44VideoView::VideoView(QWidget* parent)
45 : QWidget(parent)
46 , m_audioCodecCstr(nullptr)
47 , m_videoCodecCstr(nullptr)
48 , m_containerCstr(nullptr)
49{
50 m_ui.setupUi(this);
51
52 if (s_acodecMap.empty()) {
53 s_acodecMap["mp3"] = "libmp3lame";
54 s_acodecMap["opus"] = "libopus";
55 s_acodecMap["uncompressed"] = "pcm_s16le";
56 }
57 if (s_vcodecMap.empty()) {
58 s_vcodecMap["dirac"] = "libschroedinger";
59 s_vcodecMap["h264"] = "libx264";
60 s_vcodecMap["hevc"] = "libx265";
61 s_vcodecMap["theora"] = "libtheora";
62 s_vcodecMap["vp8"] = "libvpx";
63 s_vcodecMap["vp9"] = "libvpx-vp9";
64 s_vcodecMap["xvid"] = "libxvid";
65 }
66 if (s_containerMap.empty()) {
67 s_containerMap["mkv"] = "matroska";
68 }
69
70 connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close()));
71 connect(m_ui.start, SIGNAL(clicked()), this, SLOT(startRecording()));
72 connect(m_ui.stop, SIGNAL(clicked()), this, SLOT(stopRecording()));
73
74 connect(m_ui.selectFile, SIGNAL(clicked()), this, SLOT(selectFile()));
75 connect(m_ui.filename, SIGNAL(textChanged(const QString&)), this, SLOT(setFilename(const QString&)));
76
77 connect(m_ui.audio, SIGNAL(activated(const QString&)), this, SLOT(setAudioCodec(const QString&)));
78 connect(m_ui.video, SIGNAL(activated(const QString&)), this, SLOT(setVideoCodec(const QString&)));
79 connect(m_ui.container, SIGNAL(activated(const QString&)), this, SLOT(setContainer(const QString&)));
80 connect(m_ui.audio, SIGNAL(editTextChanged(const QString&)), this, SLOT(setAudioCodec(const QString&)));
81 connect(m_ui.video, SIGNAL(editTextChanged(const QString&)), this, SLOT(setVideoCodec(const QString&)));
82 connect(m_ui.container, SIGNAL(editTextChanged(const QString&)), this, SLOT(setContainer(const QString&)));
83
84 connect(m_ui.abr, SIGNAL(valueChanged(int)), this, SLOT(setAudioBitrate(int)));
85 connect(m_ui.vbr, SIGNAL(valueChanged(int)), this, SLOT(setVideoBitrate(int)));
86
87 connect(m_ui.width, SIGNAL(valueChanged(int)), this, SLOT(setWidth(int)));
88 connect(m_ui.height, SIGNAL(valueChanged(int)), this, SLOT(setHeight(int)));
89
90 connect(m_ui.wratio, SIGNAL(valueChanged(int)), this, SLOT(setAspectWidth(int)));
91 connect(m_ui.hratio, SIGNAL(valueChanged(int)), this, SLOT(setAspectHeight(int)));
92
93 connect(m_ui.showAdvanced, SIGNAL(clicked(bool)), this, SLOT(showAdvanced(bool)));
94
95 FFmpegEncoderInit(&m_encoder);
96
97 addPreset(m_ui.preset1080, (Preset) {
98 .container = QString(),
99 .vcodec = QString(),
100 .acodec = QString(),
101 .vbr = 0,
102 .abr = 0,
103 .width = 1620,
104 .height = 1080
105 });
106
107 addPreset(m_ui.preset720, (Preset) {
108 .container = QString(),
109 .vcodec = QString(),
110 .acodec = QString(),
111 .vbr = 0,
112 .abr = 0,
113 .width = 1080,
114 .height = 720
115 });
116
117 addPreset(m_ui.preset480, (Preset) {
118 .container = QString(),
119 .vcodec = QString(),
120 .acodec = QString(),
121 .vbr = 0,
122 .abr = 0,
123 .width = 720,
124 .height = 480
125 });
126
127 addPreset(m_ui.preset160, (Preset) {
128 .container = QString(),
129 .vcodec = QString(),
130 .acodec = QString(),
131 .vbr = 0,
132 .abr = 0,
133 .width = 240,
134 .height = 160
135 });
136
137 addPreset(m_ui.presetHQ, (Preset) {
138 .container = "MP4",
139 .vcodec = "h.264",
140 .acodec = "AAC",
141 .vbr = 8000,
142 .abr = 384,
143 .width = 1620,
144 .height = 1080
145 });
146
147 addPreset(m_ui.presetYoutube, (Preset) {
148 .container = "MP4",
149 .vcodec = "h.264",
150 .acodec = "AAC",
151 .vbr = 5000,
152 .abr = 256,
153 .width = 1080,
154 .height = 720
155 });
156
157 addPreset(m_ui.presetWebM, (Preset) {
158 .container = "WebM",
159 .vcodec = "VP8",
160 .acodec = "Vorbis",
161 .vbr = 800,
162 .abr = 128
163 });
164
165 addPreset(m_ui.presetLossless, (Preset) {
166 .container = "MKV",
167 .vcodec = "PNG",
168 .acodec = "FLAC",
169 .vbr = 0,
170 .abr = 0,
171 .width = 240,
172 .height = 160,
173 });
174
175 setPreset((Preset) {
176 .container = "MKV",
177 .vcodec = "PNG",
178 .acodec = "FLAC",
179 .vbr = 0,
180 .abr = 0,
181 .width = 240,
182 .height = 160,
183 });
184
185 showAdvanced(false);
186}
187
188VideoView::~VideoView() {
189 stopRecording();
190 free(m_audioCodecCstr);
191 free(m_videoCodecCstr);
192 free(m_containerCstr);
193}
194
195void VideoView::startRecording() {
196 if (!validateSettings()) {
197 return;
198 }
199 if (!FFmpegEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) {
200 return;
201 }
202 m_ui.start->setEnabled(false);
203 m_ui.stop->setEnabled(true);
204 emit recordingStarted(&m_encoder.d);
205}
206
207void VideoView::stopRecording() {
208 emit recordingStopped();
209 FFmpegEncoderClose(&m_encoder);
210 m_ui.stop->setEnabled(false);
211 validateSettings();
212}
213
214void VideoView::selectFile() {
215 QString filename = QFileDialog::getSaveFileName(this, tr("Select output file"));
216 if (!filename.isEmpty()) {
217 m_ui.filename->setText(filename);
218 }
219}
220
221void VideoView::setFilename(const QString& fname) {
222 m_filename = fname;
223 validateSettings();
224}
225
226void VideoView::setAudioCodec(const QString& codec, bool manual) {
227 free(m_audioCodecCstr);
228 m_audioCodec = sanitizeCodec(codec, s_acodecMap);
229 if (m_audioCodec == "none") {
230 m_audioCodecCstr = nullptr;
231 } else {
232 m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData());
233 }
234 if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) {
235 free(m_audioCodecCstr);
236 m_audioCodecCstr = nullptr;
237 m_audioCodec = QString();
238 }
239 validateSettings();
240 if (manual) {
241 uncheckIncompatible();
242 }
243}
244
245void VideoView::setVideoCodec(const QString& codec, bool manual) {
246 free(m_videoCodecCstr);
247 m_videoCodec = sanitizeCodec(codec, s_vcodecMap);
248 m_videoCodecCstr = strdup(m_videoCodec.toLocal8Bit().constData());
249 if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
250 free(m_videoCodecCstr);
251 m_videoCodecCstr = nullptr;
252 m_videoCodec = QString();
253 }
254 validateSettings();
255 if (manual) {
256 uncheckIncompatible();
257 }
258}
259
260void VideoView::setContainer(const QString& container, bool manual) {
261 free(m_containerCstr);
262 m_container = sanitizeCodec(container, s_containerMap);
263 m_containerCstr = strdup(m_container.toLocal8Bit().constData());
264 if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) {
265 free(m_containerCstr);
266 m_containerCstr = nullptr;
267 m_container = QString();
268 }
269 validateSettings();
270 if (manual) {
271 uncheckIncompatible();
272 }
273}
274
275void VideoView::setAudioBitrate(int br, bool manual) {
276 m_abr = br * 1000;
277 FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr);
278 validateSettings();
279 if (manual) {
280 uncheckIncompatible();
281 }
282}
283
284void VideoView::setVideoBitrate(int br, bool manual) {
285 m_vbr = br * 1000;
286 FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr);
287 validateSettings();
288 if (manual) {
289 uncheckIncompatible();
290 }
291}
292
293void VideoView::setWidth(int width, bool manual) {
294 m_width = width;
295 updateAspectRatio(width, 0);
296 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
297 if (manual) {
298 uncheckIncompatible();
299 }
300}
301
302void VideoView::setHeight(int height, bool manual) {
303 m_height = height;
304 updateAspectRatio(0, height);
305 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
306 if (manual) {
307 uncheckIncompatible();
308 }
309}
310
311void VideoView::setAspectWidth(int, bool manual) {
312 updateAspectRatio(0, m_height, true);
313 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
314 if (manual) {
315 uncheckIncompatible();
316 }
317}
318
319void VideoView::setAspectHeight(int, bool manual) {
320 updateAspectRatio(m_width, 0, true);
321 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
322 if (manual) {
323 uncheckIncompatible();
324 }
325}
326
327void VideoView::showAdvanced(bool show) {
328 m_ui.advancedBox->setVisible(show);
329}
330
331bool VideoView::validateSettings() {
332 bool valid = !m_filename.isNull() && !FFmpegEncoderIsOpen(&m_encoder);
333 if (m_audioCodec.isNull()) {
334 valid = false;
335 m_ui.audio->setStyleSheet("QComboBox { color: red; }");
336 } else {
337 m_ui.audio->setStyleSheet("");
338 }
339
340 if (m_videoCodec.isNull()) {
341 valid = false;
342 m_ui.video->setStyleSheet("QComboBox { color: red; }");
343 } else {
344 m_ui.video->setStyleSheet("");
345 }
346
347 if (m_container.isNull()) {
348 valid = false;
349 m_ui.container->setStyleSheet("QComboBox { color: red; }");
350 } else {
351 m_ui.container->setStyleSheet("");
352 }
353
354 // This |valid| check is necessary as if one of the cstrs
355 // is null, the encoder likely has a dangling pointer
356 if (valid && !FFmpegEncoderVerifyContainer(&m_encoder)) {
357 valid = false;
358 }
359
360 m_ui.start->setEnabled(valid);
361
362 return valid;
363}
364
365void VideoView::updateAspectRatio(int width, int height, bool force) {
366 if (m_ui.lockRatio->isChecked() || force) {
367 if (width) {
368 height = m_ui.hratio->value() * width / m_ui.wratio->value();
369 } else if (height) {
370 width = m_ui.wratio->value() * height / m_ui.hratio->value();
371 }
372
373 m_width = width;
374 m_height = height;
375 safelySet(m_ui.width, m_width);
376 safelySet(m_ui.height, m_height);
377 } else {
378 int w = m_width;
379 int h = m_height;
380 // Get greatest common divisor
381 while (w != 0) {
382 int temp = h % w;
383 h = w;
384 w = temp;
385 }
386 int gcd = h;
387 w = m_width / gcd;
388 h = m_height / gcd;
389 safelySet(m_ui.wratio, w);
390 safelySet(m_ui.hratio, h);
391 }
392}
393
394void VideoView::uncheckIncompatible() {
395 Preset current = {
396 .container = m_container,
397 .vcodec = m_videoCodec,
398 .acodec = m_audioCodec,
399 .vbr = m_vbr / 1000,
400 .abr = m_abr / 1000,
401 .width = m_width,
402 .height = m_height
403 };
404
405 m_ui.presets->setExclusive(false);
406 m_ui.resolutions->setExclusive(false);
407 for (auto iterator = m_presets.constBegin(); iterator != m_presets.constEnd(); ++iterator) {
408 Preset next = *iterator;
409 next.container = sanitizeCodec(next.container, s_containerMap);
410 next.acodec = sanitizeCodec(next.acodec, s_acodecMap);
411 next.vcodec = sanitizeCodec(next.vcodec, s_vcodecMap);
412 if (!current.compatible(next)) {
413 safelyCheck(iterator.key(), false);
414 }
415 }
416 m_ui.presets->setExclusive(true);
417 m_ui.resolutions->setExclusive(true);
418
419 if (current.compatible(m_presets[m_ui.preset160])) {
420 safelyCheck(m_ui.preset160);
421 }
422 if (current.compatible(m_presets[m_ui.preset480])) {
423 safelyCheck(m_ui.preset480);
424 }
425 if (current.compatible(m_presets[m_ui.preset720])) {
426 safelyCheck(m_ui.preset720);
427 }
428 if (current.compatible(m_presets[m_ui.preset1080])) {
429 safelyCheck(m_ui.preset1080);
430 }
431}
432
433QString VideoView::sanitizeCodec(const QString& codec, const QMap<QString, QString>& mapping) {
434 QString sanitized = codec.toLower();
435 sanitized = sanitized.remove(QChar('.'));
436 if (mapping.contains(sanitized)) {
437 sanitized = mapping[sanitized];
438 }
439 return sanitized;
440}
441
442void VideoView::safelyCheck(QAbstractButton* button, bool set) {
443 bool signalsBlocked = button->blockSignals(true);
444 bool autoExclusive = button->autoExclusive();
445 button->setAutoExclusive(false);
446 button->setChecked(set);
447 button->setAutoExclusive(autoExclusive);
448 button->blockSignals(signalsBlocked);
449}
450
451void VideoView::safelySet(QSpinBox* box, int value) {
452 bool signalsBlocked = box->blockSignals(true);
453 box->setValue(value);
454 box->blockSignals(signalsBlocked);
455}
456
457void VideoView::safelySet(QComboBox* box, const QString& value) {
458 bool signalsBlocked = box->blockSignals(true);
459 box->lineEdit()->setText(value);
460 box->blockSignals(signalsBlocked);
461}
462
463void VideoView::addPreset(QAbstractButton* button, const Preset& preset) {
464 m_presets[button] = preset;
465 connect(button, &QAbstractButton::pressed, [this, preset]() {
466 setPreset(preset);
467 });
468}
469
470void VideoView::setPreset(const Preset& preset) {
471 if (!preset.container.isNull()) {
472 setContainer(preset.container, false);
473 safelySet(m_ui.container, preset.container);
474 }
475 if (!preset.acodec.isNull()) {
476 setAudioCodec(preset.acodec, false);
477 safelySet(m_ui.audio, preset.acodec);
478 }
479 if (!preset.vcodec.isNull()) {
480 setVideoCodec(preset.vcodec, false);
481 safelySet(m_ui.video, preset.vcodec);
482 }
483 if (preset.abr) {
484 setAudioBitrate(preset.abr, false);
485 safelySet(m_ui.abr, preset.abr);
486 }
487 if (preset.vbr) {
488 setVideoBitrate(preset.vbr, false);
489 safelySet(m_ui.vbr, preset.vbr);
490 }
491 if (preset.width) {
492 setWidth(preset.width, false);
493 safelySet(m_ui.width, preset.width);
494 }
495 if (preset.height) {
496 setHeight(preset.height, false);
497 safelySet(m_ui.height, preset.height);
498 }
499
500 uncheckIncompatible();
501 validateSettings();
502}
503
504#endif