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 setAudioCodec(m_ui.audio->currentText());
176 setVideoCodec(m_ui.video->currentText());
177 setAudioBitrate(m_ui.abr->value());
178 setVideoBitrate(m_ui.vbr->value());
179 setContainer(m_ui.container->currentText());
180 setWidth(m_ui.width->value());
181 setHeight(m_ui.height->value());
182
183 showAdvanced(false);
184}
185
186VideoView::~VideoView() {
187 stopRecording();
188 free(m_audioCodecCstr);
189 free(m_videoCodecCstr);
190 free(m_containerCstr);
191}
192
193void VideoView::startRecording() {
194 if (!validateSettings()) {
195 return;
196 }
197 if (!FFmpegEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) {
198 return;
199 }
200 m_ui.start->setEnabled(false);
201 m_ui.stop->setEnabled(true);
202 emit recordingStarted(&m_encoder.d);
203}
204
205void VideoView::stopRecording() {
206 emit recordingStopped();
207 FFmpegEncoderClose(&m_encoder);
208 m_ui.stop->setEnabled(false);
209 validateSettings();
210}
211
212void VideoView::selectFile() {
213 QString filename = QFileDialog::getSaveFileName(this, tr("Select output file"));
214 if (!filename.isEmpty()) {
215 m_ui.filename->setText(filename);
216 }
217}
218
219void VideoView::setFilename(const QString& fname) {
220 m_filename = fname;
221 validateSettings();
222}
223
224void VideoView::setAudioCodec(const QString& codec, bool manual) {
225 free(m_audioCodecCstr);
226 m_audioCodec = sanitizeCodec(codec, s_acodecMap);
227 if (m_audioCodec == "none") {
228 m_audioCodecCstr = nullptr;
229 } else {
230 m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData());
231 }
232 if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) {
233 free(m_audioCodecCstr);
234 m_audioCodecCstr = nullptr;
235 m_audioCodec = QString();
236 }
237 validateSettings();
238 if (manual) {
239 uncheckIncompatible();
240 }
241}
242
243void VideoView::setVideoCodec(const QString& codec, bool manual) {
244 free(m_videoCodecCstr);
245 m_videoCodec = sanitizeCodec(codec, s_vcodecMap);
246 m_videoCodecCstr = strdup(m_videoCodec.toLocal8Bit().constData());
247 if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) {
248 free(m_videoCodecCstr);
249 m_videoCodecCstr = nullptr;
250 m_videoCodec = QString();
251 }
252 validateSettings();
253 if (manual) {
254 uncheckIncompatible();
255 }
256}
257
258void VideoView::setContainer(const QString& container, bool manual) {
259 free(m_containerCstr);
260 m_container = sanitizeCodec(container, s_containerMap);
261 m_containerCstr = strdup(m_container.toLocal8Bit().constData());
262 if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) {
263 free(m_containerCstr);
264 m_containerCstr = nullptr;
265 m_container = QString();
266 }
267 validateSettings();
268 if (manual) {
269 uncheckIncompatible();
270 }
271}
272
273void VideoView::setAudioBitrate(int br, bool manual) {
274 m_abr = br * 1000;
275 FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr);
276 validateSettings();
277 if (manual) {
278 uncheckIncompatible();
279 }
280}
281
282void VideoView::setVideoBitrate(int br, bool manual) {
283 m_vbr = br * 1000;
284 FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr);
285 validateSettings();
286 if (manual) {
287 uncheckIncompatible();
288 }
289}
290
291void VideoView::setWidth(int width, bool manual) {
292 m_width = width;
293 updateAspectRatio(width, 0);
294 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
295 if (manual) {
296 uncheckIncompatible();
297 }
298}
299
300void VideoView::setHeight(int height, bool manual) {
301 m_height = height;
302 updateAspectRatio(0, height);
303 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
304 if (manual) {
305 uncheckIncompatible();
306 }
307}
308
309void VideoView::setAspectWidth(int, bool manual) {
310 updateAspectRatio(0, m_height, true);
311 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
312 if (manual) {
313 uncheckIncompatible();
314 }
315}
316
317void VideoView::setAspectHeight(int, bool manual) {
318 updateAspectRatio(m_width, 0, true);
319 FFmpegEncoderSetDimensions(&m_encoder, m_width, m_height);
320 if (manual) {
321 uncheckIncompatible();
322 }
323}
324
325void VideoView::showAdvanced(bool show) {
326 m_ui.advancedBox->setVisible(show);
327}
328
329bool VideoView::validateSettings() {
330 bool valid = !m_filename.isNull() && !FFmpegEncoderIsOpen(&m_encoder);
331 if (m_audioCodec.isNull()) {
332 valid = false;
333 m_ui.audio->setStyleSheet("QComboBox { color: red; }");
334 } else {
335 m_ui.audio->setStyleSheet("");
336 }
337
338 if (m_videoCodec.isNull()) {
339 valid = false;
340 m_ui.video->setStyleSheet("QComboBox { color: red; }");
341 } else {
342 m_ui.video->setStyleSheet("");
343 }
344
345 if (m_container.isNull()) {
346 valid = false;
347 m_ui.container->setStyleSheet("QComboBox { color: red; }");
348 } else {
349 m_ui.container->setStyleSheet("");
350 }
351
352 // This |valid| check is necessary as if one of the cstrs
353 // is null, the encoder likely has a dangling pointer
354 if (valid && !FFmpegEncoderVerifyContainer(&m_encoder)) {
355 valid = false;
356 }
357
358 m_ui.start->setEnabled(valid);
359
360 return valid;
361}
362
363void VideoView::updateAspectRatio(int width, int height, bool force) {
364 if (m_ui.lockRatio->isChecked() || force) {
365 if (width) {
366 height = m_ui.hratio->value() * width / m_ui.wratio->value();
367 } else if (height) {
368 width = m_ui.wratio->value() * height / m_ui.hratio->value();
369 }
370
371 m_width = width;
372 m_height = height;
373 safelySet(m_ui.width, m_width);
374 safelySet(m_ui.height, m_height);
375 } else {
376 int w = m_width;
377 int h = m_height;
378 // Get greatest common divisor
379 while (w != 0) {
380 int temp = h % w;
381 h = w;
382 w = temp;
383 }
384 int gcd = h;
385 w = m_width / gcd;
386 h = m_height / gcd;
387 safelySet(m_ui.wratio, w);
388 safelySet(m_ui.hratio, h);
389 }
390}
391
392void VideoView::uncheckIncompatible() {
393 Preset current = {
394 .container = m_container,
395 .vcodec = m_videoCodec,
396 .acodec = m_audioCodec,
397 .vbr = m_vbr / 1000,
398 .abr = m_abr / 1000,
399 .width = m_width,
400 .height = m_height
401 };
402
403 m_ui.presets->setExclusive(false);
404 m_ui.resolutions->setExclusive(false);
405 for (auto iterator = m_presets.constBegin(); iterator != m_presets.constEnd(); ++iterator) {
406 Preset next = *iterator;
407 next.container = sanitizeCodec(next.container, s_containerMap);
408 next.acodec = sanitizeCodec(next.acodec, s_acodecMap);
409 next.vcodec = sanitizeCodec(next.vcodec, s_vcodecMap);
410 if (!current.compatible(next)) {
411 safelyCheck(iterator.key(), false);
412 }
413 }
414 m_ui.presets->setExclusive(true);
415 m_ui.resolutions->setExclusive(true);
416
417 if (current.compatible(m_presets[m_ui.preset160])) {
418 safelyCheck(m_ui.preset160);
419 }
420 if (current.compatible(m_presets[m_ui.preset480])) {
421 safelyCheck(m_ui.preset480);
422 }
423 if (current.compatible(m_presets[m_ui.preset720])) {
424 safelyCheck(m_ui.preset720);
425 }
426 if (current.compatible(m_presets[m_ui.preset1080])) {
427 safelyCheck(m_ui.preset1080);
428 }
429}
430
431QString VideoView::sanitizeCodec(const QString& codec, const QMap<QString, QString>& mapping) {
432 QString sanitized = codec.toLower();
433 sanitized = sanitized.remove(QChar('.'));
434 if (mapping.contains(sanitized)) {
435 sanitized = mapping[sanitized];
436 }
437 return sanitized;
438}
439
440void VideoView::safelyCheck(QAbstractButton* button, bool set) {
441 bool signalsBlocked = button->blockSignals(true);
442 bool autoExclusive = button->autoExclusive();
443 button->setAutoExclusive(false);
444 button->setChecked(set);
445 button->setAutoExclusive(autoExclusive);
446 button->blockSignals(signalsBlocked);
447}
448
449void VideoView::safelySet(QSpinBox* box, int value) {
450 bool signalsBlocked = box->blockSignals(true);
451 box->setValue(value);
452 box->blockSignals(signalsBlocked);
453}
454
455void VideoView::safelySet(QComboBox* box, const QString& value) {
456 bool signalsBlocked = box->blockSignals(true);
457 box->lineEdit()->setText(value);
458 box->blockSignals(signalsBlocked);
459}
460
461void VideoView::addPreset(QAbstractButton* button, const Preset& preset) {
462 m_presets[button] = preset;
463 connect(button, &QAbstractButton::pressed, [this, preset]() {
464 setPreset(preset);
465 });
466}
467
468void VideoView::setPreset(const Preset& preset) {
469 if (!preset.container.isNull()) {
470 setContainer(preset.container, false);
471 safelySet(m_ui.container, preset.container);
472 }
473 if (!preset.acodec.isNull()) {
474 setAudioCodec(preset.acodec, false);
475 safelySet(m_ui.audio, preset.acodec);
476 }
477 if (!preset.vcodec.isNull()) {
478 setVideoCodec(preset.vcodec, false);
479 safelySet(m_ui.video, preset.vcodec);
480 }
481 if (preset.abr) {
482 setAudioBitrate(preset.abr, false);
483 safelySet(m_ui.abr, preset.abr);
484 }
485 if (preset.vbr) {
486 setVideoBitrate(preset.vbr, false);
487 safelySet(m_ui.vbr, preset.vbr);
488 }
489 if (preset.width) {
490 setWidth(preset.width, false);
491 safelySet(m_ui.width, preset.width);
492 }
493 if (preset.height) {
494 setHeight(preset.height, false);
495 safelySet(m_ui.height, preset.height);
496 }
497
498 uncheckIncompatible();
499 validateSettings();
500}
501
502#endif