PDA

View Full Version : a QProcess + QThread question



Joelito
1st March 2014, 17:50
I'm re-taking an old project about a bunch of files convert it, with ffmpeg, to mp3. At this point everything is good, only one thing left...unfreeze the GUI, this is my ffmpeg class:


#ifndef FFMPEG_RUNNER_HPP
#define FFMPEG_RUNNER_HPP

#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QProcess>

class MyFfmpegRunnerClass : public QObject {
Q_OBJECT
private:
QProcess *process;
QString curr_file;
QString _in_file;
QString _outdir;
bool delete_me;
public:
MyFfmpegRunnerClass(QObject *parent = 0) : QObject(parent)
{
process = new QProcess(this);
process->setStandardOutputFile("/dev/null");
process->setStandardErrorFile("/dev/null");
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleprocessExited(int, QProcess::ExitStatus)));
}
void SetArguments(const QString& in_file, const QString& outdir, bool be_deleted = false) {
_in_file = in_file;
_outdir = outdir;
delete_me = be_deleted;
}

~MyFfmpegRunnerClass() {
delete process;
qDebug() << "Class ffmpeg died";
}

protected slots:
void handleprocessExited(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Exited code: " << exitCode << ". ExitStatus: " << exitStatus;
if ( (exitCode == 0) && (delete_me) ) {
// you should delete source file here
}
emit doneJob();
}

void Start() {
QFileInfo fi(_in_file);
QString file = fi.baseName() + ".mp3";
QString out_file = QDir(_outdir).filePath(file);
qDebug() << "Converting: " << out_file;
curr_file = _in_file;

process->start("ffmpeg", QStringList() << "-i" << _in_file << "-f" << "mp3" << "-ab" << "128k" << out_file);
process->waitForFinished(-1);
process->close();
}

signals:
void doneJob();
};


#endif

And this is my GUI app:


#ifndef TEST01_HPP
#define TEST01_HPP

#include <QApplication>
#include <QWidget>
#include <QFileDialog>
#include <QDir>
#include <QDesktopServices> // Qt 4.2
#include <QThread>
#include <QMutex>
#include "ui_test01.h"
#include "ffmpeg_runner.hpp"

namespace Ui {
class Form;
};

class MyForm : public QWidget {
Q_OBJECT
private:
Ui::Form *ui;
QThread *thread;
MyFfmpegRunnerClass *ffmpeg;
QMutex mMutex; // do I need this? :-/
public:
MyForm(QWidget *root = 0) : QWidget(root), ui(new Ui::Form)
{
ui->setupUi(this);

/* above is beta, watch it!!! */
thread = new QThread;
ffmpeg = new MyFfmpegRunnerClass;
ffmpeg->moveToThread(thread);
connect(thread, SIGNAL(started()), ffmpeg, SLOT(Start()));
connect(ffmpeg, SIGNAL(doneJob()), thread, SLOT(quit()));
/* ... */

connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(handleRunButton()));
connect(ui->pushButton_3, SIGNAL(clicked()), this, SLOT(handleBrowseButton()));
ui->lineEdit->setText(QDesktopServices::storageLocation(QDesktop Services::MoviesLocation));
}
virtual ~MyForm() {
delete ffmpeg;
delete thread;
delete ui;
}

protected:
void handleTraverseDirectory(const QString& source_dir, const QString& target_dir, bool delete_me) {
if (!source_dir.isEmpty()) {
QDir folder(source_dir);
folder.setNameFilters(QStringList() << "*.mp3" << "*.mp4" << "*.flv" << "*.wav" << "*.ogg" << "*.3gp");
QStringList files = folder.entryList(QDir::NoDotAndDotDot|QDir::Files) ;
int index;
/* run above and wait until there's no more files to convert */
for (index = 0; index != files.size(); index++) {
QFileInfo fi(source_dir, files.at(index));
ffmpeg->SetArguments(fi.absoluteFilePath(), target_dir, delete_me);
thread->start(); // this is fine :)
thread->wait(); // keeps freeze :(
}
/* we should see the above message until last file is converted */
qDebug() << "Done.";
}
}

protected slots:
void handleBrowseButton() {
QString folder = QFileDialog::getExistingDirectory(this);
if (!folder.isEmpty()) ui->lineEdit->setText(folder);
}

void handleRunButton() {
QString source_dir = ui->lineEdit->text();
QString target_dir = QDesktopServices::storageLocation(QDesktopServices ::MusicLocation);
bool delete_me = true;
/* need here to be async? */
handleTraverseDirectory(source_dir, target_dir, delete_me);
}

};

#endif

I'm using threads to keep unfreeze my GUI, but:

1.- I need "waitForFinished" to let ffmpeg terminates the convertion (no big deal here)
2.- I'm trying to emulate what glib's g_thread_create (https://developer.gnome.org/glib/2.28/glib-Threads.html#g-thread-create) and what QtConcurrent::run do, but with threads to learn more about them with qprocess...

what would be left to do so the GUI keeps "alive"? Thanks

ChrisW67
1st March 2014, 20:22
Do not call waitForFinished() if you want the GUI to remain responsive. Disable elements of the UI that should not be usable during the life of the subprocess, start the sub process, use the finished() signal from the QProcess instance to determine when the process is done, and reenable parts pf the UI. You do not need threads at all to achieve this.

anda_skoa
1st March 2014, 20:30
Well, you tell the main thread to wait for the other thread, so it blocks until the other thread ends.
Which happens when your runner class emits doneJob() as this quits the thread's event loop.

So effectively you always have only one running thread, i.e. you have the very same behavior as when you would not have the second thread, just that the code is more complicated :)

This is also a classic example of not needing any additional threads at all, since QProcess is asynchronous itself.

Cheers,
_

Joelito
1st March 2014, 21:35
ok, I manage to remove threads and yes, QProcess is async, but..seems that tries to convert all at the same time, that's why I was using waitforfinish...my updated codes:


#ifndef TEST01_HPP
#define TEST01_HPP

#include <QApplication>
#include <QWidget>
#include <QFileDialog>
#include <QDir>
#include <QDesktopServices> // Qt 4.2
#include "ui_test01.h"
#include "ffmpeg_runner.hpp"

namespace Ui {
class Form;
};

class MyForm : public QWidget {
Q_OBJECT
private:
Ui::Form *ui;
MyFfmpegRunnerClass *ffmpeg;
public:
MyForm(QWidget *root = 0) : QWidget(root), ui(new Ui::Form)
{
ui->setupUi(this);

/* below is beta, watch it!!! */
ffmpeg = new MyFfmpegRunnerClass(this);
connect(ffmpeg, SIGNAL(startJob()), this, SLOT(handleStart()));
connect(ffmpeg, SIGNAL(endJob()), this, SLOT(handleEnd()));
/* ... */

connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(handleRunButton()));
connect(ui->pushButton_3, SIGNAL(clicked()), this, SLOT(handleBrowseButton()));
ui->lineEdit->setText(QDesktopServices::storageLocation(QDesktop Services::MoviesLocation));
}
virtual ~MyForm() {
delete ffmpeg;
delete ui;
}

protected:

signals:
void doneJob();

protected slots:
void handleBrowseButton() {
QString folder = QFileDialog::getExistingDirectory(this);
if (!folder.isEmpty()) ui->lineEdit->setText(folder);
}

void handleRunButton() {
QString source_dir = ui->lineEdit->text();
QString target_dir = QDesktopServices::storageLocation(QDesktopServices ::MusicLocation);
bool delete_me = true;
ffmpeg->Start(source_dir, target_dir, delete_me);
}

void handleStart() {
qDebug() << "We start the process";
}

void handleEnd() {
qDebug() << "Done :v";
}

};

#endif



#ifndef FFMPEG_RUNNER_HPP
#define FFMPEG_RUNNER_HPP

#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QProcess>

class MyFfmpegRunnerClass : public QObject {
Q_OBJECT
private:
QProcess *process;
QString curr_file;
bool delete_me;
int n_files;
public:
MyFfmpegRunnerClass(QObject *parent = 0) : QObject(parent)
{
n_files = 0;
process = new QProcess(this);
process->setStandardOutputFile("/dev/null");
process->setStandardErrorFile("/dev/null");
connect(process, SIGNAL(started()), this, SLOT(handleprocessStarted()));
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleprocessExited(int, QProcess::ExitStatus)));
}

void Start(const QString& srcdir, const QString& outdir, bool be_deleted = false) {
QDir folder(srcdir);
folder.setNameFilters(QStringList() << "*.mp3" << "*.mp4" << "*.flv" << "*.wav" << "*.ogg" << "*.3gp");
QStringList files = folder.entryList(QDir::NoDotAndDotDot|QDir::Files) ;
delete_me = be_deleted;
n_files = files.size();
int index;
for (index = 0; index != n_files; index++) {
QFileInfo fi(srcdir, files.at(index));
QString file = fi.baseName() + ".mp3";
curr_file = fi.absoluteFilePath();
QString out_file = QDir(outdir).filePath(file);
qDebug() << "Converting: " << curr_file;
process->start("ffmpeg", QStringList() << "-i" << curr_file << "-f" << "mp3" << "-ab" << "128k" << out_file);
}
// here we should wait until all files are converted, right?
qDebug() << "Why do you see this line even the process is still converting?"
}

~MyFfmpegRunnerClass() {
if (process->pid()) process->close();
delete process;
qDebug() << "Class ffmpeg died";
}

protected slots:
void handleprocessStarted() {
emit startJob();
}

void handleprocessExited(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Exited code: " << exitCode << ". ExitStatus: " << exitStatus;
if ( (exitCode == 0) && (delete_me) ) {
process->close();
emit endJob();
// you should delete source file here
}
}

signals:
void startJob();
void endJob();
};


#endif

ChrisW67
1st March 2014, 22:35
If you do not want to start a process for everything at the same time then don't code it to do that. Implement some sort of job queue from which you start the first n entries (where n == 1 is effectively serial processing). When a sub-process finishes look at the queue and start the next item if there is one.

Joelito
1st March 2014, 22:58
oh I see what you guys are telling me, things finally coming together, only one thing left, I'm getting a core dump segment fault, when I close my GUI, see my updated code:


#ifndef FFMPEG_RUNNER_HPP
#define FFMPEG_RUNNER_HPP

#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QProcess>

class MyFfmpegRunnerClass : public QObject {
Q_OBJECT
private:
QProcess *process;
QString srcdir;
QString outdir;
QString curr_file;
bool delete_me;
int n_files;
QStringList files;
int index;
public:
MyFfmpegRunnerClass(QObject *parent = 0) : QObject(parent)
{
/* init some values */
n_files = 0;
index = 0;
}

void Start(const QString& _srcdir, const QString& _outdir, bool be_deleted = false) {
/* grab values from parent's */
srcdir = _srcdir;
outdir = _outdir;
delete_me = be_deleted;
/* grab only selected files */
QDir folder(srcdir);
folder.setNameFilters(QStringList() << "*.mp3" << "*.mp4" << "*.flv" << "*.wav" << "*.ogg" << "*.3gp");
files = folder.entryList(QDir::NoDotAndDotDot|QDir::Files) ;
n_files = files.size(); // hold the number of them
emit aJob();
handleNewJob(); // begin job
}

~MyFfmpegRunnerClass() {
/* ok, test if a process is running before a suicide */
if (process && process->pid()) {
process->close();
delete process; // problem here?? :-/
}
qDebug() << "Class ffmpeg died -_-";
}

protected:
void handleNewJob() {
/* do we have more files for the job? */
if (index < n_files) {
process = new QProcess(this);
process->setStandardOutputFile("/dev/null");
process->setStandardErrorFile("/dev/null");
connect(process, SIGNAL(started()), this, SLOT(handleprocessStarted()));
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleprocessExited(int, QProcess::ExitStatus)));
QFileInfo fi(srcdir, files.at(index));
QString file = fi.baseName() + ".mp3";
curr_file = fi.absoluteFilePath();
QString out_file = QDir(outdir).filePath(file);
process->start("ffmpeg", QStringList() << "-i" << curr_file << "-f" << "mp3" << "-ab" << "128k" << out_file);
qDebug() << "Converting: " << curr_file;
index++;
} else {
emit noMoreJob();
}
}

protected slots:
void handleprocessStarted() {
emit startJob();
}

void handleprocessExited(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Exited code: " << exitCode << ". ExitStatus: " << exitStatus;
if ( (exitCode == 0) && (delete_me) ) {
process->close();
emit endJob();
delete process; // delete old, before new?
handleNewJob(); // look for more files
// you should delete source file here
}
}

signals:
void aJob();
void startJob();
void noMoreJob();
void endJob();
};


#endif



#ifndef TEST01_HPP
#define TEST01_HPP

#include <QApplication>
#include <QWidget>
#include <QFileDialog>
#include <QDir>
#include <QDesktopServices> // Qt 4.2
#include "ui_test01.h"
#include "ffmpeg_runner.hpp"

namespace Ui {
class Form;
};

class MyForm : public QWidget {
Q_OBJECT
private:
Ui::Form *ui;
MyFfmpegRunnerClass *ffmpeg;
public:
MyForm(QWidget *root = 0) : QWidget(root), ui(new Ui::Form)
{
ui->setupUi(this);

/* below is beta, watch it!!! */
ffmpeg = new MyFfmpegRunnerClass(this);
connect(ffmpeg, SIGNAL(aJob()), this, SLOT(handleAJob()));
connect(ffmpeg, SIGNAL(startJob()), this, SLOT(handleStart()));
connect(ffmpeg, SIGNAL(endJob()), this, SLOT(handleEnd()));
connect(ffmpeg, SIGNAL(noMoreJob()), this, SLOT(handleNoMoreJob()));
/* ... */

connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(handleRunButton()));
connect(ui->pushButton_3, SIGNAL(clicked()), this, SLOT(handleBrowseButton()));
ui->lineEdit->setText(QDesktopServices::storageLocation(QDesktop Services::MoviesLocation));
}
virtual ~MyForm() {
delete ffmpeg;
delete ui;
}

protected slots:
void handleBrowseButton() {
QString folder = QFileDialog::getExistingDirectory(this);
if (!folder.isEmpty()) ui->lineEdit->setText(folder);
}

void handleRunButton() {
QString source_dir = ui->lineEdit->text();
QString target_dir = QDesktopServices::storageLocation(QDesktopServices ::MusicLocation);
bool delete_me = true;
ffmpeg->Start(source_dir, target_dir, delete_me);
qDebug() << "Say hello";
}

void handleStart() {
qDebug() << "We have a new job";
}

void handleEnd() {
qDebug() << "We don't have the job";
}

void handleAJob() {
qDebug() << "We should disable some stuff here";
}

void handleNoMoreJob() {
qDebug() << "We should enable some stuff here";
}

};

#endif

The message "Class ffmpeg died -_-" doesn't showed when I close my app, what could it be?

anda_skoa
2nd March 2014, 00:54
You don't need a new QProcess per job, one should do.

If you want to use a new QProcess instance, do not delete the old one directly but use deleteLater(). You are at that point in a slot connected to the process, directly deleting it is not very good.

Also kill the process in the runner's destructor.

Cheers,
_

Joelito
2nd March 2014, 02:09
Wow, I think I got it, no errors, no segment fault:


#ifndef FFMPEG_RUNNER_HPP
#define FFMPEG_RUNNER_HPP

#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QProcess>

class MyFfmpegRunnerClass : public QObject {
Q_OBJECT
private:
QProcess *process;
QString srcdir;
QString outdir;
QString curr_file;
bool delete_me;
int n_files;
QStringList files;
int index;
public:
MyFfmpegRunnerClass(QObject *parent = 0) : QObject(parent)
{
/* init some values */
n_files = 0;
index = 0;

/* we should use one QProcess instance, thread safe? */
process = new QProcess(this);
process->setStandardOutputFile("/dev/null");
process->setStandardErrorFile("/dev/null");
connect(process, SIGNAL(started()), this, SLOT(handleprocessStarted()));
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(handleprocessExited(int, QProcess::ExitStatus)));
//connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); <-- We don't need this, do we?
}

void Start(const QString& _srcdir, const QString& _outdir, bool be_deleted = false) {
/* grab values from parent's */
srcdir = _srcdir;
outdir = _outdir;
delete_me = be_deleted;
/* grab only selected files */
QDir folder(srcdir);
folder.setNameFilters(QStringList() << "*.mp3" << "*.mp4" << "*.flv" << "*.wav" << "*.ogg" << "*.3gp");
files = folder.entryList(QDir::NoDotAndDotDot|QDir::Files) ;
n_files = files.size(); // hold the number of them
emit aJob();
handleNewJob(); // begin a job
}

~MyFfmpegRunnerClass() {
/* ok, they kill us from parent's, so test if a process is running before a suicide */
if (process && process->pid()) { //
process->close();
delete process; // This is not a problem anymore :)
}
qDebug() << "Class ffmpeg died -_-";
}

protected:
void handleNewJob() {
/* do we have some files for the job? */
if (index < n_files) {
QFileInfo fi(srcdir, files.at(index));
QString file = fi.baseName() + ".mp3";
curr_file = fi.absoluteFilePath();
QString out_file = QDir(outdir).filePath(file);
process->start("ffmpeg", QStringList() << "-i" << curr_file << "-f" << "mp3" << "-ab" << "128k" << out_file);
qDebug() << "Converting: " << curr_file;
index++;
} else {
emit noMoreJob();
}
}

protected slots:
void handleprocessStarted() {
emit startJob();
}

void handleprocessExited(int exitCode, QProcess::ExitStatus exitStatus) {
qDebug() << "Exited code: " << exitCode << ". ExitStatus: " << exitStatus;
if ( (exitCode == 0) && (delete_me) ) {
emit endJob();
handleNewJob(); // look for more files
// you should delete source file here below
}
}

signals:
void aJob();
void startJob();
void noMoreJob();
void endJob();
};


#endif



#ifndef TEST01_HPP
#define TEST01_HPP

#include <QApplication>
#include <QWidget>
#include <QFileDialog>
#include <QDir>
#include <QDesktopServices> // Qt 4.2
#include "ui_test01.h"
#include "ffmpeg_runner.hpp"

namespace Ui {
class Form;
};

class MyForm : public QWidget {
Q_OBJECT
private:
Ui::Form *ui;
MyFfmpegRunnerClass *ffmpeg;
public:
MyForm(QWidget *root = 0) : QWidget(root), ui(new Ui::Form)
{
ui->setupUi(this);

/* below is beta, watch it!!! */
ffmpeg = new MyFfmpegRunnerClass(this);
connect(ffmpeg, SIGNAL(aJob()), this, SLOT(handleAJob()));
connect(ffmpeg, SIGNAL(startJob()), this, SLOT(handleStart()));
connect(ffmpeg, SIGNAL(endJob()), this, SLOT(handleEnd()));
connect(ffmpeg, SIGNAL(noMoreJob()), this, SLOT(handleNoMoreJob()));
/* ... */

connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(handleRunButton()));
connect(ui->pushButton_3, SIGNAL(clicked()), this, SLOT(handleBrowseButton()));
ui->lineEdit->setText(QDesktopServices::storageLocation(QDesktop Services::MoviesLocation));
}
virtual ~MyForm() {
delete ffmpeg;
delete ui;
}

protected slots:
void handleBrowseButton() {
QString folder = QFileDialog::getExistingDirectory(this);
if (!folder.isEmpty()) ui->lineEdit->setText(folder);
}

void handleRunButton() {
QString source_dir = ui->lineEdit->text();
QString target_dir = QDesktopServices::storageLocation(QDesktopServices ::MusicLocation);
bool delete_me = true;
ffmpeg->Start(source_dir, target_dir, delete_me);
}

void handleStart() {
qDebug() << "We have a new job";
}

void handleEnd() {
qDebug() << "We don't have the job";
}

void handleAJob() {
qDebug() << "We should disable some stuff here";
}

void handleNoMoreJob() {
qDebug() << "We should enable some stuff here";
}

};

#endif

in your opinion, everything is ok? No memory leaks?

anda_skoa
2nd March 2014, 10:34
Looks good to me.

One possible thing that your job queue based processing allows you to do is to emit progress information, e.g. something like "Processing file 2 of 23".

Cheers,
_

Joelito
2nd March 2014, 15:00
ok, thanks for your help :)