diff options
-rw-r--r-- | tools/qmlviewer/main.cpp | 15 | ||||
-rw-r--r-- | tools/qmlviewer/qmlviewer.cpp | 217 | ||||
-rw-r--r-- | tools/qmlviewer/qmlviewer.h | 17 | ||||
-rw-r--r-- | tools/qmlviewer/qmlviewer.pro | 5 | ||||
-rw-r--r-- | tools/qmlviewer/recopts.ui | 338 |
5 files changed, 562 insertions, 30 deletions
diff --git a/tools/qmlviewer/main.cpp b/tools/qmlviewer/main.cpp index 8784ae0..e5f5e05 100644 --- a/tools/qmlviewer/main.cpp +++ b/tools/qmlviewer/main.cpp @@ -32,7 +32,7 @@ void usage() qWarning(" - png file for raw frames"); qWarning(" - 'ffmpeg' for other formats"); qWarning(" -recorddither ordered|threshold|floyd .... set GIF dither recording mode"); - qWarning(" -recordperiod <milliseconds> ............. set time between recording frames"); + qWarning(" -recordrate <fps> ........................ set recording frame rate"); qWarning(" -record arg .............................. add a recording process argument"); qWarning(" -autorecord [from-]<tomilliseconds> ...... set recording to start and stop"); qWarning(" -devicekeys .............................. use numeric keys (see F1)"); @@ -63,7 +63,7 @@ int main(int argc, char ** argv) bool frameless = false; QString fileName; - int period = 0; + double fps = 0; int autorecord_from = 0; int autorecord_to = 0; QString dither = "none"; @@ -83,8 +83,8 @@ int main(int argc, char ** argv) skin = QString(argv[++i]); } else if (arg == "-netcache") { cache = QString(argv[++i]).toInt(); - } else if (arg == "-recordperiod") { - period = QString(argv[++i]).toInt(); + } else if (arg == "-recordrate") { + fps = QString(argv[++i]).toDouble(); } else if (arg == "-recordfile") { recordfile = QString(argv[++i]); } else if (arg == "-record") { @@ -127,8 +127,8 @@ int main(int argc, char ** argv) viewer.addLibraryPath(lib); viewer.setNetworkCacheSize(cache); viewer.setRecordFile(recordfile); - if (period>0) - viewer.setRecordPeriod(period); + if (fps>0) + viewer.setRecordRate(fps); if (autorecord_to) viewer.setAutoRecord(autorecord_from,autorecord_to); if (!skin.isEmpty() && QDir(skin).exists()) @@ -136,7 +136,8 @@ int main(int argc, char ** argv) if (devkeys) viewer.setDeviceKeys(true); viewer.setRecordDither(dither); - viewer.setRecordArgs(recordargs); + if (recordargs.count()) + viewer.setRecordArgs(recordargs); if (!fileName.isEmpty()) { viewer.openQml(fileName); viewer.show(); diff --git a/tools/qmlviewer/qmlviewer.cpp b/tools/qmlviewer/qmlviewer.cpp index 95cd24b..643760b 100644 --- a/tools/qmlviewer/qmlviewer.cpp +++ b/tools/qmlviewer/qmlviewer.cpp @@ -12,6 +12,7 @@ ****************************************************************************/ #include <qfxview.h> +#include "ui_recopts.h" #include "qmlviewer.h" #include <QtDeclarative/qmlcontext.h> @@ -30,6 +31,7 @@ #include <QWidget> #include <QApplication> #include <QDir> +#include <QTextBrowser> #include <QFile> #include <QFileInfo> #include <QVBoxLayout> @@ -127,6 +129,79 @@ void PreviewDeviceSkin::slotPopupMenu() menu->exec(QCursor::pos()); } +static struct { const char *name, *args; } ffmpegprofiles[] = { + {"Maximum Quality", "-sameq"}, + {"High Quality", "-qcomp 0.75"}, + {"Medium Quality", "-qcomp 0.5"}, + {"Low Quality", "-qcomp 0.2"}, + {"Custom ffmpeg arguments", ""}, + {0,0} +}; + +class RecordingDialog : public QDialog, public Ui::RecordingOptions { + Q_OBJECT + +public: + RecordingDialog(QWidget *parent) : QDialog(parent) + { + setupUi(this); + hz->setValidator(new QDoubleValidator(hz)); + for (int i=0; ffmpegprofiles[i].name; ++i) { + profile->addItem(ffmpegprofiles[i].name); + } + } + + void setArguments(QString a) + { + int i; + for (i=0; ffmpegprofiles[i].args[0]; ++i) { + if (ffmpegprofiles[i].args == a) { + profile->setCurrentIndex(i); + args->setText(QLatin1String(ffmpegprofiles[i].args)); + return; + } + } + customargs = a; + args->setText(a); + profile->setCurrentIndex(i); + } + + QString arguments() const + { + int i = profile->currentIndex(); + return ffmpegprofiles[i].args[0] ? QLatin1String(ffmpegprofiles[i].args) : customargs; + } + +private slots: + void pickProfile(int i) + { + if (ffmpegprofiles[i].args[0]) { + args->setText(QLatin1String(ffmpegprofiles[i].args)); + } else { + args->setText(customargs); + } + } + + void storeCustomArgs(QString s) + { + setArguments(s); + } + +private: + QString customargs; +}; + +QString QmlViewer::getVideoFileName() +{ + QString title = convertAvailable || ffmpegAvailable ? tr("Save Video File") : tr("Save PNG Frames"); + QStringList types; + if (ffmpegAvailable) types += tr("Common Video files")+QLatin1String(" (*.avi *.mpeg *.mov)"); + if (convertAvailable) types += tr("GIF Animation")+QLatin1String(" (*.gif)"); + types += tr("Individual PNG frames")+QLatin1String(" (*.png)"); + if (ffmpegAvailable) types += tr("All ffmpeg formats (*.*)"); + return QFileDialog::getSaveFileName(this, title, "", types.join(";; ")); +} + QmlViewer::QmlViewer(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags), frame_stream(0), scaleSkin(true), mb(0) @@ -137,7 +212,28 @@ QmlViewer::QmlViewer(QWidget *parent, Qt::WindowFlags flags) palette = 0; disabledPalette = 0; record_autotime = 0; - record_period = 20; + record_rate = 50; + record_args += QLatin1String("-sameq"); + + recdlg = new RecordingDialog(this); + connect(recdlg->pickfile, SIGNAL(clicked()), this, SLOT(pickRecordingFile())); + senseFfmpeg(); + senseImageMagick(); + if (!ffmpegAvailable) + recdlg->ffmpegOptions->hide(); + if (!ffmpegAvailable && !convertAvailable) + recdlg->rateOptions->hide(); + QString warn; + if (!ffmpegAvailable) { + if (!convertAvailable) + warn = tr("ffmpeg and ImageMagick not available - no video output"); + else + warn = tr("ffmpeg not available - GIF and PNG outputs only"); + recdlg->warning->setText(warn); + } else { + recdlg->warning->hide(); + } + if (!(flags & Qt::FramelessWindowHint)) createMenu(menuBar(),0); @@ -205,6 +301,10 @@ void QmlViewer::createMenu(QMenuBar *menu, QMenu *flatmenu) connect(recordAction, SIGNAL(triggered()), this, SLOT(toggleRecordingWithSelection())); recordMenu->addAction(recordAction); + QAction *recordOptions = new QAction(tr("Recording &Options..."), parent); + connect(recordOptions, SIGNAL(triggered()), this, SLOT(chooseRecordingOptions())); + recordMenu->addAction(recordOptions); + if (flatmenu) flatmenu->addSeparator(); QMenu *skinMenu = flatmenu ? flatmenu->addMenu(tr("&Skin")) : menu->addMenu(tr("&Skin")); @@ -310,15 +410,57 @@ void QmlViewer::takeSnapShot() ++snapshotcount; } +void QmlViewer::pickRecordingFile() +{ + QString fileName = getVideoFileName(); + if (!fileName.isEmpty()) + recdlg->file->setText(fileName); +} + +void QmlViewer::chooseRecordingOptions() +{ + recdlg->file->setText(record_file); + if (record_rate == 24) + recdlg->hz24->setChecked(true); + else if (record_rate == 25) + recdlg->hz25->setChecked(true); + else if (record_rate == 50) + recdlg->hz50->setChecked(true); + else if (record_rate == 60) + recdlg->hz60->setChecked(true); + else { + recdlg->hzCustom->setChecked(true); + recdlg->hz->setText(QString::number(record_rate)); + } + recdlg->setArguments(record_args.join(" ")); + if (recdlg->exec()) { + record_file = recdlg->file->text(); + if (recdlg->hz24->isChecked()) + record_rate = 24; + else if (recdlg->hz25->isChecked()) + record_rate = 25; + else if (recdlg->hz50->isChecked()) + record_rate = 50; + else if (recdlg->hz60->isChecked()) + record_rate = 60; + else { + record_rate = recdlg->hz->text().toDouble(); + } + record_args = recdlg->arguments().split(" ",QString::SkipEmptyParts); + } +} + void QmlViewer::toggleRecordingWithSelection() { if (!recordTimer.isRunning()) { - QString fileName = QFileDialog::getSaveFileName(this, tr("Save Video File"), "", tr("Common Video files (*.avi *.mpeg *.mov);; GIF Animation (*.gif);; Individual PNG frames (*.png);; All ffmpeg formats (*.*)")); - if (fileName.isEmpty()) - return; - if (!fileName.contains(QRegExp(".[^\\/]*$"))) - fileName += ".avi"; - setRecordFile(fileName); + if (record_file.isEmpty()) { + QString fileName = getVideoFileName(); + if (fileName.isEmpty()) + return; + if (!fileName.contains(QRegExp(".[^\\/]*$"))) + fileName += ".avi"; + setRecordFile(fileName); + } } toggleRecording(); } @@ -503,9 +645,9 @@ void QmlViewer::setRecordFile(const QString& f) record_file = f; } -void QmlViewer::setRecordPeriod(int ms) +void QmlViewer::setRecordRate(int fps) { - record_period = ms; + record_rate = fps; } void QmlViewer::sceneResized(QSize size) @@ -545,32 +687,63 @@ void QmlViewer::keyPressEvent(QKeyEvent *event) QWidget::keyPressEvent(event); } +void QmlViewer::senseImageMagick() +{ + QProcess proc; + proc.start("convert", QStringList() << "-h"); + proc.waitForFinished(2000); + QString help = proc.readAllStandardOutput(); + convertAvailable = help.contains("ImageMagick"); +} + +void QmlViewer::senseFfmpeg() +{ + QProcess proc; + proc.start("ffmpeg", QStringList() << "-h"); + proc.waitForFinished(2000); + QString ffmpegHelp = proc.readAllStandardOutput(); + ffmpegAvailable = ffmpegHelp.contains("-s "); + ffmpegHelp = tr("Video recording use ffmpeg:")+"\n\n"+ffmpegHelp; + + QDialog *d = new QDialog(recdlg); + QVBoxLayout *l = new QVBoxLayout(d); + QTextBrowser *b = new QTextBrowser(d); + QFont f = b->font(); + f.setFamily("courier"); + b->setFont(f); + b->setText(ffmpegHelp); + l->addWidget(b); + d->setLayout(l); + ffmpegHelpWindow = d; + connect(recdlg->ffmpegHelp,SIGNAL(clicked()), ffmpegHelpWindow, SLOT(show())); +} + void QmlViewer::setRecording(bool on) { if (on == recordTimer.isRunning()) return; - QUnifiedTimer::instance()->setTimingInterval(on ? record_period:16); + int period = int(1000/record_rate+0.5); + QUnifiedTimer::instance()->setTimingInterval(on ? period:16); QUnifiedTimer::instance()->setConsistentTiming(on); if (on) { canvas->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); - recordTimer.setInterval(record_period); + recordTimer.setInterval(period); recordTimer.setRunning(true); - QString fmt = record_file.right(4).toLower(); - if (fmt != ".png" && fmt != ".gif") { + frame_fmt = record_file.right(4).toLower(); + frame = QImage(canvas->width(),canvas->height(),QImage::Format_RGB32); + if (frame_fmt != ".png" && (!convertAvailable || frame_fmt != ".gif")) { // Stream video to ffmpeg QProcess *proc = new QProcess(this); connect(proc, SIGNAL(finished(int)), this, SLOT(ffmpegFinished(int))); frame_stream = proc; - frame = QImage(canvas->width(),canvas->height(),QImage::Format_RGB32); QStringList args; - args << "-sameq"; // ie. high args << "-y"; - args << "-r" << QString::number(1000/record_period); + args << "-r" << QString::number(record_rate); args << "-f" << "rawvideo"; - args << "-pix_fmt" << "rgb32"; + args << "-pix_fmt" << (frame_fmt == ".gif" ? "rgb24" : "rgb32"); args << "-s" << QString("%1x%2").arg(canvas->width()).arg(canvas->height()); args << "-i" << "-"; args += record_args; @@ -635,7 +808,7 @@ void QmlViewer::setRecording(bool on) // ImageMagick and gifsicle for GIF encoding progress.setLabelText(tr("Converting frames to GIF file...")); QStringList args; - args << "-delay" << QString::number(record_period/10); + args << "-delay" << QString::number(period/10); args << inputs; args << record_file; qDebug() << "Converting..." << record_file << "(this may take a while)"; @@ -685,7 +858,13 @@ void QmlViewer::recordFrame() { canvas->QWidget::render(&frame); if (frame_stream) { - frame_stream->write((char*)frame.bits(),frame.numBytes()); + if (frame_fmt == ".gif") { + // ffmpeg can't do 32bpp with gif + QImage rgb24 = frame.convertToFormat(QImage::Format_RGB888); + frame_stream->write((char*)rgb24.bits(),rgb24.numBytes()); + } else { + frame_stream->write((char*)frame.bits(),frame.numBytes()); + } } else { frames.append(new QImage(frame)); } diff --git a/tools/qmlviewer/qmlviewer.h b/tools/qmlviewer/qmlviewer.h index e7177eb..15acf72 100644 --- a/tools/qmlviewer/qmlviewer.h +++ b/tools/qmlviewer/qmlviewer.h @@ -26,6 +26,7 @@ class PreviewDeviceSkin; class QFxTestEngine; class QmlPalette; class QProcess; +class RecordingDialog; class QmlViewer : public QWidget { @@ -34,10 +35,9 @@ public: QmlViewer(QWidget *parent=0, Qt::WindowFlags flags=0); void setRecordDither(const QString& s) { record_dither = s; } - void setRecordPeriod(int ms); + void setRecordRate(int fps); void setRecordFile(const QString&); void setRecordArgs(const QStringList&); - int recordPeriod() const { return record_period; } void setRecording(bool on); bool isRecording() const { return recordTimer.isRunning(); } void setAutoRecord(int from, int to); @@ -69,10 +69,13 @@ private slots: void autoStartRecording(); void autoStopRecording(); void recordFrame(); + void chooseRecordingOptions(); + void pickRecordingFile(); private: void setupProxy(); void setupPalettes(); + QString getVideoFileName(); QString currentFileName; PreviewDeviceSkin *skin; @@ -81,6 +84,7 @@ private: QmlPalette *palette; QmlPalette *disabledPalette; QmlTimer recordTimer; + QString frame_fmt; QImage frame; QList<QImage*> frames; QProcess* frame_stream; @@ -89,13 +93,20 @@ private: QString record_dither; QString record_file; QStringList record_args; - int record_period; + int record_rate; int record_autotime; bool devicemode; QAction *recordAction; QString currentSkin; bool scaleSkin; mutable QMenuBar *mb; + RecordingDialog *recdlg; + + void senseImageMagick(); + void senseFfmpeg(); + QWidget *ffmpegHelpWindow; + bool ffmpegAvailable; + bool convertAvailable; }; QT_END_NAMESPACE diff --git a/tools/qmlviewer/qmlviewer.pro b/tools/qmlviewer/qmlviewer.pro index 08d2d2b..a8ccd91 100644 --- a/tools/qmlviewer/qmlviewer.pro +++ b/tools/qmlviewer/qmlviewer.pro @@ -1,12 +1,15 @@ +TEMPLATE = app +CONFIG += qt uic DESTDIR = ../../bin QT += declarative script network sql # Input HEADERS += qmlviewer.h SOURCES += main.cpp qmlviewer.cpp +FORMS = recopts.ui + include($$QT_SOURCE_TREE/tools/shared/deviceskin/deviceskin.pri) target.path=$$[QT_INSTALL_BINS] INSTALLS += target -CONFIG += console diff --git a/tools/qmlviewer/recopts.ui b/tools/qmlviewer/recopts.ui new file mode 100644 index 0000000..041a4c4 --- /dev/null +++ b/tools/qmlviewer/recopts.ui @@ -0,0 +1,338 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>RecordingOptions</class> + <widget class="QDialog" name="RecordingOptions"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>290</width> + <height>297</height> + </rect> + </property> + <property name="windowTitle"> + <string>Recording options</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>File:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="file"/> + </item> + <item row="6" column="2" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QToolButton" name="pickfile"> + <property name="text"> + <string>...</string> + </property> + </widget> + </item> + <item row="2" column="0" colspan="4"> + <widget class="QWidget" name="ffmpegOptions" native="true"> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="margin"> + <number>0</number> + </property> + <item row="1" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>Profile:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QComboBox" name="profile"/> + </item> + <item row="2" column="1"> + <widget class="QLineEdit" name="args"/> + </item> + <item row="2" column="2"> + <widget class="QToolButton" name="ffmpegHelp"> + <property name="text"> + <string>Help</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="1" column="0" colspan="4"> + <widget class="QWidget" name="rateOptions" native="true"> + <layout class="QGridLayout" name="gridLayout_3"> + <property name="margin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Rate:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QRadioButton" name="hz24"> + <property name="text"> + <string>24Hz</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QRadioButton" name="hz50"> + <property name="text"> + <string>50Hz</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QRadioButton" name="hz25"> + <property name="text"> + <string>25Hz</string> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QRadioButton" name="hz60"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>60Hz</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QRadioButton" name="hzCustom"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="hz"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>60</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>100</width> + <height>16777215</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Hz</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <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> + </layout> + </widget> + </item> + <item row="4" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="0" colspan="4"> + <widget class="QLabel" name="warning"> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>hzCustom</sender> + <signal>clicked()</signal> + <receiver>hz</receiver> + <slot>setFocus()</slot> + <hints> + <hint type="sourcelabel"> + <x>54</x> + <y>104</y> + </hint> + <hint type="destinationlabel"> + <x>85</x> + <y>107</y> + </hint> + </hints> + </connection> + <connection> + <sender>hz</sender> + <signal>textChanged(QString)</signal> + <receiver>hzCustom</receiver> + <slot>toggle()</slot> + <hints> + <hint type="sourcelabel"> + <x>138</x> + <y>102</y> + </hint> + <hint type="destinationlabel"> + <x>49</x> + <y>104</y> + </hint> + </hints> + </connection> + <connection> + <sender>hz</sender> + <signal>selectionChanged()</signal> + <receiver>hzCustom</receiver> + <slot>toggle()</slot> + <hints> + <hint type="sourcelabel"> + <x>110</x> + <y>105</y> + </hint> + <hint type="destinationlabel"> + <x>51</x> + <y>103</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>RecordingOptions</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>280</x> + <y>287</y> + </hint> + <hint type="destinationlabel"> + <x>60</x> + <y>219</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>RecordingOptions</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>280</x> + <y>287</y> + </hint> + <hint type="destinationlabel"> + <x>92</x> + <y>219</y> + </hint> + </hints> + </connection> + <connection> + <sender>profile</sender> + <signal>activated(int)</signal> + <receiver>RecordingOptions</receiver> + <slot>pickProfile(int)</slot> + <hints> + <hint type="sourcelabel"> + <x>72</x> + <y>132</y> + </hint> + <hint type="destinationlabel"> + <x>48</x> + <y>194</y> + </hint> + </hints> + </connection> + <connection> + <sender>args</sender> + <signal>textEdited(QString)</signal> + <receiver>RecordingOptions</receiver> + <slot>storeCustomArgs(QString)</slot> + <hints> + <hint type="sourcelabel"> + <x>108</x> + <y>161</y> + </hint> + <hint type="destinationlabel"> + <x>102</x> + <y>189</y> + </hint> + </hints> + </connection> + </connections> + <slots> + <signal>filePicked(QString)</signal> + <signal>argumentsPicked(QString)</signal> + <slot>pickFile()</slot> + <slot>pickProfile(int)</slot> + <slot>storeCustomArgs(QString)</slot> + </slots> +</ui> |