feature: Replace the preview system with qtmultimedia

Signed-off-by: 's avatarLuis Felipe Dominguez Vega <ldominguezvega@gmail.com>
parent 3a713525
......@@ -4,7 +4,7 @@
#
#-------------------------------------------------
QT += core gui network opengl multimedia qml widgets quick quickwidgets
QT += core gui network opengl multimedia qml widgets quick quickwidgets multimediawidgets
#DEFINES += QT_DEPRECATED_WARNINGS
QMAKE_CFLAGS_RELEASE += -flto
......@@ -52,12 +52,9 @@ SOURCES += main.cpp \
services/xmllookuptable.cpp \
ui/updatedialog.cpp \
services/settingtimer.cpp \
services/ffplaypreviewer.cpp \
services/abstractpreviewer.cpp \
services/mplayerpreviewer.cpp \
ui/previewdialog.cpp \
ui/interactivecuttingdialog.cpp \
ui/myqmpwidget.cpp \
ui/mediaplayerwidget.cpp \
ui/rangewidgetbinder.cpp
......@@ -100,13 +97,10 @@ HEADERS += \
services/xmllookuptable.h \
ui/updatedialog.h \
services/settingtimer.h \
services/ffplaypreviewer.h \
services/abstractpreviewer.h \
services/mplayerpreviewer.h \
ui/previewdialog.h \
ui/interactivecuttingdialog.h \
ui/mediaplayerwidget.h \
ui/myqmpwidget.h \
ui/rangewidgetbinder.h
FORMS += \
......@@ -194,16 +188,6 @@ DEFINES += VERSION_ID_STRING=$(VERSION_ID_STRING)
DEFINES += OPERATION_TIMEOUT=30000
DEFINES += DEFAULT_THREAD_COUNT=1
# QMPwidget (embedded mplayer)
SOURCES += qmpwidget/qmpwidget.cpp
HEADERS += qmpwidget/qmpwidget.h
INCLUDEPATH += qmpwidget
#CONFIG += qmpwidget_pipemode
!win32:qmpwidget_pipemode: {
HEADERS += qmpwidget/qmpyuvreader.h
DEFINES += QMP_USE_YUVPIPE
}
OTHER_FILES +=
DISTFILES +=
qmpwidget - A Qt widget for embedding MPlayer
=============================================
This is a small class which allows Qt developers to embed an MPlayer
instance into their application for convenient video playback. Starting
with version 4.4, Qt can be build with Phonon, the KDE multimedia
framework (see http://doc.trolltech.com/phonon-overview.html for an
overview). However, the Phonon API provides only a limited amount of
functionality, which may be too limited for some purposes.
In contrast, this class provides a way of embedding a full-fledged movie
player within an application. This means you can use all of MPlayer's
features, but also that you will need to ship a MPlayer binary with your
application if you can't make sure that it is already installed on a
user system.
For more information about MPlayer, please visit http://www.mplayerhq.hu/.
QMPwidget is written by Jonas Gehring and licensed under GPLv3. For more
information about QMPwidget, please visit http://qmpwidget.sourceforge.net/.
/*
* qmpwidget - A Qt widget for embedding MPlayer
* Copyright (C) 2010 by Jonas Gehring
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QAbstractSlider>
#include <QKeyEvent>
#include <QPainter>
#include <QProcess>
#include <QStringList>
#include <QTemporaryFile>
#include <QThread>
#include <QtDebug>
#ifdef QT_OPENGL_LIB
#include <QGLWidget>
#endif
#include "qmpwidget.h"
//#define QMP_DEBUG_OUTPUT
#ifdef QMP_USE_YUVPIPE
#include "qmpyuvreader.h"
#endif // QMP_USE_YUVPIPE
// A plain video widget
class QMPPlainVideoWidget : public QWidget
{
Q_OBJECT
public:
QMPPlainVideoWidget(QWidget *parent = nullptr)
: QWidget(parent)
{
setAttribute(Qt::WA_NoSystemBackground);
setMouseTracking(true);
}
void showUserImage(const QImage &image)
{
m_userImage = image;
update();
}
public slots:
void displayImage(const QImage &image)
{
m_pixmap = QPixmap::fromImage(image);
update();
}
protected:
void paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter p(this);
p.setCompositionMode(QPainter::CompositionMode_Source);
if (!m_userImage.isNull()) {
p.fillRect(rect(), Qt::black);
p.drawImage(rect().center() - m_userImage.rect().center(), m_userImage);
} else if (!m_pixmap.isNull()) {
p.drawPixmap(rect(), m_pixmap);
} else {
p.fillRect(rect(), Qt::black);
}
p.end();
}
private:
QPixmap m_pixmap;
QImage m_userImage;
};
#ifdef QT_OPENGL_LIB
// A OpenGL video widget
class QMPOpenGLVideoWidget : public QGLWidget
{
Q_OBJECT
public:
QMPOpenGLVideoWidget(QWidget *parent = nullptr)
: QGLWidget(parent), m_tex(-1)
{
setMouseTracking(true);
}
void showUserImage(const QImage &image)
{
m_userImage = image;
makeCurrent();
if (m_tex >= 0) {
deleteTexture(m_tex);
}
if (!m_userImage.isNull()) {
m_tex = bindTexture(image);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glViewport(0, 0, width(), qMax(height(), 1));
}
updateGL();
}
public slots:
void displayImage(const QImage &image)
{
if (!m_userImage.isNull()) {
return;
}
makeCurrent();
if (m_tex >= 0) {
deleteTexture(m_tex);
}
m_tex = bindTexture(image);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
updateGL();
}
protected:
void initializeGL()
{
glEnable(GL_TEXTURE_2D);
glClearColor(0, 0, 0, 0);
glClearDepth(1);
}
void resizeGL(int w, int h)
{
glViewport(0, 0, w, qMax(h, 1));
}
void paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
if (m_tex >= 0) {
glBindTexture(GL_TEXTURE_2D, m_tex);
if (!m_userImage.isNull()) {
QRect r = m_userImage.rect();
r.moveTopLeft(rect().center() - m_userImage.rect().center());
glViewport(r.x(), r.y(), r.width(), r.height());
}
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex2f(-1, -1);
glTexCoord2f(1, 0); glVertex2f( 1, -1);
glTexCoord2f(1, 1); glVertex2f( 1, 1);
glTexCoord2f(0, 1); glVertex2f(-1, 1);
glEnd();
}
}
private:
QImage m_userImage;
int m_tex;
};
#endif // QT_OPENGL_LIB
// A custom QProcess designed for the MPlayer slave interface
class QMPProcess : public QProcess
{
Q_OBJECT
public:
QMPProcess(QObject *parent = nullptr)
: QProcess(parent), m_state(QMPwidget::NotStartedState), m_mplayerPath("mplayer"),
m_fakeInputconf(nullptr)
#ifdef QMP_USE_YUVPIPE
, m_yuvReader(NULL)
#endif
{
resetValues();
#ifdef Q_WS_WIN
m_mode = QMPwidget::EmbeddedMode;
m_videoOutput = "directx,directx:noaccel";
#elif defined(Q_WS_X11)
m_mode = QMPwidget::EmbeddedMode;
#ifdef QT_OPENGL_LIB
m_videoOutput = "gl2,gl,xv";
#else
m_videoOutput = "xv";
#endif
#elif defined(Q_WS_MAC)
m_mode = QMPwidget::PipeMode;
#ifdef QT_OPENGL_LIB
m_videoOutput = "gl,quartz";
#else
m_videoOutput = "quartz";
#endif
#endif
m_movieFinishedTimer.setSingleShot(true);
m_movieFinishedTimer.setInterval(100);
connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(readStdout()));
connect(this, SIGNAL(readyReadStandardError()), this, SLOT(readStderr()));
connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished()));
connect(&m_movieFinishedTimer, SIGNAL(timeout()), this, SLOT(movieFinished()));
}
~QMPProcess()
{
#ifdef QMP_USE_YUVPIPE
if (m_yuvReader != NULL) {
m_yuvReader->stop();
}
#endif
if (m_fakeInputconf != nullptr) {
delete m_fakeInputconf;
}
}
// Starts the MPlayer process in idle mode
void start(QWidget *widget, const QStringList &args)
{
if (m_mode == QMPwidget::PipeMode) {
#ifdef QMP_USE_YUVPIPE
m_yuvReader = new QMPYuvReader(this);
#else
m_mode = QMPwidget::EmbeddedMode;
#endif
}
// Figure out the mplayer version in order to check if
// "-input nodefault-bindings" is available
bool useFakeInputconf = true;
QString version = mplayerVersion();
if (version.contains("SVN")) { // Check revision
QRegExp re("SVN-r([0-9]*)");
if (re.indexIn(version) > -1) {
int revision = re.cap(1).toInt();
if (revision >= 28878) {
useFakeInputconf = false;
}
}
}
QStringList myargs;
myargs += "-slave";
myargs += "-idle";
myargs += "-noquiet";
myargs += "-identify";
myargs += "-nomouseinput";
myargs += "-nokeepaspect";
myargs += "-monitorpixelaspect";
myargs += "1";
if (!useFakeInputconf) {
myargs += "-input";
myargs += "nodefault-bindings:conf=/dev/null";
} else {
#ifndef Q_WS_WIN
// Ugly hack for older versions of mplayer (used in kmplayer and other)
if (m_fakeInputconf == nullptr) {
m_fakeInputconf = new QTemporaryFile();
if (m_fakeInputconf->open()) {
writeFakeInputconf(m_fakeInputconf);
} else {
delete m_fakeInputconf;
m_fakeInputconf = nullptr;
}
}
if (m_fakeInputconf != nullptr) {
myargs += "-input";
myargs += QString("conf=%1").arg(m_fakeInputconf->fileName());
}
#endif
}
if (m_mode == QMPwidget::EmbeddedMode) {
myargs += "-wid";
myargs += QString::number((int)widget->winId());
if (!m_videoOutput.isEmpty()) {
myargs += "-vo";
myargs += m_videoOutput;
}
} else {
#ifdef QMP_USE_YUVPIPE
myargs += "-vo";
myargs += QString("yuv4mpeg:file=%1").arg(m_yuvReader->m_pipe);
#endif
}
myargs += args;
#ifdef QMP_DEBUG_OUTPUT
qDebug() << myargs;
#endif
QProcess::start(m_mplayerPath, myargs);
changeState(QMPwidget::IdleState);
if (m_mode == QMPwidget::PipeMode) {
#ifdef QMP_USE_YUVPIPE
connect(m_yuvReader, SIGNAL(imageReady(const QImage &)), widget, SLOT(displayImage(const QImage &)));
m_yuvReader->start();
#endif
}
}
QString mplayerVersion()
{
QProcess p;
p.start(m_mplayerPath, QStringList("-version"));
if (!p.waitForStarted()) {
return QString();
}
if (!p.waitForFinished()) {
return QString();
}
QString output = QString(p.readAll());
QRegExp re("MPlayer ([^ ]*)");
if (re.indexIn(output) > -1) {
return re.cap(1);
}
return output;
}
QProcess::ProcessState processState() const
{
return QProcess::state();
}
void writeCommand(const QString &command)
{
#ifdef QMP_DEBUG_OUTPUT
qDebug("in: \"%s\"", qPrintable(command));
#endif
QProcess::write(command.toLocal8Bit()+"\n");
}
void quit()
{
writeCommand("quit");
QProcess::waitForFinished(100);
if (QProcess::state() == QProcess::Running) {
QProcess::kill();
}
QProcess::waitForFinished(-1);
}
void pause()
{
writeCommand("pause");
}
void stop()
{
writeCommand("stop");
}
signals:
void stateChanged(int state);
void streamPositionChanged(double position);
void error(const QString &reason);
void readStandardOutput(const QString &line);
void readStandardError(const QString &line);
private slots:
void readStdout()
{
QStringList lines = QString::fromLocal8Bit(readAllStandardOutput()).split("\n", QString::SkipEmptyParts);
for (int i = 0; i < lines.count(); i++) {
lines[i].remove("\r");
#ifdef QMP_DEBUG_OUTPUT
qDebug("out: \"%s\"", qPrintable(lines[i]));
#endif
parseLine(lines[i]);
emit readStandardOutput(lines[i]);
}
}
void readStderr()
{
QStringList lines = QString::fromLocal8Bit(readAllStandardError()).split("\n", QString::SkipEmptyParts);
for (int i = 0; i < lines.count(); i++) {
lines[i].remove("\r");
#ifdef QMP_DEBUG_OUTPUT
qDebug("err: \"%s\"", qPrintable(lines[i]));
#endif
parseLine(lines[i]);
emit readStandardError(lines[i]);
}
}
void finished()
{
// Called if the *process* has finished
changeState(QMPwidget::NotStartedState);
}
void movieFinished()
{
if (m_state == QMPwidget::PlayingState) {
changeState(QMPwidget::IdleState);
}
}
private:
// Parses a line of MPlayer output
void parseLine(const QString &line)
{
if (line.startsWith("Playing ")) {
changeState(QMPwidget::LoadingState);
} else if (line.startsWith("Cache fill:")) {
changeState(QMPwidget::BufferingState);
} else if (line.startsWith("Starting playback...")) {
m_mediaInfo.ok = true; // No more info here
changeState(QMPwidget::PlayingState);
} else if (line.startsWith("File not found: ")) {
changeState(QMPwidget::ErrorState);
} else if (line.endsWith("ID_PAUSED")) {
changeState(QMPwidget::PausedState);
} else if (line.startsWith("ID_")) {
parseMediaInfo(line);
} else if (line.startsWith("No stream found")) {
changeState(QMPwidget::ErrorState, line);
} else if (line.startsWith("A:") || line.startsWith("V:")) {
if (m_state != QMPwidget::PlayingState) {
changeState(QMPwidget::PlayingState);
}
parsePosition(line);
} else if (line.startsWith("Exiting...")) {
changeState(QMPwidget::NotStartedState);
}
}
// Parses MPlayer's media identification output
void parseMediaInfo(const QString &line)
{
QStringList info = line.split("=");
if (info.count() < 2) {
return;
}
if (info[0] == "ID_VIDEO_FORMAT") {
m_mediaInfo.videoFormat = info[1];
} else if (info[0] == "ID_VIDEO_BITRATE") {
m_mediaInfo.videoBitrate = info[1].toInt();
} else if (info[0] == "ID_VIDEO_WIDTH") {
m_mediaInfo.size.setWidth(info[1].toInt());
} else if (info[0] == "ID_VIDEO_HEIGHT") {
m_mediaInfo.size.setHeight(info[1].toInt());
} else if (info[0] == "ID_VIDEO_FPS") {
m_mediaInfo.framesPerSecond = info[1].toDouble();
} else if (info[0] == "ID_AUDIO_FORMAT") {
m_mediaInfo.audioFormat = info[1];
} else if (info[0] == "ID_AUDIO_BITRATE") {
m_mediaInfo.audioBitrate = info[1].toInt();
} else if (info[0] == "ID_AUDIO_RATE") {
m_mediaInfo.sampleRate = info[1].toInt();
} else if (info[0] == "ID_AUDIO_NCH") {
m_mediaInfo.numChannels = info[1].toInt();
} else if (info[0] == "ID_LENGTH") {
m_mediaInfo.length = info[1].toDouble();
} else if (info[0] == "ID_SEEKABLE") {
m_mediaInfo.seekable = static_cast<bool>(info[1].toInt());
} else if (info[0].startsWith("ID_CLIP_INFO_NAME")) {
m_currentTag = info[1];
} else if (info[0].startsWith("ID_CLIP_INFO_VALUE") && !m_currentTag.isEmpty()) {
m_mediaInfo.tags.insert(m_currentTag, info[1]);
}
}
// Parsas MPlayer's position output
void parsePosition(const QString &line)
{
static QRegExp rx("[ :]");
QStringList info = line.split(rx, QString::SkipEmptyParts);
double oldpos = m_streamPosition;
for (int i = 0; i < info.count(); i++) {
if ( (info[i] == "V" || info[i] == "A") && info.count() > i) {
m_streamPosition = info[i+1].toDouble();
// If the movie is near its end, start a timer that will check whether
// the movie has really finished.
if (qAbs(m_streamPosition - m_mediaInfo.length) < 1) {
m_movieFinishedTimer.start();
}
}
}
if (oldpos != m_streamPosition) {
emit streamPositionChanged(m_streamPosition);
}
}
// Changes the current state, possibly emitting multiple signals
void changeState(QMPwidget::State state, const QString &comment = QString())
{
#ifdef QMP_USE_YUVPIPE
if (m_yuvReader != NULL && (state == QMPwidget::ErrorState || state == QMPwidget::NotStartedState)) {
m_yuvReader->stop();
m_yuvReader->deleteLater();
}
#endif
if (m_state == state) {
return;
}
if (m_state == QMPwidget::PlayingState) {
m_movieFinishedTimer.stop();
}
m_state = state;
emit stateChanged(m_state);
switch (m_state) {
case QMPwidget::NotStartedState:
resetValues();
break;
case QMPwidget::ErrorState:
emit error(comment);
resetValues();
break;
default: break;
}
}
// Resets the media info and position values
void resetValues()
{
m_mediaInfo = QMPwidget::MediaInfo();
m_streamPosition = -1;
}
// Writes a dummy input configuration to the given device
void writeFakeInputconf(QIODevice *device)
{
// Query list of supported keys
QProcess p;
p.start(m_mplayerPath, QStringList("-input") += "keylist");
if (!p.waitForStarted()) {
return;
}
if (!p.waitForFinished()) {
return;
}
QStringList keys = QString(p.readAll()).split("\n", QString::SkipEmptyParts);
// Write dummy command for each key
QTextStream out(device);
for (int i = 0; i < keys.count(); i++) {
keys[i].remove("\r");
out << keys[i] << " " << "ignored" << endl;
}
}
public:
QMPwidget::State m_state;
QString m_mplayerPath;
QString m_videoOutput;
QString m_pipe;
QMPwidget::Mode m_mode;
QMPwidget::MediaInfo m_mediaInfo;
double m_streamPosition; // This is the video position
QTimer m_movieFinishedTimer;
QString m_currentTag;
QTemporaryFile *m_fakeInputconf;
#ifdef QMP_USE_YUVPIPE
QPointer<QMPYuvReader> m_yuvReader;
#endif
};
// Initialize the media info structure
QMPwidget::MediaInfo::MediaInfo()
: videoBitrate(0), framesPerSecond(0), sampleRate(0), numChannels(0),
ok(false), length(0), seekable(false)
{
}
/*!
* \brief Constructor
*
* \param parent Parent widget
*/
QMPwidget::QMPwidget(QWidget *parent)
: QWidget(parent)
{
setFocusPolicy(Qt::StrongFocus);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
#ifdef QT_OPENGL_LIB
m_widget = new QMPOpenGLVideoWidget(this);
#else
m_widget = new QMPPlainVideoWidget(this);
#endif
QPalette p = palette();
p.setColor(QPalette::Window, Qt::black);
setPalette(p);
m_seekTimer.setInterval(50);
m_seekTimer.setSingleShot(true);
connect(&m_seekTimer, SIGNAL(timeout()), this, SLOT(delayedSeek()));
m_process = new QMPProcess(this);
connect(m_process, SIGNAL(stateChanged(int)), this, SLOT(mpStateChanged(int)));
connect(m_process, SIGNAL(streamPositionChanged(double)), this, SLOT(mpStreamPositionChanged(double)));
connect(m_process, SIGNAL(error(const QString &)), this, SIGNAL(error(const QString &)));
connect(m_process, SIGNAL(readStandardOutput(const QString &)), this, SIGNAL(readStandardOutput(const QString &)));
connect(m_process, SIGNAL(readStandardError(const QString &)), this, SIGNAL(readStandardError(const QString &)));
}
/*!
* \brief Destructor
* \details
* This function will ask the MPlayer process to quit and block until it has really
* finished.
*/
QMPwidget::~QMPwidget()
{
if (m_process->processState() == QProcess::Running) {
m_process->quit();
}
delete m_process;
}