added ffmpeg launcher

This commit is contained in:
Martchus 2015-08-25 19:53:54 +02:00
parent 72bdfa1597
commit a03a2a0e5b
7 changed files with 437 additions and 28 deletions

View File

@ -1,2 +1,42 @@
# dbus-soundrecorder
Records sound from Pulse Audio using ffmpeg. Uses dbus to determine current song, album and artist.
Records sound from Pulse Audio using ffmpeg.
Uses D-Bus to determine current song, album, artist and other meta data
provided by the media player application via MPRIS D-Bus service.
When the next song start, the recorder automatically starts a new file
and sets available meta data.
## Usage
```
dbus-soundrecorder record [options]
```
Here's a simple example:
First, get a list of your Pulse Audio sinks:
```
pactl list short sinks
```
You can also create a new virtual Pulse Audio sink:
```
pactl load-module module-null-sink sink_name=virtual1
```
In any case, you should ensure that the media player uses the sink (eg. using pavucontrol).
Then start the recorder. You need to specify the media player application and the sink:
```
dbus-soundrecorder -a yfitops -s virtual1.monitor -o "-c:a libfdk_aac -vbr 4 -ar 44100"
```
As you can see, it is also possible to specify options for ffmpeg. However input sink,
output file and meta data are provided by the recorder and shouldn't be specified.
For all available options, use the --help command.
## Build instructions
The application depends on the c++utilities library. It is built in the same way as c++utilities.
The following Qt 5 modules are requried: core dbus

View File

@ -1,4 +1,7 @@
projectname = dbus-soundrecorder
appname = "D-Bus Sound Recorder"
appauthor = Martchus
appurl = "https://github.com/$${appauthor}/$${projectname}"
VERSION = 1.0.0
# include ../../common.pri when building as part of a subdirs project; otherwise include general.pri
@ -15,10 +18,12 @@ CONFIG += console
QT += core dbus
SOURCES += main.cpp \
playerwatcher.cpp
playerwatcher.cpp \
ffmpeglauncher.cpp
HEADERS += \
playerwatcher.h
playerwatcher.h \
ffmpeglauncher.h
DBUS_INTERFACES += \
org.freedesktop.DBus.Properties.xml \

177
ffmpeglauncher.cpp Normal file
View File

@ -0,0 +1,177 @@
#include "ffmpeglauncher.h"
#include "playerwatcher.h"
#include <c++utilities/io/inifile.h>
#include <c++utilities/conversion/stringconversion.h>
#include <iostream>
#include <fstream>
using namespace std;
using namespace IoUtilities;
using namespace ConversionUtilities;
namespace DBusSoundRecorder {
inline ostream &operator <<(ostream &stream, const QString &str)
{
stream << str.toLocal8Bit().data();
return stream;
}
FfmpegLauncher::FfmpegLauncher(PlayerWatcher &watcher, QObject *parent) :
QObject(parent),
m_watcher(watcher),
m_sink(QStringLiteral("default")),
m_options(),
m_targetDir(QStringLiteral(".")),
m_targetExtension(QStringLiteral(".m4a")),
m_ffmpeg(new QProcess(this))
{
connect(&watcher, &PlayerWatcher::nextSong, this, &FfmpegLauncher::nextSong);
connect(m_ffmpeg, &QProcess::started, this, &FfmpegLauncher::ffmpegStarted);
connect(m_ffmpeg, static_cast<void(QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this, &FfmpegLauncher::ffmpegError);
connect(m_ffmpeg, static_cast<void(QProcess::*)(int)>(&QProcess::finished), this, &FfmpegLauncher::ffmpegFinished);
m_ffmpeg->setProgram(QStringLiteral("ffmpeg"));
m_ffmpeg->setProcessChannelMode(QProcess::ForwardedChannels);
}
void addMetaData(QStringList &args, const QString &field, const QString &value)
{
if(!value.isEmpty()) {
args << QStringLiteral("-metadata");
args << QStringLiteral("%1=%2").arg(field, value);
}
}
void FfmpegLauncher::nextSong()
{
// terminate/kill the current process
if(m_ffmpeg->state() != QProcess::NotRunning) {
m_ffmpeg->terminate();
m_ffmpeg->waitForFinished(250);
if(m_ffmpeg->state() != QProcess::NotRunning) {
m_ffmpeg->kill();
m_ffmpeg->waitForFinished(250);
if(m_ffmpeg->state() != QProcess::NotRunning) {
throw runtime_error("Unable to terminate/kill ffmpeg process.");
}
}
}
if(m_watcher.isPlaying()) {
// determine output file, create target directory
static const QString miscCategory(QStringLiteral("misc"));
static const QString unknownTitle(QStringLiteral("unknown track"));
QDir targetDir(QStringLiteral("%1/%2").arg(m_watcher.artist().isEmpty() ? miscCategory : m_watcher.artist(), m_watcher.artist().isEmpty() ? miscCategory : m_watcher.album()));
if(!m_targetDir.mkpath(targetDir.path())) {
cerr << "Error: Can not create target directory: " << targetDir.absolutePath() << endl;
return;
}
// determine track number
QString number;
QString length;
if(m_watcher.trackNumber()) {
if(m_watcher.diskNumber()) {
number = QStringLiteral("%2-%1").arg(m_watcher.trackNumber(), 2, 10, QLatin1Char('0')).arg(m_watcher.diskNumber());
} else {
number = QStringLiteral("%1").arg(m_watcher.trackNumber(), 2, 10, QLatin1Char('0'));
}
}
if(!number.isEmpty()) {
number.append(QStringLiteral(" - "));
}
// determine additional info
// - from a file called info.ini in the album directory
// - currently only track length is supported (used to get rid of advertisements at the end)
if(targetDir.exists(QStringLiteral("info.ini"))) {
fstream infoFile;
infoFile.exceptions(ios_base::badbit | ios_base::failbit);
try {
infoFile.open((targetDir.path() + QStringLiteral("/info.ini")).toLocal8Bit().data(), ios_base::in);
IniFile infoIni;
infoIni.parse(infoFile);
// read length scope, only possible if track number known because the track number is used for mapping
if(m_watcher.trackNumber()) {
try {
const auto &lengthScope = infoIni.data().at("length");
for(const auto &entry : lengthScope) {
try {
if(stringToNumber<unsigned int>(entry.first) == m_watcher.trackNumber()) {
// length entry for this track
length = QString::fromLocal8Bit(entry.second.data());
break;
}
} catch( const ConversionException &) {
cerr << "Warning: Ignoring non-numeric key \"" << entry.first << "\" in info.ini." << endl;
}
}
} catch(const out_of_range &) {
// no length for the current track specified
}
}
} catch(const ios_base::failure &) {
cerr << "Warning: Can't parse info.ini because an IO error occured." << endl;
}
}
// determine target name/path
QString targetName(QStringLiteral("%3%1%2").arg(m_watcher.title().isEmpty() ? unknownTitle : m_watcher.title(), m_targetExtension, number));
unsigned int count = 1;
while(targetDir.exists(targetName)) {
++count;
targetName = QStringLiteral("%3%1 (%4)%2").arg(m_watcher.title().isEmpty() ? unknownTitle : m_watcher.title(), m_targetExtension, number).arg(count);
}
QString targetPath = targetDir.absoluteFilePath(targetName);
// set input device
QStringList args;
args << QStringLiteral("-f");
args << QStringLiteral("pulse");
args << QStringLiteral("-i");
args << m_sink;
// set length
if(!length.isEmpty()) {
args << "-t";
args << length;
}
// set additional options
args << m_options;
// set meta data
addMetaData(args, QStringLiteral("title"), m_watcher.title());
addMetaData(args, QStringLiteral("album"), m_watcher.album());
addMetaData(args, QStringLiteral("artist"), m_watcher.artist());
addMetaData(args, QStringLiteral("genre"), m_watcher.genre());
addMetaData(args, QStringLiteral("year"), m_watcher.year());
if(m_watcher.trackNumber()) {
addMetaData(args, QStringLiteral("track"), QString::number(m_watcher.trackNumber()));
}
if(m_watcher.diskNumber()) {
addMetaData(args, QStringLiteral("disk"), QString::number(m_watcher.diskNumber()));
}
// set output file
args << targetPath;
m_ffmpeg->setArguments(args);
// start process
m_ffmpeg->start();
}
}
void FfmpegLauncher::ffmpegStarted()
{
cerr << "Started ffmpeg: ";
cerr << m_ffmpeg->program();
for(const auto &arg : m_ffmpeg->arguments()) {
cerr << ' ' << arg;
}
cerr << endl;
}
void FfmpegLauncher::ffmpegError()
{
cerr << "Failed to start ffmpeg: " << m_ffmpeg->errorString();
}
void FfmpegLauncher::ffmpegFinished(int exitCode)
{
cerr << "FFmpeg finished with exit code " << exitCode << endl;
}
}

66
ffmpeglauncher.h Normal file
View File

@ -0,0 +1,66 @@
#ifndef FFMPEGLAUNCHER_H
#define FFMPEGLAUNCHER_H
#include <QObject>
#include <QProcess>
#include <QDir>
namespace DBusSoundRecorder {
class PlayerWatcher;
class FfmpegLauncher : public QObject
{
Q_OBJECT
public:
explicit FfmpegLauncher(PlayerWatcher &watcher, QObject *parent = nullptr);
void setSink(const QString &sinkName);
void setFfmpegBinary(const QString &path);
void setFfmpegOptions(const QString &options);
void setTargetDir(const QString &path);
void setTargetExtension(const QString &extension);
private slots:
void nextSong();
void ffmpegStarted();
void ffmpegError();
void ffmpegFinished(int exitCode);
private:
PlayerWatcher &m_watcher;
QString m_sink;
QStringList m_options;
QDir m_targetDir;
QString m_targetExtension;
QProcess *m_ffmpeg;
};
inline void FfmpegLauncher::setSink(const QString &sinkName)
{
m_sink = sinkName;
}
inline void FfmpegLauncher::setFfmpegBinary(const QString &path)
{
m_ffmpeg->setProgram(path);
}
inline void FfmpegLauncher::setFfmpegOptions(const QString &options)
{
m_options = options.split(QChar(' '), QString::SkipEmptyParts);
}
inline void FfmpegLauncher::setTargetDir(const QString &path)
{
m_targetDir = QDir(path);
}
inline void FfmpegLauncher::setTargetExtension(const QString &extension)
{
m_targetExtension = extension.startsWith(QChar('.')) ? extension : QStringLiteral(".") + extension;
}
}
#endif // FFMPEGLAUNCHER_H

View File

@ -1,4 +1,5 @@
#include "playerwatcher.h"
#include "ffmpeglauncher.h"
#include <c++utilities/application/argumentparser.h>
#include <c++utilities/application/failure.h>
@ -14,8 +15,11 @@ using namespace DBusSoundRecorder;
int main(int argc, char *argv[])
{
// setup the argument parser
SET_APPLICATION_INFO;
ArgumentParser parser;
HelpArgument helpArg(parser);
Argument recordArg("record", "r", "starts recording");
recordArg.setDenotesOperation(true);
Argument applicationArg("application", "a", "specifies the application providing meta information via D-Bus interface");
applicationArg.setRequired(true);
applicationArg.setValueNames({"name"});
@ -28,20 +32,57 @@ int main(int argc, char *argv[])
targetDirArg.setValueNames({"path"});
targetDirArg.setRequiredValueCount(1);
targetDirArg.setCombinable(true);
Argument targetExtArg("target-extension", "e", "specifies the target extension (default is .m4a)");
targetExtArg.setValueNames({"extension"});
targetExtArg.setRequiredValueCount(1);
targetExtArg.setCombinable(true);
Argument ffmpegBinArg("ffmpeg-bin", "f", "specifies the path to the ffmpeg binary");
ffmpegBinArg.setValueNames({"path"});
ffmpegBinArg.setRequiredValueCount(1);
ffmpegBinArg.setCombinable(true);
Argument ffmpegOptions("ffmpeg-options", "o", "specifies options for ffmpeg");
ffmpegOptions.setValueNames({"options"});
ffmpegOptions.setRequiredValueCount(1);
ffmpegOptions.setCombinable(true);
parser.setMainArguments({&applicationArg, &sinkArg, &targetDirArg, &ffmpegOptions, &helpArg});
recordArg.setSecondaryArguments({&applicationArg, &sinkArg, &targetDirArg, &targetExtArg, &ffmpegBinArg, &ffmpegOptions});
parser.setMainArguments({&recordArg, &helpArg});
parser.setIgnoreUnknownArguments(false);
// parse command line arguments
try {
parser.parseArgs(argc, argv);
} catch (Failure &e) {
} catch (const Failure &e) {
cerr << "Unable to parse arguments: " << e.what() << endl;
return 2;
}
QCoreApplication app(argc, argv);
PlayerWatcher watcher(QString::fromLocal8Bit(applicationArg.values().front().data()));
return app.exec();
try {
if(recordArg.isPresent()) {
// start watching/recording
cerr << "Watching MPRIS service of the specified application \"" << applicationArg.values().front() << "\" ..." << endl;
// create app loop, player watcher and ffmpeg launcher
QCoreApplication app(argc, argv);
PlayerWatcher watcher(QString::fromLocal8Bit(applicationArg.values().front().data()));
FfmpegLauncher ffmpeg(watcher);
// pass specified args to ffmpeg launcher
if(sinkArg.isPresent()) {
ffmpeg.setSink(QString::fromLocal8Bit(sinkArg.values().front().data()));
}
if(ffmpegBinArg.isPresent()) {
ffmpeg.setFfmpegBinary(QString::fromLocal8Bit(ffmpegBinArg.values().front().data()));
}
if(ffmpegOptions.isPresent()) {
ffmpeg.setFfmpegOptions(QString::fromLocal8Bit(ffmpegOptions.values().front().data()));
}
if(targetDirArg.isPresent()) {
ffmpeg.setTargetDir(QString::fromLocal8Bit(targetDirArg.values().front().data()));
}
if(targetExtArg.isPresent()) {
ffmpeg.setTargetExtension(QString::fromLocal8Bit(targetExtArg.values().front().data()));
}
// enter app loop
return app.exec();
}
} catch(const runtime_error &e) {
cerr << "Fatal error: " << e.what() << endl;
}
}

View File

@ -24,10 +24,33 @@ PlayerWatcher::PlayerWatcher(const QString &appName, QObject *parent) :
m_serviceWatcher(new QDBusServiceWatcher(m_service, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this)),
m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(m_service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
m_playerInterface(new OrgMprisMediaPlayer2PlayerInterface(m_service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this)),
m_trackNumber(0)
m_isPlaying(false),
m_trackNumber(0),
m_diskNumber(0)
{
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &PlayerWatcher::serviceOwnerChanged);
connect(m_propertiesInterface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &PlayerWatcher::propertiesChanged);
propertiesChanged();
}
void PlayerWatcher::play()
{
m_playerInterface->Play();
}
void PlayerWatcher::stop()
{
m_playerInterface->Stop();
}
void PlayerWatcher::pause()
{
m_playerInterface->Pause();
}
void PlayerWatcher::playPause()
{
m_playerInterface->PlayPause();
}
void PlayerWatcher::serviceOwnerChanged(const QString &service, const QString &oldOwner, const QString &newOwner)
@ -40,27 +63,55 @@ void PlayerWatcher::serviceOwnerChanged(const QString &service, const QString &o
}
}
void PlayerWatcher::propertiesChanged(const QString &, const QVariantMap &, const QStringList &)
void PlayerWatcher::propertiesChanged()
{
// get meta data
QVariantMap metadata = m_playerInterface->metadata();
QString title = metadata.value(QStringLiteral("xesam:title")).toString();
QString album = metadata.value(QStringLiteral("xesam:album")).toString();
QString artist = metadata.value(QStringLiteral("xesam:artist")).toString();
// use title, album and artist to identify song
if(m_title != title || m_album != album || m_artist != artist) {
// next song playing
m_title = title;
m_album = album;
m_artist = artist;
// read additional meta data
m_year = metadata.value(QStringLiteral("xesam:contentCreated")).toString();
m_genre = metadata.value(QStringLiteral("xesam:genre")).toString();
m_trackNumber = metadata.value(QStringLiteral("xesam:tracknumber")).toUInt();
// notify
cerr << "Next song: " << m_title << endl;
emit nextSong();
if(!m_playerInterface->playbackStatus().compare(QLatin1String("playing"), Qt::CaseInsensitive)) {
if(!m_isPlaying) {
m_isPlaying = true;
cerr << "Playback started" << endl;
}
QVariantMap metadata = m_playerInterface->metadata();
QString title = metadata.value(QStringLiteral("xesam:title")).toString();
QString album = metadata.value(QStringLiteral("xesam:album")).toString();
QString artist = metadata.value(QStringLiteral("xesam:artist")).toString();
// use title, album and artist to identify song
if(m_title != title || m_album != album || m_artist != artist) {
// next song playing
m_title = title;
m_album = album;
m_artist = artist;
// read additional meta data
m_year = metadata.value(QStringLiteral("xesam:contentCreated")).toString();
m_genre = metadata.value(QStringLiteral("xesam:genre")).toString();
m_trackNumber = metadata.value(QStringLiteral("xesam:tracknumber")).toUInt();
if(!m_trackNumber) {
m_trackNumber = metadata.value(QStringLiteral("xesam:trackNumber")).toUInt();
}
m_diskNumber = metadata.value(QStringLiteral("xesam:discnumber")).toUInt();
if(!m_diskNumber) {
m_diskNumber = metadata.value(QStringLiteral("xesam:discNumber")).toUInt();
}
m_length = metadata.value(QStringLiteral("xesam:length")).toULongLong();
// notify
cerr << "Next song: " << m_title << endl;
if(!m_isPlaying) {
emit playbackStarted();
}
emit nextSong();
} else if(!m_isPlaying) {
emit playbackStarted();
}
} else if(m_isPlaying) {
m_isPlaying = false;
cerr << "Playback stopped" << endl;
emit playbackStopped();
}
}
void PlayerWatcher::seeked(qlonglong pos)
{
cerr << "Seeked: " << pos << endl;
}
}

View File

@ -16,33 +16,52 @@ class PlayerWatcher : public QObject
public:
explicit PlayerWatcher(const QString &appName, QObject *parent = nullptr);
void play();
void stop();
void pause();
void playPause();
bool isPlaying() const;
const QString &title() const;
const QString &album() const;
const QString &artist() const;
const QString &year() const;
const QString &genre() const;
unsigned int trackNumber() const;
unsigned int diskNumber() const;
unsigned long long length() const;
signals:
void nextSong();
void playbackStarted();
void playbackStopped();
private slots:
void serviceOwnerChanged(const QString &service, const QString &oldOwner, const QString &newOwner);
void propertiesChanged(const QString &, const QVariantMap &, const QStringList &);
void propertiesChanged();
void seeked(qlonglong pos);
private:
QString m_service;
QDBusServiceWatcher *m_serviceWatcher;
OrgFreedesktopDBusPropertiesInterface *m_propertiesInterface;
OrgMprisMediaPlayer2PlayerInterface *m_playerInterface;
bool m_isPlaying;
QString m_title;
QString m_album;
QString m_artist;
QString m_year;
QString m_genre;
unsigned int m_trackNumber;
unsigned int m_diskNumber;
unsigned long long m_length;
};
inline bool PlayerWatcher::isPlaying() const
{
return m_isPlaying;
}
inline const QString &PlayerWatcher::title() const
{
return m_title;
@ -73,6 +92,16 @@ inline unsigned int PlayerWatcher::trackNumber() const
return m_trackNumber;
}
inline unsigned int PlayerWatcher::diskNumber() const
{
return m_diskNumber;
}
inline unsigned long long PlayerWatcher::length() const
{
return m_length;
}
}
#endif // PLAYERWATCHER_H