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