1 /*
2 Copyright (c) 2012-2020 Maarten Baert <maarten-baert@hotmail.com>
3
4 This file is part of SimpleScreenRecorder.
5
6 SimpleScreenRecorder is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 SimpleScreenRecorder is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with SimpleScreenRecorder. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "PageRecord.h"
21
22 #include "CommandLineOptions.h"
23 #include "Icons.h"
24 #include "Dialogs.h"
25 #include "EnumStrings.h"
26 #include "MainWindow.h"
27 #include "PageInput.h"
28 #include "PageOutput.h"
29 #include "DialogRecordSchedule.h"
30
31 #include "HotkeyListener.h"
32
33 #include "Muxer.h"
34 #include "VideoEncoder.h"
35 #include "AudioEncoder.h"
36 #include "Synchronizer.h"
37 #include "X11Input.h"
38 #if SSR_USE_OPENGL_RECORDING
39 #include "GLInjectInput.h"
40 #endif
41 #if SSR_USE_V4L2
42 #include "V4L2Input.h"
43 #endif
44 #if SSR_USE_ALSA
45 #include "ALSAInput.h"
46 #endif
47 #if SSR_USE_PULSEAUDIO
48 #include "PulseAudioInput.h"
49 #endif
50 #if SSR_USE_JACK
51 #include "JACKInput.h"
52 #endif
53 #include "SimpleSynth.h"
54 #include "VideoPreviewer.h"
55 #include "AudioPreviewer.h"
56
GetNewSegmentFile(const QString & file,bool add_timestamp)57 static QString GetNewSegmentFile(const QString& file, bool add_timestamp) {
58 QFileInfo fi(file);
59 QDateTime now = QDateTime::currentDateTime();
60 QString newfile;
61 unsigned int counter = 0;
62 do {
63 ++counter;
64 newfile = fi.completeBaseName();
65 if(add_timestamp) {
66 if(!newfile.isEmpty())
67 newfile += "-";
68 newfile += now.toString("yyyy-MM-dd_hh.mm.ss");
69 }
70 if(counter != 1) {
71 if(!newfile.isEmpty())
72 newfile += "-";
73 newfile += "(" + QString::number(counter) + ")";
74 }
75 if(!fi.suffix().isEmpty())
76 newfile += "." + fi.suffix();
77 newfile = fi.path() + "/" + newfile;
78 } while(QFileInfo(newfile).exists());
79 return newfile;
80 }
81
GetOptionsFromString(const QString & str)82 static std::vector<std::pair<QString, QString> > GetOptionsFromString(const QString& str) {
83 std::vector<std::pair<QString, QString> > options;
84 QStringList optionlist = SplitSkipEmptyParts(str, ',');
85 for(int i = 0; i < optionlist.size(); ++i) {
86 QString a = optionlist[i];
87 int p = a.indexOf('=');
88 if(p < 0) {
89 options.push_back(std::make_pair(a.trimmed(), QString()));
90 } else {
91 options.push_back(std::make_pair(a.mid(0, p).trimmed(), a.mid(p + 1).trimmed()));
92 }
93 }
94 return options;
95 }
96
ReadableSizeIEC(uint64_t size,const QString & suffix)97 static QString ReadableSizeIEC(uint64_t size, const QString& suffix) {
98 if(size < (uint64_t) 10 * 1024)
99 return QString::number(size) + " " + suffix;
100 if(size < (uint64_t) 10 * 1024 * 1024)
101 return QString::number((size + 512) / 1024) + " Ki" + suffix;
102 if(size < (uint64_t) 10 * 1024 * 1024 * 1024)
103 return QString::number((size / 1024 + 512) / 1024) + " Mi" + suffix;
104 return QString::number((size / (1024 * 1024) + 512) / 1024) + " Gi" + suffix;
105 }
106
ReadableSizeSI(uint64_t size,const QString & suffix)107 static QString ReadableSizeSI(uint64_t size, const QString& suffix) {
108 if(size < (uint64_t) 10 * 1000)
109 return QString::number(size) + " " + suffix;
110 if(size < (uint64_t) 10 * 1000 * 1000)
111 return QString::number((size + 500) / 1000) + " k" + suffix;
112 if(size < (uint64_t) 10 * 1000 * 1000 * 1000)
113 return QString::number((size / 1000 + 500) / 1000) + " M" + suffix;
114 return QString::number((size / (1000 * 1000) + 512) / 1024) + " G" + suffix;
115 }
116
ReadableTime(int64_t time_micro)117 static QString ReadableTime(int64_t time_micro) {
118 unsigned int time = (time_micro + 500000) / 1000000;
119 return QString("%1:%2:%3")
120 .arg(time / 3600)
121 .arg((time / 60) % 60, 2, 10, QLatin1Char('0'))
122 .arg(time % 60, 2, 10, QLatin1Char('0'));
123 }
124
ReadableWidthHeight(unsigned int width,unsigned int height)125 static QString ReadableWidthHeight(unsigned int width, unsigned int height) {
126 if(width == 0 && height == 0)
127 return "?";
128 return QString::number(width) + "x" + QString::number(height);
129 }
130
131 class QTextEditSmall : public QTextEdit {
132
133 public:
QTextEditSmall(QWidget * parent)134 QTextEditSmall(QWidget *parent) : QTextEdit(parent) {}
QTextEditSmall(const QString & text,QWidget * parent)135 QTextEditSmall(const QString& text, QWidget *parent) : QTextEdit(text, parent) {}
sizeHint() const136 virtual QSize sizeHint() const override { return QSize(-1, 100); }
137
138 };
139
140 // sound notification sequences
141 #if SSR_USE_ALSA
142 static const std::array<SimpleSynth::Note, 1> SEQUENCE_RECORD_START = {{
143 {0 , 500, 10000, 440.0f * exp2f( 3.0f / 12.0f), 0.8f}, // C5
144 }};
145 static const std::array<SimpleSynth::Note, 2> SEQUENCE_RECORD_STOP = {{
146 {0 , 500, 20000, 440.0f * exp2f( 3.0f / 12.0f), 0.8f}, // C5
147 {10000, 500, 20000, 440.0f * exp2f(-2.0f / 12.0f), 0.8f}, // G4
148 }};
149 static const std::array<SimpleSynth::Note, 4> SEQUENCE_RECORD_ERROR = {{
150 {0 , 500, 20000, 440.0f * exp2f(-2.0f / 12.0f), 0.8f}, // G4
151 {10000, 500, 20000, 440.0f * exp2f(-2.0f / 12.0f), 0.8f}, // G4
152 {20000, 500, 20000, 440.0f * exp2f(-6.0f / 12.0f), 0.4f}, // D#4
153 {20000, 500, 20000, 440.0f * exp2f(-9.0f / 12.0f), 0.6f}, // C4
154 }};
155 #endif
156
PageRecord(MainWindow * main_window)157 PageRecord::PageRecord(MainWindow* main_window)
158 : QWidget(main_window->centralWidget()) {
159
160 m_main_window = main_window;
161
162 m_page_started = false;
163 m_input_started = false;
164 m_output_started = false;
165 m_previewing = false;
166
167 m_schedule_active = false;
168 m_schedule_time_zone = SCHEDULE_TIME_ZONE_LOCAL;
169
170 #if SSR_USE_ALSA
171 m_last_error_sound = std::numeric_limits<int64_t>::min();
172 #endif
173
174 m_stdin_reentrant = false;
175
176 QGroupBox *groupbox_recording = new QGroupBox(tr("Recording"), this);
177 {
178 m_pushbutton_record = new QPushButton(groupbox_recording);
179
180 m_label_schedule_status = new QLabel(groupbox_recording);
181 m_pushbutton_schedule_activate = new QPushButton(groupbox_recording);
182 m_pushbutton_schedule_edit = new QPushButton(tr("Edit schedule"), groupbox_recording);
183 // TODO: tooptips
184
185 m_checkbox_hotkey_enable = new QCheckBox(tr("Enable recording hotkey"), groupbox_recording);
186 m_checkbox_hotkey_enable->setToolTip(tr("The recording hotkey is a global keyboard shortcut that can be used to start or pause the recording at any time,\n"
187 "even when the SimpleScreenRecorder window is not visible. This way you can create recordings without having the\n"
188 "SimpleScreenRecorder window show up in the final video."));
189 #if SSR_USE_ALSA
190 m_checkbox_sound_notifications_enable = new QCheckBox(tr("Enable sound notifications"), groupbox_recording);
191 m_checkbox_sound_notifications_enable->setToolTip(tr("When enabled, a sound will be played when the recording is started or paused, or when an error occurs."));
192 #endif
193 QLabel *label_hotkey = new QLabel(tr("Hotkey:"), groupbox_recording);
194 m_checkbox_hotkey_ctrl = new QCheckBox(tr("Ctrl +"), groupbox_recording);
195 m_checkbox_hotkey_shift = new QCheckBox(tr("Shift +"), groupbox_recording);
196 m_checkbox_hotkey_alt = new QCheckBox(tr("Alt +"), groupbox_recording);
197 m_checkbox_hotkey_super = new QCheckBox(tr("Super +"), groupbox_recording);
198 m_combobox_hotkey_key = new QComboBox(groupbox_recording);
199 m_combobox_hotkey_key->setToolTip(tr("The key that you have to press (combined with the given modifiers) to start or pause recording.\n"
200 "The program that you are recording will not receive the key press."));
201 // Note: The choice of keys is currently rather limited, because capturing key presses session-wide is a bit harder than it looks.
202 // For example, applications are not allowed to capture the F1-F12 keys (on Ubuntu at least). The A-Z keys don't have this limitation apparently.
203 for(unsigned int i = 0; i < 26; ++i) {
204 m_combobox_hotkey_key->addItem(QString('A' + i));
205 }
206
207 connect(m_pushbutton_record, SIGNAL(clicked()), this, SLOT(OnRecordStartPause()));
208 connect(m_pushbutton_schedule_activate, SIGNAL(clicked()), this, SLOT(OnScheduleActivateDeactivate()));
209 connect(m_pushbutton_schedule_edit, SIGNAL(clicked()), this, SLOT(OnScheduleEdit()));
210 connect(m_checkbox_hotkey_enable, SIGNAL(clicked()), this, SLOT(OnUpdateHotkeyFields()));
211 #if SSR_USE_ALSA
212 connect(m_checkbox_sound_notifications_enable, SIGNAL(clicked()), this, SLOT(OnUpdateSoundNotifications()));
213 #endif
214 connect(m_checkbox_hotkey_ctrl, SIGNAL(clicked()), this, SLOT(OnUpdateHotkey()));
215 connect(m_checkbox_hotkey_shift, SIGNAL(clicked()), this, SLOT(OnUpdateHotkey()));
216 connect(m_checkbox_hotkey_alt, SIGNAL(clicked()), this, SLOT(OnUpdateHotkey()));
217 connect(m_checkbox_hotkey_super, SIGNAL(clicked()), this, SLOT(OnUpdateHotkey()));
218 connect(m_combobox_hotkey_key, SIGNAL(activated(int)), this, SLOT(OnUpdateHotkey()));
219
220 QVBoxLayout *layout = new QVBoxLayout(groupbox_recording);
221 layout->addWidget(m_pushbutton_record);
222 {
223 QHBoxLayout *layout2 = new QHBoxLayout();
224 layout->addLayout(layout2);
225 layout2->addWidget(m_label_schedule_status, 2);
226 layout2->addWidget(m_pushbutton_schedule_activate, 1);
227 layout2->addWidget(m_pushbutton_schedule_edit, 1);
228 }
229 {
230 QHBoxLayout *layout2 = new QHBoxLayout();
231 layout->addLayout(layout2);
232 layout2->addWidget(m_checkbox_hotkey_enable);
233 #if SSR_USE_ALSA
234 layout2->addWidget(m_checkbox_sound_notifications_enable);
235 #endif
236 }
237 {
238 QHBoxLayout *layout2 = new QHBoxLayout();
239 layout->addLayout(layout2);
240 layout2->addWidget(label_hotkey);
241 layout2->addWidget(m_checkbox_hotkey_ctrl);
242 layout2->addWidget(m_checkbox_hotkey_shift);
243 layout2->addWidget(m_checkbox_hotkey_alt);
244 layout2->addWidget(m_checkbox_hotkey_super);
245 layout2->addWidget(m_combobox_hotkey_key);
246 }
247 }
248 QSplitter *splitter_vertical = new QSplitter(Qt::Vertical, this);
249 {
250 QSplitter *splitter_horizontal = new QSplitter(Qt::Horizontal, splitter_vertical);
251 {
252 QGroupBox *groupbox_information = new QGroupBox(tr("Information"), splitter_horizontal);
253 {
254 QLabel *label_total_time = new QLabel(tr("Total time:"), groupbox_information);
255 m_label_info_total_time = new QLabel(groupbox_information);
256 QLabel *label_frame_rate_in = new QLabel(tr("FPS in:"), groupbox_information);
257 m_label_info_frame_rate_in = new QLabel(groupbox_information);
258 QLabel *label_frame_rate_out = new QLabel(tr("FPS out:"), groupbox_information);
259 m_label_info_frame_rate_out = new QLabel(groupbox_information);
260 QLabel *label_size_in = new QLabel(tr("Size in:"), groupbox_information);
261 m_label_info_size_in = new QLabel(groupbox_information);
262 QLabel *label_size_out = new QLabel(tr("Size out:"), groupbox_information);
263 m_label_info_size_out = new QLabel(groupbox_information);
264 QLabel *label_file_name = new QLabel(tr("File name:"), groupbox_information);
265 m_label_info_file_name = new ElidedLabel(QString(), Qt::ElideMiddle, groupbox_information);
266 m_label_info_file_name->setMinimumWidth(100);
267 QLabel *label_file_size = new QLabel(tr("File size:"), groupbox_information);
268 m_label_info_file_size = new QLabel(groupbox_information);
269 QLabel *label_bit_rate = new QLabel(tr("Bit rate:"), groupbox_information);
270 m_label_info_bit_rate = new QLabel(groupbox_information);
271 m_checkbox_show_recording_area = new QCheckBox(tr("Show recording area"), groupbox_information);
272 m_checkbox_show_recording_area->setToolTip(tr("When enabled, the recorded area is marked on the screen."));
273
274 connect(m_checkbox_show_recording_area, SIGNAL(clicked()), this, SLOT(OnUpdateRecordingFrame()));
275
276 QGridLayout *layout = new QGridLayout(groupbox_information);
277 layout->addWidget(label_total_time, 0, 0);
278 layout->addWidget(m_label_info_total_time, 0, 1);
279 layout->addWidget(label_frame_rate_in, 1, 0);
280 layout->addWidget(m_label_info_frame_rate_in, 1, 1);
281 layout->addWidget(label_frame_rate_out, 2, 0);
282 layout->addWidget(m_label_info_frame_rate_out, 2, 1);
283 layout->addWidget(label_size_in, 3, 0);
284 layout->addWidget(m_label_info_size_in, 3, 1);
285 layout->addWidget(label_size_out, 4, 0);
286 layout->addWidget(m_label_info_size_out, 4, 1);
287 layout->addWidget(label_file_name, 5, 0);
288 layout->addWidget(m_label_info_file_name, 5, 1);
289 layout->addWidget(label_file_size, 6, 0);
290 layout->addWidget(m_label_info_file_size, 6, 1);
291 layout->addWidget(label_bit_rate, 7, 0);
292 layout->addWidget(m_label_info_bit_rate, 7, 1);
293 layout->addWidget(m_checkbox_show_recording_area, 9, 0, 1, 2);
294 layout->setColumnStretch(1, 1);
295 layout->setRowStretch(8, 1);
296 }
297 QGroupBox *groupbox_preview = new QGroupBox(tr("Preview"), splitter_horizontal);
298 {
299 m_preview_page1 = new QWidget(groupbox_preview);
300 {
301 QLabel *label_preview_frame_rate = new QLabel(tr("Preview frame rate:"), m_preview_page1);
302 m_spinbox_preview_frame_rate = new QSpinBox(m_preview_page1);
303 m_spinbox_preview_frame_rate->setRange(1, 1000);
304 m_spinbox_preview_frame_rate->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
305 QLabel *label_preview_note = new QLabel(tr("Note: Previewing requires extra CPU time (especially at high frame rates)."), m_preview_page1);
306 label_preview_note->setWordWrap(true);
307 label_preview_note->setAlignment(Qt::AlignLeft | Qt::AlignTop);
308 label_preview_note->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::MinimumExpanding);
309
310 QGridLayout *layout = new QGridLayout(m_preview_page1);
311 layout->setMargin(0);
312 layout->addWidget(label_preview_frame_rate, 0, 0);
313 layout->addWidget(m_spinbox_preview_frame_rate, 0, 1);
314 layout->addWidget(label_preview_note, 1, 0, 1, 2);
315 }
316 m_preview_page2 = new QWidget(groupbox_preview);
317 {
318 m_video_previewer = new VideoPreviewer(m_preview_page2);
319 m_label_mic_icon = new QLabel(m_preview_page2);
320 m_label_mic_icon->setPixmap(g_icon_microphone.pixmap(24, 24));
321 m_audio_previewer = new AudioPreviewer(m_preview_page2);
322
323 QVBoxLayout *layout = new QVBoxLayout(m_preview_page2);
324 layout->setMargin(0);
325 layout->addWidget(m_video_previewer);
326 {
327 QHBoxLayout *layout2 = new QHBoxLayout();
328 layout->addLayout(layout2);
329 layout2->addStretch();
330 layout2->addWidget(m_label_mic_icon);
331 layout2->addWidget(m_audio_previewer);
332 layout2->addStretch();
333 }
334 }
335 m_pushbutton_preview_start_stop = new QPushButton(groupbox_preview);
336
337 connect(m_pushbutton_preview_start_stop, SIGNAL(clicked()), this, SLOT(OnPreviewStartStop()));
338
339 QVBoxLayout *layout = new QVBoxLayout(groupbox_preview);
340 {
341 m_stacked_layout_preview = new QStackedLayout();
342 layout->addLayout(m_stacked_layout_preview);
343 m_stacked_layout_preview->addWidget(m_preview_page1);
344 m_stacked_layout_preview->addWidget(m_preview_page2);
345 }
346 layout->addWidget(m_pushbutton_preview_start_stop);
347 }
348
349 splitter_horizontal->addWidget(groupbox_information);
350 splitter_horizontal->addWidget(groupbox_preview);
351 splitter_horizontal->setStretchFactor(0, 1);
352 splitter_horizontal->setStretchFactor(1, 3);
353 }
354 QGroupBox *groupbox_log = new QGroupBox(tr("Log"), splitter_vertical);
355 {
356 m_textedit_log = new QTextEditSmall(groupbox_log);
357 m_textedit_log->setReadOnly(true);
358
359 QVBoxLayout *layout = new QVBoxLayout(groupbox_log);
360 layout->addWidget(m_textedit_log);
361 }
362
363 splitter_vertical->addWidget(splitter_horizontal);
364 splitter_vertical->addWidget(groupbox_log);
365 splitter_vertical->setStretchFactor(0, 3);
366 splitter_vertical->setStretchFactor(1, 1);
367 }
368
369 QPushButton *button_cancel = new QPushButton(g_icon_cancel, tr("Cancel recording"), this);
370 QPushButton *button_save = new QPushButton(g_icon_save, tr("Save recording"), this);
371
372 if(CommandLineOptions::GetSysTray()) {
373 m_systray_icon = new QSystemTrayIcon(g_icon_ssr_idle, m_main_window);
374 QMenu *menu = new QMenu(m_main_window);
375 m_systray_action_start_pause = menu->addAction(QString(), this, SLOT(OnRecordStartPause()));
376 m_systray_action_start_pause->setIconVisibleInMenu(true);
377 m_systray_action_cancel = menu->addAction(g_icon_cancel, tr("Cancel recording"), this, SLOT(OnRecordCancel()));
378 m_systray_action_cancel->setIconVisibleInMenu(true);
379 m_systray_action_save = menu->addAction(g_icon_save, tr("Save recording"), this, SLOT(OnRecordSave()));
380 m_systray_action_save->setIconVisibleInMenu(true);
381 menu->addSeparator();
382 m_systray_action_show_hide = menu->addAction(QString(), m_main_window, SLOT(OnShowHide()));
383 m_systray_action_show_hide->setIconVisibleInMenu(true);
384 m_systray_action_quit = menu->addAction(g_icon_quit, tr("Quit"), m_main_window, SLOT(close()));
385 m_systray_action_quit->setIconVisibleInMenu(true);
386 m_systray_icon->setContextMenu(menu);
387 } else {
388 m_systray_icon = NULL;
389 }
390
391 connect(button_cancel, SIGNAL(clicked()), this, SLOT(OnRecordCancel()));
392 connect(button_save, SIGNAL(clicked()), this, SLOT(OnRecordSave()));
393 if(m_systray_icon != NULL)
394 connect(m_systray_icon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), m_main_window, SLOT(OnSysTrayActivated(QSystemTrayIcon::ActivationReason)));
395
396 QVBoxLayout *layout = new QVBoxLayout(this);
397 layout->addWidget(groupbox_recording);
398 layout->addWidget(splitter_vertical);
399 {
400 QHBoxLayout *layout2 = new QHBoxLayout();
401 layout->addLayout(layout2);
402 layout2->addWidget(button_cancel);
403 layout2->addWidget(button_save);
404 }
405
406 m_stdin_notifier = new QSocketNotifier(0, QSocketNotifier::Read, this);
407 connect(m_stdin_notifier, SIGNAL(activated(int)), this, SLOT(OnStdin()));
408
409 m_timer_schedule = new QTimer(this);
410 m_timer_schedule->setSingleShot(true);
411 m_timer_update_info = new QTimer(this);
412 connect(m_timer_schedule, SIGNAL(timeout()), this, SLOT(OnScheduleTimer()));
413 connect(m_timer_update_info, SIGNAL(timeout()), this, SLOT(OnUpdateInformation()));
414 connect(&m_hotkey_start_pause, SIGNAL(Triggered()), this, SLOT(OnRecordStartPause()), Qt::QueuedConnection);
415 connect(Logger::GetInstance(), SIGNAL(NewLine(Logger::enum_type,QString)), this, SLOT(OnNewLogLine(Logger::enum_type,QString)), Qt::QueuedConnection);
416
417 UpdateSysTray();
418 UpdateRecordButton();
419 UpdateSchedule();
420 UpdatePreview();
421 OnUpdateRecordingFrame();
422
423 if(m_systray_icon != NULL)
424 m_systray_icon->show();
425
426 }
427
~PageRecord()428 PageRecord::~PageRecord() {
429 StopPage(false);
430 }
431
ShouldBlockClose()432 bool PageRecord::ShouldBlockClose() {
433 if(m_output_manager != NULL) {
434 if(MessageBox(QMessageBox::Warning, this, MainWindow::WINDOW_CAPTION,
435 tr("You have not saved the current recording yet, if you quit now it will be lost.\n"
436 "Are you sure that you want to quit?"), BUTTON_YES | BUTTON_NO, BUTTON_YES) != BUTTON_YES) {
437 return true;
438 }
439 }
440 return false;
441 }
442
UpdateShowHide()443 void PageRecord::UpdateShowHide() {
444 if(m_systray_icon == NULL)
445 return;
446 if(m_main_window->isVisible()) {
447 m_systray_action_show_hide->setText(tr("Hide window"));
448 } else {
449 m_systray_action_show_hide->setText(tr("Show window"));
450 }
451 }
452
LoadSettings(QSettings * settings)453 void PageRecord::LoadSettings(QSettings *settings) {
454 SetHotkeyEnabled(settings->value("record/hotkey_enable", true).toBool());
455 SetHotkeyCtrlEnabled(settings->value("record/hotkey_ctrl", false).toBool());
456 SetHotkeyShiftEnabled(settings->value("record/hotkey_shift", false).toBool());
457 SetHotkeyAltEnabled(settings->value("record/hotkey_alt", false).toBool());
458 SetHotkeySuperEnabled(settings->value("record/hotkey_super", true).toBool());
459 SetHotkeyKey(settings->value("record/hotkey_key", 'r' - 'a').toUInt());
460 #if SSR_USE_ALSA
461 SetSoundNotificationsEnabled(settings->value("record/sound_notifications_enable", false).toBool());
462 #endif
463 SetShowRecordingArea(settings->value("record/show_recording_area", false).toBool());
464 SetPreviewFrameRate(settings->value("record/preview_frame_rate", 10).toUInt());
465 SetScheduleTimeZone(StringToEnum(settings->value("record/schedule_time_zone", QString()).toString(), SCHEDULE_TIME_ZONE_LOCAL));
466 unsigned int num_entries = clamp(settings->value("record/schedule_num_entries", 0).toUInt(), 0u, 1000u);
467 m_schedule_entries.clear();
468 m_schedule_entries.resize(num_entries);
469 Qt::TimeSpec spec = SCHEDULE_TIME_ZONE_TIMESPECS[GetScheduleTimeZone()];
470 for(unsigned int i = 0; i < num_entries; ++i) {
471 QString timestring = settings->value(QString("record/schedule_entry%1_time").arg(i), QString()).toString();
472 QString actionstring = settings->value(QString("record/schedule_entry%1_action").arg(i), QString()).toString();
473 m_schedule_entries[i].time = QDateTime::fromString(timestring, "yyyy-MM-dd hh:mm:ss");
474 m_schedule_entries[i].time.setTimeSpec(spec);
475 m_schedule_entries[i].action = StringToEnum(actionstring, SCHEDULE_ACTION_START);
476 }
477 OnUpdateHotkeyFields();
478 #if SSR_USE_ALSA
479 OnUpdateSoundNotifications();
480 #endif
481 OnUpdateRecordingFrame();
482 }
483
SaveSettings(QSettings * settings)484 void PageRecord::SaveSettings(QSettings *settings) {
485 settings->setValue("record/hotkey_enable", IsHotkeyEnabled());
486 settings->setValue("record/hotkey_ctrl", IsHotkeyCtrlEnabled());
487 settings->setValue("record/hotkey_shift", IsHotkeyShiftEnabled());
488 settings->setValue("record/hotkey_alt", IsHotkeyAltEnabled());
489 settings->setValue("record/hotkey_super", IsHotkeySuperEnabled());
490 settings->setValue("record/hotkey_key", GetHotkeyKey());
491 #if SSR_USE_ALSA
492 settings->setValue("record/sound_notifications_enable", AreSoundNotificationsEnabled());
493 #endif
494 settings->setValue("record/show_recording_area", GetShowRecordingArea());
495 settings->setValue("record/preview_frame_rate", GetPreviewFrameRate());
496 settings->setValue("record/schedule_time_zone", EnumToString(GetScheduleTimeZone()));
497 settings->setValue("record/schedule_num_entries", (unsigned int) m_schedule_entries.size());
498 for(unsigned int i = 0; i < m_schedule_entries.size(); ++i) {
499 settings->setValue(QString("record/schedule_entry%1_time").arg(i), m_schedule_entries[i].time.toString("yyyy-MM-dd hh:mm:ss"));
500 settings->setValue(QString("record/schedule_entry%1_action").arg(i), EnumToString(m_schedule_entries[i].action));
501 }
502 }
503
TryStartPage()504 bool PageRecord::TryStartPage() {
505 if(m_page_started)
506 return true;
507 if(!m_main_window->Validate())
508 return false;
509 m_main_window->GoPageRecord();
510 assert(m_page_started);
511 return true;
512 }
513
StartPage()514 void PageRecord::StartPage() {
515
516 if(m_page_started)
517 return;
518
519 assert(!m_input_started);
520 assert(!m_output_started);
521
522 // save the settings in case libav/ffmpeg decides to kill the process
523 m_main_window->SaveSettings();
524
525 // clear the log
526 m_textedit_log->clear();
527
528 // clear the preview
529 if(m_previewing) {
530 m_video_previewer->Reset();
531 m_audio_previewer->Reset();
532 }
533
534 PageInput *page_input = m_main_window->GetPageInput();
535 PageOutput *page_output = m_main_window->GetPageOutput();
536
537 // get the video input settings
538 m_video_area = page_input->GetVideoArea();
539 m_video_area_follow_fullscreen = page_input->GetVideoAreaFollowFullscreen();
540 #if SSR_USE_V4L2
541 m_v4l2_device = page_input->GetVideoV4L2Device();
542 #endif
543 m_video_x = page_input->GetVideoX();
544 m_video_y = page_input->GetVideoY();
545 #if SSR_USE_OPENGL_RECORDING
546 if(m_video_area == PageInput::VIDEO_AREA_GLINJECT) {
547 m_video_in_width = 0;
548 m_video_in_height = 0;
549 } else {
550 #else
551 {
552 #endif
553 m_video_in_width = page_input->GetVideoW();
554 m_video_in_height = page_input->GetVideoH();
555 }
556 m_video_in_width = page_input->GetVideoW();
557 m_video_in_height = page_input->GetVideoH();
558 m_video_frame_rate = page_input->GetVideoFrameRate();
559 m_video_scaling = page_input->GetVideoScalingEnabled();
560 m_video_scaled_width = page_input->GetVideoScaledW();
561 m_video_scaled_height = page_input->GetVideoScaledH();
562 m_video_record_cursor = page_input->GetVideoRecordCursor();
563
564 // get the audio input settings
565 m_audio_enabled = page_input->GetAudioEnabled();
566 m_audio_channels = 2;
567 m_audio_sample_rate = 48000;
568 m_audio_backend = page_input->GetAudioBackend();
569 #if SSR_USE_ALSA
570 m_alsa_source = page_input->GetALSASourceName();
571 #endif
572 #if SSR_USE_PULSEAUDIO
573 m_pulseaudio_source = page_input->GetPulseAudioSourceName();
574 #endif
575 #if SSR_USE_JACK
576 bool jack_connect_system_capture = page_input->GetJackConnectSystemCapture();
577 bool jack_connect_system_playback = page_input->GetJackConnectSystemPlayback();
578 #endif
579
580 // override sample rate for problematic cases (these are hard-coded for now)
581 if(page_output->GetContainer() == PageOutput::CONTAINER_OTHER && page_output->GetContainerAVName() == "flv") {
582 m_audio_sample_rate = 44100;
583 }
584
585 #if SSR_USE_OPENGL_RECORDING
586 // get the glinject settings
587 QString glinject_channel = page_input->GetGLInjectChannel();
588 bool glinject_relax_permissions = page_input->GetGLInjectRelaxPermissions();
589 QString glinject_command = page_input->GetGLInjectCommand();
590 QString glinject_working_directory = page_input->GetGLInjectWorkingDirectory();
591 bool glinject_auto_launch = page_input->GetGLInjectAutoLaunch();
592 bool glinject_limit_fps = page_input->GetGLInjectLimitFPS();
593 #endif
594
595 // get file settings
596 m_file_base = page_output->GetFile();
597 m_file_protocol = page_output->GetFileProtocol();
598 m_separate_files = page_output->GetSeparateFiles();
599 m_add_timestamp = page_output->GetAddTimestamp();
600
601 // get the output settings
602 m_output_settings.file = QString(); // will be set later
603 m_output_settings.container_avname = page_output->GetContainerAVName();
604
605 m_output_settings.video_codec_avname = page_output->GetVideoCodecAVName();
606 m_output_settings.video_kbit_rate = page_output->GetVideoKBitRate();
607 m_output_settings.video_options.clear();
608 m_output_settings.video_width = 0;
609 m_output_settings.video_height = 0;
610 m_output_settings.video_frame_rate = m_video_frame_rate;
611 m_output_settings.video_allow_frame_skipping = page_output->GetVideoAllowFrameSkipping();
612
613 m_output_settings.audio_codec_avname = (m_audio_enabled)? page_output->GetAudioCodecAVName() : QString();
614 m_output_settings.audio_kbit_rate = page_output->GetAudioKBitRate();
615 m_output_settings.audio_options.clear();
616 m_output_settings.audio_channels = m_audio_channels;
617 m_output_settings.audio_sample_rate = m_audio_sample_rate;
618
619 // some codec-specific things
620 // you can get more information about all these options by running 'ffmpeg -h' or 'avconv -h' from a terminal
621 switch(page_output->GetVideoCodec()) {
622 case PageOutput::VIDEO_CODEC_H264: {
623 // x264 has a 'constant quality' mode, where the bit rate is simply set to whatever is needed to keep a certain quality. The quality is set
624 // with the 'crf' option. 'preset' changes the encoding speed (and hence the efficiency of the compression) but doesn't really influence the quality,
625 // which is great because it means you don't have to experiment with different bit rates and different speeds to get good results.
626 m_output_settings.video_options.push_back(std::make_pair(QString("crf"), QString::number(page_output->GetH264CRF())));
627 m_output_settings.video_options.push_back(std::make_pair(QString("preset"), EnumToString(page_output->GetH264Preset())));
628 break;
629 }
630 case PageOutput::VIDEO_CODEC_VP8: {
631 // The names of there parameters are very unintuitive. The two options we care about (because they change the speed) are 'deadline' and 'cpu-used'.
632 // 'deadline=best' is unusably slow. 'deadline=good' is the normal setting, it tells the encoder to use the speed set with 'cpu-used'. Higher
633 // numbers will use *less* CPU, confusingly, so a higher number is faster. I haven't done much testing with 'realtime' so I'm not sure if it's a good idea here.
634 // It sounds useful, but I think it will use so much CPU that it will slow down the program that is being recorded.
635 m_output_settings.video_options.push_back(std::make_pair(QString("deadline"), QString("good")));
636 m_output_settings.video_options.push_back(std::make_pair(QString("cpu-used"), QString::number(page_output->GetVP8CPUUsed())));
637 break;
638 }
639 case PageOutput::VIDEO_CODEC_OTHER: {
640 m_output_settings.video_options = GetOptionsFromString(page_output->GetVideoOptions());
641 break;
642 }
643 default: break; // to keep GCC happy
644 }
645 switch(page_output->GetAudioCodec()) {
646 case PageOutput::AUDIO_CODEC_OTHER: {
647 m_output_settings.audio_options = GetOptionsFromString(page_output->GetAudioOptions());
648 break;
649 }
650 default: break; // to keep GCC happy
651 }
652
653 // only show the recording frame option when using a fixed rectangle
654 GroupVisible({m_checkbox_show_recording_area}, (m_video_area == PageInput::VIDEO_AREA_FIXED));
655
656 // hide the audio previewer if there is no audio
657 GroupVisible({m_label_mic_icon, m_audio_previewer}, m_audio_enabled);
658
659 Logger::LogInfo("[PageRecord::StartPage] " + tr("Starting page ..."));
660
661
662 try {
663
664 #if SSR_USE_OPENGL_RECORDING
665 // for OpenGL recording, create the input now
666 if(m_video_area == PageInput::VIDEO_AREA_GLINJECT) {
667 if(glinject_auto_launch)
668 GLInjectInput::LaunchApplication(glinject_channel, glinject_relax_permissions, glinject_command, glinject_working_directory);
669 m_gl_inject_input.reset(new GLInjectInput(glinject_channel, glinject_relax_permissions, m_video_record_cursor, glinject_limit_fps, m_video_frame_rate));
670 }
671 #endif
672
673 #if SSR_USE_JACK
674 if(m_audio_enabled) {
675 // for JACK, start the input now
676 if(m_audio_backend == PageInput::AUDIO_BACKEND_JACK)
677 m_jack_input.reset(new JACKInput(jack_connect_system_capture, jack_connect_system_playback));
678 }
679 #endif
680
681 } catch(...) {
682 Logger::LogError("[PageRecord::StartPage] " + tr("Error: Something went wrong during initialization."));
683 #if SSR_USE_OPENGL_RECORDING
684 m_gl_inject_input.reset();
685 #endif
686 #if SSR_USE_JACK
687 m_jack_input.reset();
688 #endif
689 }
690
691 Logger::LogInfo("[PageRecord::StartPage] " + tr("Started page."));
692
693 m_page_started = true;
694 m_recorded_something = false;
695 m_wait_saving = false;
696 m_error_occurred = false;
697 UpdateSysTray();
698 #if SSR_USE_ALSA
699 OnUpdateSoundNotifications();
700 #endif
701
702 UpdateInput();
703 OnUpdateRecordingFrame();
704
705 OnUpdateInformation();
706 m_timer_update_info->start(1000);
707
708 m_schedule_active = false;
709 UpdateSchedule();
710
711 }
712
713 void PageRecord::StopPage(bool save) {
714
715 if(!m_page_started)
716 return;
717
718 m_schedule_active = false;
719 UpdateSchedule();
720
721 StopOutput(true);
722 StopInput();
723
724 Logger::LogInfo("[PageRecord::StopPage] " + tr("Stopping page ..."));
725
726 if(m_output_manager != NULL) {
727
728 // stop the output
729 if(save)
730 FinishOutput();
731 m_output_manager.reset();
732
733 // delete the file if it isn't needed
734 if(!save && m_file_protocol.isNull()) {
735 if(QFileInfo(m_output_settings.file).exists())
736 QFile(m_output_settings.file).remove();
737 }
738
739 }
740
741 #if SSR_USE_OPENGL_RECORDING
742 // stop GLInject input
743 m_gl_inject_input.reset();
744 #endif
745
746 #if SSR_USE_JACK
747 // stop JACK input
748 m_jack_input.reset();
749 #endif
750
751 Logger::LogInfo("[PageRecord::StopPage] " + tr("Stopped page."));
752
753 m_page_started = false;
754 UpdateSysTray();
755 #if SSR_USE_ALSA
756 OnUpdateSoundNotifications();
757 #endif
758 OnUpdateRecordingFrame();
759
760 m_timer_update_info->stop();
761 OnUpdateInformation();
762
763 }
764
765 void PageRecord::StartOutput() {
766 assert(m_page_started);
767
768 if(m_output_started)
769 return;
770
771 #if SSR_USE_ALSA
772 if(m_simple_synth != NULL) {
773 m_simple_synth->PlaySequence(SEQUENCE_RECORD_START.data(), SEQUENCE_RECORD_START.size());
774 usleep(200000);
775 }
776 #endif
777
778 try {
779
780 Logger::LogInfo("[PageRecord::StartOutput] " + tr("Starting output ..."));
781
782 if(m_output_manager == NULL) {
783
784 // set the file name
785 m_output_settings.file = GetNewSegmentFile(m_file_base, m_add_timestamp);
786
787 // for X11 recording, update the video size (if possible)
788 if(m_x11_input != NULL)
789 m_x11_input->GetCurrentSize(&m_video_in_width, &m_video_in_height);
790
791 #if SSR_USE_OPENGL_RECORDING
792 // for OpenGL recording, detect the video size
793 if(m_video_area == PageInput::VIDEO_AREA_GLINJECT && !m_video_scaling) {
794 if(m_gl_inject_input == NULL) {
795 Logger::LogError("[PageRecord::StartOutput] " + tr("Error: Could not get the size of the OpenGL application because the GLInject input has not been created."));
796 throw GLInjectException();
797 }
798 m_gl_inject_input->GetCurrentSize(&m_video_in_width, &m_video_in_height);
799 if(m_video_in_width == 0 && m_video_in_height == 0) {
800 Logger::LogError("[PageRecord::StartOutput] " + tr("Error: Could not get the size of the OpenGL application. Either the "
801 "application wasn't started correctly, or the application hasn't created an OpenGL window yet. If "
802 "you want to start recording before starting the application, you have to enable scaling and enter "
803 "the video size manually."));
804 throw GLInjectException();
805 }
806 }
807 #endif
808
809 // calculate the output width and height
810 if(m_video_scaling) {
811 // Only even width and height is allowed because some pixel formats (e.g. YUV420) require this.
812 m_output_settings.video_width = m_video_scaled_width / 2 * 2;
813 m_output_settings.video_height = m_video_scaled_height / 2 * 2;
814 #if SSR_USE_OPENGL_RECORDING
815 } else if(m_video_area == PageInput::VIDEO_AREA_GLINJECT) {
816 // The input size is the size of the OpenGL application and can't be changed. The output size is set to the current size of the application.
817 m_output_settings.video_width = m_video_in_width / 2 * 2;
818 m_output_settings.video_height = m_video_in_height / 2 * 2;
819 #endif
820 } else {
821 // If the user did not explicitly select scaling, then don't force scaling just because the recording area is one pixel too large.
822 // One missing row/column of pixels is probably better than a blurry video (and scaling is SLOW).
823 m_video_in_width = m_video_in_width / 2 * 2;
824 m_video_in_height = m_video_in_height / 2 * 2;
825 m_output_settings.video_width = m_video_in_width;
826 m_output_settings.video_height = m_video_in_height;
827 }
828
829 // start the output
830 m_output_manager.reset(new OutputManager(m_output_settings));
831
832 } else {
833
834 // start a new segment
835 m_output_manager->GetSynchronizer()->NewSegment();
836
837 }
838
839 Logger::LogInfo("[PageRecord::StartOutput] " + tr("Started output."));
840
841 m_output_started = true;
842 m_recorded_something = true;
843 UpdateSysTray();
844 UpdateRecordButton();
845 UpdateInput();
846 OnUpdateRecordingFrame();
847
848 } catch(...) {
849 Logger::LogError("[PageRecord::StartOutput] " + tr("Error: Something went wrong during initialization."));
850 }
851
852 }
853
854 void PageRecord::StopOutput(bool final) {
855 assert(m_page_started);
856
857 if(!m_output_started)
858 return;
859
860 Logger::LogInfo("[PageRecord::StopOutput] " + tr("Stopping output ..."));
861
862 // if final, then StopPage will stop the output (and delete the file if needed)
863 if(m_separate_files && !final) {
864
865 // stop the output
866 FinishOutput();
867 m_output_manager.reset();
868
869 // change the file name
870 m_output_settings.file = QString();
871
872 // reset the output video size
873 m_output_settings.video_width = 0;
874 m_output_settings.video_height = 0;
875
876 }
877
878 Logger::LogInfo("[PageRecord::StopOutput] " + tr("Stopped output."));
879
880 #if SSR_USE_ALSA
881 // if final, don't play the notification (it would get interrupted anyway)
882 if(m_simple_synth != NULL && !final)
883 m_simple_synth->PlaySequence(SEQUENCE_RECORD_STOP.data(), SEQUENCE_RECORD_STOP.size());
884 #endif
885
886 m_output_started = false;
887 UpdateSysTray();
888 UpdateRecordButton();
889 UpdateInput();
890 OnUpdateRecordingFrame();
891
892 }
893
894 void PageRecord::StartInput() {
895 assert(m_page_started);
896
897 if(m_input_started)
898 return;
899
900 assert(m_x11_input == NULL);
901 #if SSR_USE_ALSA
902 assert(m_alsa_input == NULL);
903 #endif
904 #if SSR_USE_PULSEAUDIO
905 assert(m_pulseaudio_input == NULL);
906 #endif
907
908 try {
909
910 Logger::LogInfo("[PageRecord::StartInput] " + tr("Starting input ..."));
911
912 // start the video input
913 if(m_video_area == PageInput::VIDEO_AREA_SCREEN || m_video_area == PageInput::VIDEO_AREA_FIXED || m_video_area == PageInput::VIDEO_AREA_CURSOR) {
914 m_x11_input.reset(new X11Input(m_video_x, m_video_y, m_video_in_width, m_video_in_height, m_video_record_cursor,
915 m_video_area == PageInput::VIDEO_AREA_CURSOR, m_video_area_follow_fullscreen));
916 connect(m_x11_input.get(), SIGNAL(CurrentRectangleChanged()), this, SLOT(OnUpdateRecordingFrame()), Qt::QueuedConnection);
917 }
918 #if SSR_USE_OPENGL_RECORDING
919 if(m_video_area == PageInput::VIDEO_AREA_GLINJECT) {
920 if(m_gl_inject_input == NULL) {
921 Logger::LogError("[PageRecord::StartInput] " + tr("Error: Could not start the GLInject input because it has not been created."));
922 throw GLInjectException();
923 }
924 m_gl_inject_input->SetCapturing(true);
925 }
926 #endif
927 #if SSR_USE_V4L2
928 if(m_video_area == PageInput::VIDEO_AREA_V4L2) {
929 m_v4l2_input.reset(new V4L2Input(m_v4l2_device, m_video_in_width, m_video_in_height));
930 m_v4l2_input->GetCurrentSize(&m_video_in_width, &m_video_in_height);
931 }
932 #endif
933
934 // start the audio input
935 if(m_audio_enabled) {
936 #if SSR_USE_ALSA
937 if(m_audio_backend == PageInput::AUDIO_BACKEND_ALSA)
938 m_alsa_input.reset(new ALSAInput(m_alsa_source, m_audio_sample_rate));
939 #endif
940 #if SSR_USE_PULSEAUDIO
941 if(m_audio_backend == PageInput::AUDIO_BACKEND_PULSEAUDIO)
942 m_pulseaudio_input.reset(new PulseAudioInput(m_pulseaudio_source, m_audio_sample_rate));
943 #endif
944 // JACK was started when the page was started
945 }
946
947 Logger::LogInfo("[PageRecord::StartInput] " + tr("Started input."));
948
949 m_input_started = true;
950
951 } catch(...) {
952 Logger::LogError("[PageRecord::StartInput] " + tr("Error: Something went wrong during initialization."));
953 m_x11_input.reset();
954 #if SSR_USE_OPENGL_RECORDING
955 if(m_gl_inject_input != NULL)
956 m_gl_inject_input->SetCapturing(false);
957 #endif
958 #if SSR_USE_V4L2
959 m_v4l2_input.reset();
960 #endif
961 #if SSR_USE_ALSA
962 m_alsa_input.reset();
963 #endif
964 #if SSR_USE_PULSEAUDIO
965 m_pulseaudio_input.reset();
966 #endif
967 // JACK shouldn't stop until the page stops
968 return;
969 }
970
971 }
972
973 void PageRecord::StopInput() {
974 assert(m_page_started);
975
976 if(!m_input_started)
977 return;
978
979 Logger::LogInfo("[PageRecord::StopInput] " + tr("Stopping input ..."));
980
981 m_x11_input.reset();
982 #if SSR_USE_OPENGL_RECORDING
983 if(m_gl_inject_input != NULL)
984 m_gl_inject_input->SetCapturing(false);
985 #endif
986 #if SSR_USE_V4L2
987 m_v4l2_input.reset();
988 #endif
989 #if SSR_USE_ALSA
990 m_alsa_input.reset();
991 #endif
992 #if SSR_USE_PULSEAUDIO
993 m_pulseaudio_input.reset();
994 #endif
995 // JACK shouldn't stop until the page stops
996
997 Logger::LogInfo("[PageRecord::StopInput] " + tr("Stopped input."));
998
999 m_input_started = false;
1000
1001 }
1002
1003 void PageRecord::FinishOutput() {
1004 assert(m_output_manager != NULL);
1005
1006 // tell the output manager to finish
1007 m_output_manager->Finish();
1008
1009 // wait until it has actually finished
1010 m_wait_saving = true;
1011 //unsigned int frames_left = m_output_manager->GetVideoEncoder()->GetFrameLatency();
1012 unsigned int frames_done = 0, frames_total = 0;
1013 QProgressDialog dialog(tr("Encoding remaining data ..."), QString(), 0, frames_total, this);
1014 dialog.setWindowTitle(MainWindow::WINDOW_CAPTION);
1015 dialog.setWindowModality(Qt::WindowModal);
1016 dialog.setCancelButton(NULL);
1017 dialog.setMinimumDuration(500);
1018 while(!m_output_manager->IsFinished()) {
1019 unsigned int frames = m_output_manager->GetTotalQueuedFrameCount();
1020 if(frames > frames_total)
1021 frames_total = frames;
1022 if(frames_total - frames > frames_done)
1023 frames_done = frames_total - frames;
1024 //qDebug() << "frames_done" << frames_done << "frames_total" << frames_total << "frames" << frames;
1025 dialog.setMaximum(frames_total);
1026 dialog.setValue(frames_done);
1027 usleep(20000);
1028 }
1029 m_wait_saving = false;
1030
1031 }
1032
1033 void PageRecord::UpdateInput() {
1034 assert(m_page_started);
1035
1036 if(m_output_started || m_previewing) {
1037 StartInput();
1038 } else {
1039 StopInput();
1040 }
1041
1042 // get sources
1043 VideoSource *video_source = NULL;
1044 AudioSource *audio_source = NULL;
1045 if(m_video_area == PageInput::VIDEO_AREA_SCREEN || m_video_area == PageInput::VIDEO_AREA_FIXED|| m_video_area == PageInput::VIDEO_AREA_CURSOR)
1046 video_source = m_x11_input.get();
1047 #if SSR_USE_OPENGL_RECORDING
1048 if(m_video_area == PageInput::VIDEO_AREA_GLINJECT)
1049 video_source = m_gl_inject_input.get();
1050 #endif
1051 #if SSR_USE_V4L2
1052 if(m_video_area == PageInput::VIDEO_AREA_V4L2)
1053 video_source = m_v4l2_input.get();
1054 #endif
1055 if(m_audio_enabled) {
1056 #if SSR_USE_ALSA
1057 if(m_audio_backend == PageInput::AUDIO_BACKEND_ALSA)
1058 audio_source = m_alsa_input.get();
1059 #endif
1060 #if SSR_USE_PULSEAUDIO
1061 if(m_audio_backend == PageInput::AUDIO_BACKEND_PULSEAUDIO)
1062 audio_source = m_pulseaudio_input.get();
1063 #endif
1064 #if SSR_USE_JACK
1065 if(m_audio_backend == PageInput::AUDIO_BACKEND_JACK)
1066 audio_source = m_jack_input.get();
1067 #endif
1068 }
1069
1070 // connect sinks
1071 if(m_output_manager != NULL) {
1072 if(m_output_started) {
1073 m_output_manager->GetSynchronizer()->ConnectVideoSource(video_source, PRIORITY_RECORD);
1074 m_output_manager->GetSynchronizer()->ConnectAudioSource(audio_source, PRIORITY_RECORD);
1075 } else {
1076 m_output_manager->GetSynchronizer()->ConnectVideoSource(NULL);
1077 m_output_manager->GetSynchronizer()->ConnectAudioSource(NULL);
1078 }
1079 }
1080 if(m_previewing) {
1081 m_video_previewer->ConnectVideoSource(video_source, PRIORITY_PREVIEW);
1082 m_audio_previewer->ConnectAudioSource(audio_source, PRIORITY_PREVIEW);
1083 } else {
1084 m_video_previewer->ConnectVideoSource(NULL);
1085 m_audio_previewer->ConnectAudioSource(NULL);
1086 }
1087
1088 }
1089
1090 void PageRecord::UpdateSysTray() {
1091 if(m_systray_icon == NULL)
1092 return;
1093 GroupEnabled({m_systray_action_cancel, m_systray_action_save}, m_page_started);
1094 if(m_page_started) {
1095 if(m_error_occurred) {
1096 m_systray_icon->setIcon(g_icon_ssr_error);
1097 } else if(m_output_started) {
1098 m_systray_icon->setIcon(g_icon_ssr_recording);
1099 } else {
1100 m_systray_icon->setIcon(g_icon_ssr_paused);
1101 }
1102 } else {
1103 m_systray_icon->setIcon(g_icon_ssr_idle);
1104 }
1105 if(m_page_started && m_output_started) {
1106 m_systray_action_start_pause->setIcon(g_icon_pause);
1107 m_systray_action_start_pause->setText(tr("Pause recording"));
1108 } else {
1109 m_systray_action_start_pause->setIcon(g_icon_record);
1110 m_systray_action_start_pause->setText(tr("Start recording"));
1111 }
1112 }
1113
1114 void PageRecord::UpdateRecordButton() {
1115 if(m_output_started) {
1116 m_pushbutton_record->setIcon(g_icon_pause);
1117 m_pushbutton_record->setText(tr("Pause recording"));
1118 } else {
1119 m_pushbutton_record->setIcon(g_icon_record);
1120 m_pushbutton_record->setText(tr("Start recording"));
1121 }
1122 }
1123
1124 void PageRecord::UpdateSchedule() {
1125 if(!m_page_started)
1126 return;
1127 if(m_schedule_active) {
1128 m_pushbutton_schedule_activate->setText(tr("Deactivate schedule"));
1129 m_schedule_position = 0;
1130 QDateTime now = QDateTime::currentDateTimeUtc();
1131 while(m_schedule_position < m_schedule_entries.size()) {
1132 ScheduleEntry &entry = m_schedule_entries[m_schedule_position];
1133 if(now < entry.time)
1134 break;
1135 ++m_schedule_position;
1136 }
1137 } else {
1138 m_pushbutton_schedule_activate->setText(tr("Activate schedule"));
1139 }
1140 OnScheduleTimer();
1141 }
1142
1143 void PageRecord::UpdatePreview() {
1144 if(m_previewing) {
1145 m_video_previewer->SetFrameRate(GetPreviewFrameRate());
1146 m_stacked_layout_preview->setCurrentWidget(m_preview_page2);
1147 m_pushbutton_preview_start_stop->setText(tr("Stop preview"));
1148 } else {
1149 m_stacked_layout_preview->setCurrentWidget(m_preview_page1);
1150 m_pushbutton_preview_start_stop->setText(tr("Start preview"));
1151 }
1152 }
1153
1154 QString PageRecord::ReadStdinCommand() {
1155 for(int i = 0; i < m_stdin_buffer.size(); ++i) {
1156 if(m_stdin_buffer[i] == '\n') {
1157 QString command = QString::fromUtf8(m_stdin_buffer.data(), i);
1158 m_stdin_buffer = QByteArray(m_stdin_buffer.data() + i + 1, m_stdin_buffer.size() - i - 1);
1159 return command;
1160 }
1161 }
1162 return QString();
1163 }
1164
1165 void PageRecord::OnUpdateHotkeyFields() {
1166 bool enabled = IsHotkeyEnabled();
1167 GroupEnabled({m_checkbox_hotkey_ctrl, m_checkbox_hotkey_shift, m_checkbox_hotkey_alt, m_checkbox_hotkey_super, m_combobox_hotkey_key}, enabled);
1168 OnUpdateHotkey();
1169 }
1170
1171 void PageRecord::OnUpdateHotkey() {
1172 if(IsHotkeyEnabled()) {
1173 unsigned int modifiers = 0;
1174 if(IsHotkeyCtrlEnabled()) modifiers |= ControlMask;
1175 if(IsHotkeyShiftEnabled()) modifiers |= ShiftMask;
1176 if(IsHotkeyAltEnabled()) modifiers |= Mod1Mask;
1177 if(IsHotkeySuperEnabled()) modifiers |= Mod4Mask;
1178 m_hotkey_start_pause.Bind(XK_A + GetHotkeyKey(), modifiers);
1179 } else {
1180 m_hotkey_start_pause.Unbind();
1181 }
1182 }
1183
1184 #if SSR_USE_ALSA
1185 void PageRecord::OnUpdateSoundNotifications() {
1186 if(m_page_started && AreSoundNotificationsEnabled()) {
1187 if(m_simple_synth == NULL) {
1188 try {
1189 m_simple_synth.reset(new SimpleSynth("default", 48000));
1190 } catch(...) {
1191 Logger::LogError("[PageRecord::OnUpdateSoundNotifications] " + tr("Error: Something went wrong while creating the synth."));
1192 }
1193 }
1194 } else {
1195 m_simple_synth.reset();
1196 }
1197 }
1198 #endif
1199
1200 void PageRecord::OnUpdateRecordingFrame() {
1201 if(m_page_started && m_video_area == PageInput::VIDEO_AREA_FIXED && GetShowRecordingArea()) {
1202 if(m_recording_frame == NULL)
1203 m_recording_frame.reset(new RecordingFrameWindow(this, true));
1204 if(m_x11_input == NULL) {
1205 m_recording_frame->SetRectangle(QRect(m_video_x, m_video_y, m_video_in_width, m_video_in_height));
1206 } else {
1207 unsigned int x, y, width, height;
1208 m_x11_input->GetCurrentRectangle(&x, &y, &width, &height);
1209 m_recording_frame->SetRectangle(QRect(x, y, width, height));
1210 }
1211 } else {
1212 m_recording_frame.reset();
1213 }
1214 }
1215
1216 void PageRecord::OnRecordStart() {
1217 if(m_main_window->IsBusy())
1218 return;
1219 if(!TryStartPage())
1220 return;
1221 if(m_wait_saving)
1222 return;
1223 if(!m_output_started)
1224 StartOutput();
1225 }
1226
1227 void PageRecord::OnRecordPause() {
1228 if(m_main_window->IsBusy())
1229 return;
1230 if(!m_page_started)
1231 return;
1232 if(m_wait_saving)
1233 return;
1234 if(m_output_started)
1235 StopOutput(false);
1236 }
1237
1238 void PageRecord::OnRecordStartPause() {
1239 if(m_page_started && m_output_started) {
1240 OnRecordPause();
1241 } else {
1242 OnRecordStart();
1243 }
1244 }
1245
1246
1247 void PageRecord::OnRecordCancel(bool confirm) {
1248 if(m_main_window->IsBusy())
1249 return;
1250 if(!m_page_started)
1251 return;
1252 if(m_wait_saving)
1253 return;
1254 if(m_output_manager != NULL && confirm) {
1255 if(MessageBox(QMessageBox::Warning, this, MainWindow::WINDOW_CAPTION, tr("Are you sure that you want to cancel this recording?"),
1256 BUTTON_YES | BUTTON_NO, BUTTON_YES) != BUTTON_YES) {
1257 return;
1258 }
1259 }
1260 StopPage(false);
1261 m_main_window->GoPageOutput();
1262 }
1263
1264 void PageRecord::OnRecordSave(bool confirm) {
1265 if(m_main_window->IsBusy())
1266 return;
1267 if(!m_page_started)
1268 return;
1269 if(m_wait_saving)
1270 return;
1271 if(!m_recorded_something && confirm) {
1272 MessageBox(QMessageBox::Information, this, MainWindow::WINDOW_CAPTION, tr("You haven't recorded anything, there is nothing to save."),
1273 BUTTON_OK, BUTTON_OK);
1274 return;
1275 }
1276 StopPage(true);
1277 m_main_window->GoPageDone();
1278 }
1279
1280 void PageRecord::OnScheduleTimer() {
1281 if(!m_page_started)
1282 return;
1283 if(m_schedule_active) {
1284 QDateTime now = QDateTime::currentDateTimeUtc();
1285 while(m_schedule_position < m_schedule_entries.size()) {
1286 ScheduleEntry &entry = m_schedule_entries[m_schedule_position];
1287 if(now < entry.time)
1288 break;
1289 Logger::LogInfo("[PageRecord::OnScheduleTimer] " + tr("Triggering scheduled action '%1' ...").arg(EnumToString(entry.action)));
1290 switch(entry.action) {
1291 case SCHEDULE_ACTION_START: OnRecordStart(); break;
1292 case SCHEDULE_ACTION_PAUSE: OnRecordPause(); break;
1293 default: break; // to keep GCC happy
1294 }
1295 ++m_schedule_position;
1296 }
1297 if(m_schedule_position < m_schedule_entries.size()) {
1298 ScheduleEntry &entry = m_schedule_entries[m_schedule_position];
1299 int64_t msec = now.msecsTo(entry.time);
1300 m_label_schedule_status->setText(tr("Schedule: %1 in %2").arg(SCHEDULE_ACTION_TEXT[entry.action]).arg(ReadableTime(msec * 1000)));
1301 if(msec < 1000) {
1302 m_timer_schedule->start(msec);
1303 } else {
1304 m_timer_schedule->start((msec - 100) % 1000 + 100);
1305 }
1306 } else {
1307 m_label_schedule_status->setText(tr("Schedule: (none)"));
1308 m_timer_schedule->stop();
1309 }
1310 } else {
1311 m_label_schedule_status->setText(tr("Schedule: (inactive)"));
1312 m_timer_schedule->stop();
1313 }
1314 }
1315
1316 void PageRecord::OnScheduleActivate() {
1317 if(m_main_window->IsBusy())
1318 return;
1319 if(!TryStartPage())
1320 return;
1321 if(!m_schedule_active) {
1322 m_schedule_active = true;
1323 UpdateSchedule();
1324 }
1325 }
1326
1327 void PageRecord::OnScheduleDeactivate() {
1328 if(m_main_window->IsBusy())
1329 return;
1330 if(!m_page_started)
1331 return;
1332 if(m_schedule_active) {
1333 m_schedule_active = false;
1334 UpdateSchedule();
1335 }
1336 }
1337
1338 void PageRecord::OnScheduleActivateDeactivate() {
1339 if(m_page_started && m_schedule_active) {
1340 OnScheduleDeactivate();
1341 } else {
1342 OnScheduleActivate();
1343 }
1344 }
1345
1346 void PageRecord::OnScheduleEdit() {
1347 DialogRecordSchedule dialog(this);
1348 dialog.exec();
1349 UpdateSchedule();
1350 }
1351
1352 void PageRecord::OnPreviewStartStop() {
1353 if(!m_page_started)
1354 return;
1355 if(m_wait_saving)
1356 return;
1357 m_previewing = !m_previewing;
1358 if(m_previewing) {
1359 m_video_previewer->Reset();
1360 m_audio_previewer->Reset();
1361 }
1362 UpdatePreview();
1363 UpdateInput();
1364 OnUpdateRecordingFrame();
1365 }
1366
1367 void PageRecord::OnStdin() {
1368
1369 // get available length
1370 int len, res;
1371 do {
1372 res = ioctl(0, FIONREAD, &len);
1373 } while(res == -1 && errno == EINTR);
1374 if(res == -1) {
1375 Logger::LogError("[PageRecord::OnStdin] " + tr("Standard input read error (%1).").arg("ioctl"));
1376 m_stdin_notifier->setEnabled(false);
1377 return;
1378 }
1379 if(len == 0) {
1380 Logger::LogInfo("[PageRecord::OnStdin] " + tr("Standard input closed (%1).").arg("ioctl"));
1381 m_stdin_notifier->setEnabled(false);
1382 return;
1383 }
1384
1385 // read data
1386 QByteArray buffer(len, 0);
1387 ssize_t bytes;
1388 do {
1389 bytes = read(0, buffer.data(), buffer.size());
1390 } while(bytes == -1 && errno == EINTR);
1391 if(bytes == -1) {
1392 Logger::LogError("[PageRecord::OnStdin] " + tr("Standard input read error (%1).").arg("read"));
1393 m_stdin_notifier->setEnabled(false);
1394 return;
1395 }
1396 if(bytes == 0) {
1397 Logger::LogInfo("[PageRecord::OnStdin] " + tr("Standard input closed (%1).").arg("read"));
1398 m_stdin_notifier->setEnabled(false);
1399 return;
1400 }
1401 m_stdin_buffer.append(buffer.data(), bytes);
1402
1403 // process commands
1404 if(!m_stdin_reentrant) {
1405 m_stdin_reentrant = true;
1406 for( ; ; ) {
1407 QString command = ReadStdinCommand();
1408 if(command.isNull())
1409 break;
1410 Logger::LogInfo("[PageRecord::OnStdin] " + tr("Received command '%1'.").arg(command));
1411 if(command == "record-start") {
1412 OnRecordStart();
1413 } else if(command == "record-pause") {
1414 OnRecordPause();
1415 } else if(command == "record-cancel") {
1416 OnRecordCancel(false);
1417 } else if(command == "record-save") {
1418 OnRecordSave(false);
1419 } else if(command == "schedule-activate") {
1420 OnScheduleActivate();
1421 } else if(command == "schedule-deactivate") {
1422 OnScheduleDeactivate();
1423 } else if(command == "window-show") {
1424 m_main_window->OnShow();
1425 } else if(command == "window-hide") {
1426 m_main_window->OnHide();
1427 } else if(command == "quit") {
1428 m_main_window->Quit();
1429 } else {
1430 Logger::LogError("[PageRecord::OnStdin] " + tr("Unknown command."));
1431 }
1432 }
1433 m_stdin_reentrant = false;
1434 }
1435
1436 }
1437
1438 void PageRecord::OnUpdateInformation() {
1439
1440 if(m_page_started) {
1441
1442 int64_t total_time = 0;
1443 double fps_in = 0.0;
1444 double fps_out = 0.0;
1445 uint64_t bit_rate = 0, total_bytes = 0;
1446
1447 if(m_x11_input != NULL)
1448 fps_in = m_x11_input->GetFPS();
1449 #if SSR_USE_OPENGL_RECORDING
1450 if(m_gl_inject_input != NULL)
1451 fps_in = m_gl_inject_input->GetFPS();
1452 #endif
1453 #if SSR_USE_V4L2
1454 if(m_v4l2_input != NULL)
1455 fps_in = m_v4l2_input->GetFPS();
1456 #endif
1457
1458 if(m_output_manager != NULL) {
1459 total_time = (m_output_manager->GetSynchronizer() == NULL)? 0 : m_output_manager->GetSynchronizer()->GetTotalTime();
1460 fps_out = m_output_manager->GetActualFrameRate();
1461 bit_rate = (uint64_t) (m_output_manager->GetActualBitRate() + 0.5);
1462 total_bytes = m_output_manager->GetTotalBytes();
1463 }
1464
1465 QString file_name;
1466 if(m_file_protocol.isNull())
1467 file_name = (m_output_settings.file.isNull())? "?" : QFileInfo(m_output_settings.file).fileName();
1468 else
1469 file_name = "(" + m_file_protocol + ")";
1470
1471 // for X11 recording, update the video size
1472 if(m_x11_input != NULL)
1473 m_x11_input->GetCurrentSize(&m_video_in_width, &m_video_in_height);
1474
1475 #if SSR_USE_OPENGL_RECORDING
1476 // for OpenGL recording, update the video size
1477 if(m_gl_inject_input != NULL)
1478 m_gl_inject_input->GetCurrentSize(&m_video_in_width, &m_video_in_height);
1479 #endif
1480
1481 m_label_info_total_time->setText(ReadableTime(total_time));
1482 m_label_info_frame_rate_in->setText(QString::number(fps_in, 'f', 2));
1483 m_label_info_frame_rate_out->setText(QString::number(fps_out, 'f', 2));
1484 m_label_info_size_in->setText(ReadableWidthHeight(m_video_in_width, m_video_in_height));
1485 m_label_info_size_out->setText(ReadableWidthHeight(m_output_settings.video_width, m_output_settings.video_height));
1486 m_label_info_file_name->setText(file_name);
1487 m_label_info_file_size->setText(ReadableSizeIEC(total_bytes, "B"));
1488 m_label_info_bit_rate->setText(ReadableSizeSI(bit_rate, "bit/s"));
1489
1490 if(!CommandLineOptions::GetStatsFile().isNull()) {
1491 QString str = QString() +
1492 "capturing\t" + ((m_input_started)? "1" : "0") + "\n"
1493 "recording\t" + ((m_output_started)? "1" : "0") + "\n"
1494 "total_time\t" + QString::number(total_time) + "\n"
1495 "frame_rate_in\t" + QString::number(fps_in, 'f', 8) + "\n"
1496 "frame_rate_out\t" + QString::number(fps_out, 'f', 8) + "\n"
1497 "size_in_width\t" + QString::number(m_video_in_width) + "\n"
1498 "size_in_height\t" + QString::number(m_video_in_height) + "\n"
1499 "size_out_width\t" + QString::number(m_output_settings.video_width) + "\n"
1500 "size_out_height\t" + QString::number(m_output_settings.video_height) + "\n"
1501 "file_name\t" + file_name + "\n"
1502 "file_size\t" + QString::number(total_bytes) + "\n"
1503 "bit_rate\t" + QString::number(bit_rate) + "\n";
1504 QByteArray data = str.toUtf8();
1505 QByteArray old_file = QFile::encodeName(CommandLineOptions::GetStatsFile());
1506 QByteArray new_file = QFile::encodeName(CommandLineOptions::GetStatsFile() + "-new");
1507 // Qt doesn't get the permissions right (you can only change the permissions after creating the file, that's too late),
1508 // and it doesn't allow renaming a file over another file, so don't bother with QFile and just use POSIX and C functions.
1509 int fd = open(new_file.constData(), O_WRONLY | O_CREAT | O_CLOEXEC, 0600);
1510 if(fd != -1) {
1511 ssize_t b = write(fd, data.constData(), data.size()); Q_UNUSED(b);
1512 ::close(fd);
1513 rename(new_file.constData(), old_file.constData());
1514 }
1515 }
1516
1517 } else {
1518
1519 m_label_info_total_time->clear();
1520 m_label_info_frame_rate_in->clear();
1521 m_label_info_frame_rate_out->clear();
1522 m_label_info_size_in->clear();
1523 m_label_info_size_out->clear();
1524 m_label_info_file_name->clear();
1525 m_label_info_file_size->clear();
1526 m_label_info_bit_rate->clear();
1527
1528 if(!CommandLineOptions::GetStatsFile().isNull()) {
1529 QByteArray old_file = QFile::encodeName(CommandLineOptions::GetStatsFile());
1530 remove(old_file.constData());
1531 }
1532
1533 }
1534
1535 }
1536
1537 void PageRecord::OnNewLogLine(Logger::enum_type type, QString string) {
1538
1539 #if SSR_USE_ALSA
1540 // play sound for errors
1541 //TODO// this is an ugly way to detect errors, this should be improved at some point
1542 int64_t time = hrt_time_micro();
1543 if(m_simple_synth != NULL && type == Logger::TYPE_ERROR && time > m_last_error_sound + 1000000) {
1544 m_simple_synth->PlaySequence(SEQUENCE_RECORD_ERROR.data(), SEQUENCE_RECORD_ERROR.size());
1545 m_last_error_sound = time;
1546 }
1547 #endif
1548
1549 // change system tray icon if an error has occurred
1550 if(m_page_started && type == Logger::TYPE_ERROR && !m_error_occurred) {
1551 m_error_occurred = true;
1552 UpdateSysTray();
1553 }
1554
1555 // add line to log
1556 QTextCursor cursor = m_textedit_log->textCursor();
1557 QTextCharFormat format;
1558 bool should_scroll = (m_textedit_log->verticalScrollBar()->value() >= m_textedit_log->verticalScrollBar()->maximum());
1559 switch(type) {
1560 case Logger::TYPE_INFO: format.setForeground(m_textedit_log->palette().windowText()); break;
1561 case Logger::TYPE_WARNING: format.setForeground(Qt::darkYellow); break;
1562 case Logger::TYPE_ERROR: format.setForeground(Qt::red); break;
1563 case Logger::TYPE_STDERR: format.setForeground(Qt::gray); break;
1564 }
1565 cursor.movePosition(QTextCursor::End);
1566 if(cursor.position() != 0)
1567 cursor.insertBlock();
1568 cursor.insertText(string, format);
1569 if(should_scroll)
1570 m_textedit_log->verticalScrollBar()->setValue(m_textedit_log->verticalScrollBar()->maximum());
1571
1572 }
1573