Qt: Add video recording presets
Jeffrey Pfau jeffrey@endrift.com
Wed, 12 Nov 2014 00:55:37 -0800
3 files changed,
664 insertions(+),
201 deletions(-)
M
src/platform/qt/VideoView.cpp
→
src/platform/qt/VideoView.cpp
@@ -11,6 +11,31 @@ QMap<QString, QString> VideoView::s_acodecMap;
QMap<QString, QString> VideoView::s_vcodecMap; QMap<QString, QString> VideoView::s_containerMap; +bool VideoView::Preset::compatible(const Preset& other) const { + if (!other.container.isNull() && !container.isNull() && other.container != container) { + return false; + } + if (!other.acodec.isNull() && !acodec.isNull() && other.acodec != acodec) { + return false; + } + if (!other.vcodec.isNull() && !vcodec.isNull() && other.vcodec != vcodec) { + return false; + } + if (other.abr && abr && other.abr != abr) { + return false; + } + if (other.vbr && vbr && other.vbr != vbr) { + return false; + } + if (other.width && width && other.width != width) { + return false; + } + if (other.height && height && other.height != height) { + return false; + } + return true; +} + VideoView::VideoView(QWidget* parent) : QWidget(parent) , m_audioCodecCstr(nullptr)@@ -54,13 +79,73 @@
connect(m_ui.abr, SIGNAL(valueChanged(int)), this, SLOT(setAudioBitrate(int))); connect(m_ui.vbr, SIGNAL(valueChanged(int)), this, SLOT(setVideoBitrate(int))); + connect(m_ui.showAdvanced, SIGNAL(clicked(bool)), this, SLOT(showAdvanced(bool))); + FFmpegEncoderInit(&m_encoder); + addPreset(m_ui.preset1080, (Preset) { + .width = 1620, + .height = 1080 + }); + + addPreset(m_ui.preset720, (Preset) { + .width = 1080, + .height = 720 + }); + + addPreset(m_ui.preset480, (Preset) { + .width = 720, + .height = 480 + }); + + addPreset(m_ui.preset160, (Preset) { + .width = 240, + .height = 160 + }); + + addPreset(m_ui.presetHQ, (Preset) { + .container = "MP4", + .vcodec = "h.264", + .acodec = "AAC", + .vbr = 6000, + .abr = 384, + .width = 1620, + .height = 1080 + }); + + addPreset(m_ui.presetYoutube, (Preset) { + .container = "MP4", + .vcodec = "h.264", + .acodec = "AAC", + .vbr = 3000, + .abr = 256, + .width = 1080, + .height = 720 + }); + + addPreset(m_ui.presetWebM, (Preset) { + .container = "WebM", + .vcodec = "VP8", + .acodec = "Vorbis", + .vbr = 800, + .abr = 128 + }); + + addPreset(m_ui.presetLossless, (Preset) { + .container = "MKV", + .vcodec = "PNG", + .acodec = "FLAC", + .width = 240, + .height = 160, + }); + setAudioCodec(m_ui.audio->currentText()); setVideoCodec(m_ui.video->currentText()); setAudioBitrate(m_ui.abr->value()); setVideoBitrate(m_ui.vbr->value()); setContainer(m_ui.container->currentText()); + + showAdvanced(false); } VideoView::~VideoView() {@@ -101,58 +186,68 @@ m_filename = fname;
validateSettings(); } -void VideoView::setAudioCodec(const QString& codec) { +void VideoView::setAudioCodec(const QString& codec, bool manual) { free(m_audioCodecCstr); - m_audioCodec = sanitizeCodec(codec); - if (s_acodecMap.contains(m_audioCodec)) { - m_audioCodec = s_acodecMap[m_audioCodec]; - } + m_audioCodec = sanitizeCodec(codec, s_acodecMap); m_audioCodecCstr = strdup(m_audioCodec.toLocal8Bit().constData()); if (!FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr)) { free(m_audioCodecCstr); m_audioCodecCstr = nullptr; } validateSettings(); + if (manual) { + uncheckIncompatible(); + } } -void VideoView::setVideoCodec(const QString& codec) { +void VideoView::setVideoCodec(const QString& codec, bool manual) { free(m_videoCodecCstr); - m_videoCodec = sanitizeCodec(codec); - if (s_vcodecMap.contains(m_videoCodec)) { - m_videoCodec = s_vcodecMap[m_videoCodec]; - } + m_videoCodec = sanitizeCodec(codec, s_vcodecMap); m_videoCodecCstr = strdup(m_videoCodec.toLocal8Bit().constData()); if (!FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr)) { free(m_videoCodecCstr); m_videoCodecCstr = nullptr; } validateSettings(); + if (manual) { + uncheckIncompatible(); + } } -void VideoView::setContainer(const QString& container) { +void VideoView::setContainer(const QString& container, bool manual) { free(m_containerCstr); - m_container = sanitizeCodec(container); - if (s_containerMap.contains(m_container)) { - m_container = s_containerMap[m_container]; - } + m_container = sanitizeCodec(container, s_containerMap); m_containerCstr = strdup(m_container.toLocal8Bit().constData()); if (!FFmpegEncoderSetContainer(&m_encoder, m_containerCstr)) { free(m_containerCstr); m_containerCstr = nullptr; } validateSettings(); + if (manual) { + uncheckIncompatible(); + } } -void VideoView::setAudioBitrate(int br) { +void VideoView::setAudioBitrate(int br, bool manual) { m_abr = br * 1000; FFmpegEncoderSetAudio(&m_encoder, m_audioCodecCstr, m_abr); validateSettings(); + if (manual) { + uncheckIncompatible(); + } } -void VideoView::setVideoBitrate(int br) { +void VideoView::setVideoBitrate(int br, bool manual) { m_vbr = br * 1000; FFmpegEncoderSetVideo(&m_encoder, m_videoCodecCstr, m_vbr); validateSettings(); + if (manual) { + uncheckIncompatible(); + } +} + +void VideoView::showAdvanced(bool show) { + m_ui.advancedBox->setVisible(show); } bool VideoView::validateSettings() {@@ -185,12 +280,100 @@ valid = false;
} m_ui.start->setEnabled(valid && !m_filename.isNull()); + return valid; } -QString VideoView::sanitizeCodec(const QString& codec) { +void VideoView::uncheckIncompatible() { + Preset current = { + .container = m_container, + .acodec = m_audioCodec, + .vcodec = m_videoCodec, + .abr = m_abr / 1000, + .vbr = m_vbr / 1000 + }; + + for (auto iterator = m_presets.constBegin(); iterator != m_presets.constEnd(); ++iterator) { + Preset next = *iterator; + next.container = sanitizeCodec(next.container, s_containerMap); + next.acodec = sanitizeCodec(next.acodec, s_acodecMap); + next.vcodec = sanitizeCodec(next.vcodec, s_vcodecMap); + if (!current.compatible(next)) { + safelyCheck(iterator.key(), false); + } + } + if (current.compatible(m_presets[m_ui.preset160])) { + safelyCheck(m_ui.preset160); + } + if (current.compatible(m_presets[m_ui.preset480])) { + safelyCheck(m_ui.preset480); + } + if (current.compatible(m_presets[m_ui.preset720])) { + safelyCheck(m_ui.preset720); + } + if (current.compatible(m_presets[m_ui.preset1080])) { + safelyCheck(m_ui.preset1080); + } +} + +QString VideoView::sanitizeCodec(const QString& codec, const QMap<QString, QString>& mapping) { QString sanitized = codec.toLower(); - return sanitized.remove(QChar('.')); + sanitized = sanitized.remove(QChar('.')); + if (mapping.contains(sanitized)) { + sanitized = mapping[sanitized]; + } + return sanitized; +} + +void VideoView::safelyCheck(QAbstractButton* button, bool set) { + bool signalsBlocked = button->blockSignals(true); + button->setChecked(set); + button->blockSignals(signalsBlocked); +} + +void VideoView::safelySet(QSpinBox* box, int value) { + bool signalsBlocked = box->blockSignals(true); + box->setValue(value); + box->blockSignals(signalsBlocked); +} + +void VideoView::safelySet(QComboBox* box, const QString& value) { + bool signalsBlocked = box->blockSignals(true); + box->lineEdit()->setText(value); + box->blockSignals(signalsBlocked); +} + +void VideoView::addPreset(QAbstractButton* button, const Preset& preset) { + m_presets[button] = preset; + connect(button, &QAbstractButton::pressed, [this, preset]() { + setPreset(preset); + }); +} + +void VideoView::setPreset(const Preset& preset) { + if (!preset.container.isNull()) { + setContainer(preset.container, false); + safelySet(m_ui.container, preset.container); + } + if (!preset.acodec.isNull()) { + setAudioCodec(preset.acodec, false); + safelySet(m_ui.audio, preset.acodec); + } + if (!preset.vcodec.isNull()) { + setVideoCodec(preset.vcodec, false); + safelySet(m_ui.video, preset.vcodec); + } + if (preset.abr) { + setAudioBitrate(preset.abr, false); + safelySet(m_ui.abr, preset.abr); + } + if (preset.vbr) { + setVideoBitrate(preset.vbr, false); + safelySet(m_ui.vbr, preset.vbr); + } + + uncheckIncompatible(); + validateSettings(); } #endif
M
src/platform/qt/VideoView.h
→
src/platform/qt/VideoView.h
@@ -17,6 +17,18 @@ class VideoView : public QWidget {
Q_OBJECT public: + struct Preset { + QString container; + QString vcodec; + QString acodec; + int vbr; + int abr; + int width; + int height; + + bool compatible(const Preset&) const; + }; + VideoView(QWidget* parent = nullptr); virtual ~VideoView();@@ -33,16 +45,26 @@
private slots: void selectFile(); void setFilename(const QString&); - void setAudioCodec(const QString&); - void setVideoCodec(const QString&); - void setContainer(const QString&); + void setAudioCodec(const QString&, bool manual = true); + void setVideoCodec(const QString&, bool manual = true); + void setContainer(const QString&, bool manual = true); - void setAudioBitrate(int); - void setVideoBitrate(int); + void setAudioBitrate(int, bool manual = true); + void setVideoBitrate(int, bool manual = true); + + void showAdvanced(bool); + + void uncheckIncompatible(); private: bool validateSettings(); - static QString sanitizeCodec(const QString&); + static QString sanitizeCodec(const QString&, const QMap<QString, QString>& mapping); + static void safelyCheck(QAbstractButton*, bool set = true); + static void safelySet(QSpinBox*, int value); + static void safelySet(QComboBox*, const QString& value); + + void addPreset(QAbstractButton*, const Preset&); + void setPreset(const Preset&); Ui::VideoView m_ui;@@ -58,6 +80,8 @@ char* m_containerCstr;
int m_abr; int m_vbr; + + QMap<QAbstractButton*, Preset> m_presets; static QMap<QString, QString> s_acodecMap; static QMap<QString, QString> s_vcodecMap;
M
src/platform/qt/VideoView.ui
→
src/platform/qt/VideoView.ui
@@ -6,8 +6,8 @@ <property name="geometry">
<rect> <x>0</x> <y>0</y> - <width>462</width> - <height>194</height> + <width>351</width> + <height>510</height> </rect> </property> <property name="sizePolicy">@@ -19,175 +19,11 @@ </property>
<property name="windowTitle"> <string>Record Video</string> </property> - <layout class="QGridLayout" name="gridLayout_2"> + <layout class="QVBoxLayout" name="verticalLayout_4"> <property name="sizeConstraint"> <enum>QLayout::SetFixedSize</enum> </property> - <item row="0" column="0" rowspan="2"> - <widget class="QGroupBox" name="groupBox_4"> - <property name="title"> - <string>Format</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <widget class="QComboBox" name="container"> - <property name="editable"> - <bool>true</bool> - </property> - <item> - <property name="text"> - <string>MKV</string> - </property> - </item> - <item> - <property name="text"> - <string>WebM</string> - </property> - </item> - <item> - <property name="text"> - <string>AVI</string> - </property> - </item> - <item> - <property name="text"> - <string>MP4</string> - </property> - </item> - </widget> - </item> - <item> - <widget class="QComboBox" name="video"> - <property name="editable"> - <bool>true</bool> - </property> - <item> - <property name="text"> - <string>PNG</string> - </property> - </item> - <item> - <property name="text"> - <string>h.264</string> - </property> - </item> - <item> - <property name="text"> - <string>VP8</string> - </property> - </item> - <item> - <property name="text"> - <string>Xvid</string> - </property> - </item> - <item> - <property name="text"> - <string>FFV1</string> - </property> - </item> - </widget> - </item> - <item> - <widget class="QComboBox" name="audio"> - <property name="editable"> - <bool>true</bool> - </property> - <item> - <property name="text"> - <string>FLAC</string> - </property> - </item> - <item> - <property name="text"> - <string>Vorbis</string> - </property> - </item> - <item> - <property name="text"> - <string>MP3</string> - </property> - </item> - <item> - <property name="text"> - <string>AAC</string> - </property> - </item> - <item> - <property name="text"> - <string>Uncompressed</string> - </property> - </item> - </widget> - </item> - </layout> - </widget> - </item> - <item row="1" column="1"> - <widget class="QGroupBox" name="groupBox"> - <property name="title"> - <string> Bitrate</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QLabel" name="label"> - <property name="text"> - <string>Video</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>vbr</cstring> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="vbr"> - <property name="suffix"> - <string/> - </property> - <property name="minimum"> - <number>200</number> - </property> - <property name="maximum"> - <number>2000</number> - </property> - <property name="value"> - <number>400</number> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Audio</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - <property name="buddy"> - <cstring>abr</cstring> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="abr"> - <property name="minimum"> - <number>16</number> - </property> - <property name="maximum"> - <number>320</number> - </property> - <property name="value"> - <number>192</number> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item row="0" column="1"> + <item> <layout class="QGridLayout" name="gridLayout"> <item row="1" column="0"> <widget class="QPushButton" name="start">@@ -221,7 +57,7 @@ <string>Stop</string>
</property> </widget> </item> - <item row="1" column="2"> + <item row="1" column="3"> <widget class="QPushButton" name="selectFile"> <property name="sizePolicy"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">@@ -234,7 +70,7 @@ <string>Select File</string>
</property> </widget> </item> - <item row="0" column="0" colspan="3"> + <item row="0" column="0" colspan="4"> <widget class="QLineEdit" name="filename"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">@@ -244,15 +80,433 @@ </sizepolicy>
</property> </widget> </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> </layout> </item> - <item row="2" column="0" colspan="2"> - <widget class="QDialogButtonBox" name="buttonBox"> - <property name="standardButtons"> - <set>QDialogButtonBox::Close</set> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Presets</string> </property> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QRadioButton" name="presetHQ"> + <property name="text"> + <string>High Quality</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">presets</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="presetYoutube"> + <property name="text"> + <string>YouTube</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">presets</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="presetWebM"> + <property name="text"> + <string>WebM</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">presets</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="presetLossless"> + <property name="text"> + <string>Lossless</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">presets</string> + </attribute> + </widget> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QRadioButton" name="preset1080"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>1080p</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">resolutions</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="preset720"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>720p</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">resolutions</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="preset480"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>480p</string> + </property> + <attribute name="buttonGroup"> + <string notr="true">resolutions</string> + </attribute> + </widget> + </item> + <item> + <widget class="QRadioButton" name="preset160"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>GBA (240x160)</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + <attribute name="buttonGroup"> + <string notr="true">resolutions</string> + </attribute> + </widget> + </item> + </layout> + </item> + </layout> </widget> </item> + <item> + <widget class="QWidget" name="advancedBox" native="true"> + <layout class="QGridLayout" name="gridLayout_6"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QGroupBox" name="formatBox"> + <property name="title"> + <string>Format</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <item> + <widget class="QComboBox" name="container"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>MKV</string> + </property> + </item> + <item> + <property name="text"> + <string>WebM</string> + </property> + </item> + <item> + <property name="text"> + <string>AVI</string> + </property> + </item> + <item> + <property name="text"> + <string>MP4</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="video"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>PNG</string> + </property> + </item> + <item> + <property name="text"> + <string>h.264</string> + </property> + </item> + <item> + <property name="text"> + <string>VP8</string> + </property> + </item> + <item> + <property name="text"> + <string>Xvid</string> + </property> + </item> + <item> + <property name="text"> + <string>FFV1</string> + </property> + </item> + </widget> + </item> + <item> + <widget class="QComboBox" name="audio"> + <property name="editable"> + <bool>true</bool> + </property> + <item> + <property name="text"> + <string>FLAC</string> + </property> + </item> + <item> + <property name="text"> + <string>Vorbis</string> + </property> + </item> + <item> + <property name="text"> + <string>MP3</string> + </property> + </item> + <item> + <property name="text"> + <string>AAC</string> + </property> + </item> + <item> + <property name="text"> + <string>Uncompressed</string> + </property> + </item> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="1"> + <widget class="QGroupBox" name="bitrateBox"> + <property name="title"> + <string> Bitrate (kbps)</string> + </property> + <layout class="QGridLayout" name="gridLayout_3"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>VBR </string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>vbr</cstring> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="vbr"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string/> + </property> + <property name="minimum"> + <number>200</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + <property name="value"> + <number>400</number> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="abr"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="minimum"> + <number>16</number> + </property> + <property name="maximum"> + <number>320</number> + </property> + <property name="value"> + <number>128</number> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>ABR</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="buddy"> + <cstring>abr</cstring> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QGroupBox" name="dimensionsBox"> + <property name="title"> + <string>Dimensions</string> + </property> + <layout class="QGridLayout" name="gridLayout_4"> + <item row="1" column="2"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>:</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>×</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QSpinBox" name="height"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimum"> + <number>160</number> + </property> + <property name="maximum"> + <number>3160</number> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="width"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimum"> + <number>240</number> + </property> + <property name="maximum"> + <number>3840</number> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QSpinBox" name="hratio"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="value"> + <number>2</number> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QSpinBox" name="wratio"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="value"> + <number>3</number> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QCheckBox" name="lockRatio"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Lock aspect ratio</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QCheckBox" name="showAdvanced"> + <property name="text"> + <string>Show advanced</string> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </item> </layout> </widget> <tabstops>@@ -263,9 +517,11 @@ <tabstop>selectFile</tabstop>
<tabstop>container</tabstop> <tabstop>video</tabstop> <tabstop>audio</tabstop> - <tabstop>vbr</tabstop> - <tabstop>abr</tabstop> </tabstops> <resources/> <connections/> + <buttongroups> + <buttongroup name="presets"/> + <buttongroup name="resolutions"/> + </buttongroups> </ui>