all repos — mgba @ 9c92a29b28d1f81224ba28d5fc83a9481eccd5eb

mGBA Game Boy Advance Emulator

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