Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ qt_standard_project_setup(REQUIRES 6.4)
qt_add_executable(applinux
main.cpp
logger.h
mediacontroller.cpp
mediacontroller.h
media/mediacontroller.cpp
media/mediacontroller.h
airpods_packets.h
trayiconmanager.cpp
trayiconmanager.h
Expand All @@ -32,6 +32,8 @@ qt_add_executable(applinux
thirdparty/QR-Code-generator/qrcodegen.hpp
QRCodeImageProvider.hpp
eardetection.hpp
media/playerstatuswatcher.cpp
media/playerstatuswatcher.h
)

qt_add_qml_module(applinux
Expand Down
35 changes: 1 addition & 34 deletions linux/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#include "airpods_packets.h"
#include "logger.h"
#include "mediacontroller.h"
#include "media/mediacontroller.h"
#include "trayiconmanager.h"
#include "enums.h"
#include "battery.hpp"
Expand Down Expand Up @@ -66,7 +66,6 @@ class AirPodsTrayApp : public QObject {
// Initialize MediaController and connect signals
mediaController = new MediaController(this);
connect(mediaController, &MediaController::mediaStateChanged, this, &AirPodsTrayApp::handleMediaStateChange);
mediaController->initializeMprisInterface();
mediaController->followMediaChanges();

monitor = new BluetoothMonitor(this);
Expand Down Expand Up @@ -795,13 +794,6 @@ private slots:
process.waitForFinished();
QString output = process.readAllStandardOutput().trimmed();
LOG_INFO("Bluetoothctl output: " << output);
if (output.contains("Connection successful")) {
LOG_INFO("Connection successful, proceeding with L2CAP connection");
QBluetoothAddress btAddress(m_deviceInfo->bluetoothAddress());
forceL2capConnection(btAddress);
} else {
LOG_ERROR("Connection failed, cannot proceed with L2CAP connection");
}
}
QBluetoothLocalDevice localDevice;
const QList<QBluetoothAddress> connectedDevices = localDevice.connectedDevices();
Expand All @@ -816,31 +808,6 @@ private slots:
LOG_WARN("AirPods not found among connected devices");
}

void forceL2capConnection(const QBluetoothAddress &address) {
LOG_INFO("Retrying L2CAP connection for up to 10 seconds...");
QBluetoothDeviceInfo device(address, "", 0);
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < 10000) {
QProcess bcProcess;
bcProcess.start("bluetoothctl", QStringList() << "connect" << address.toString());
bcProcess.waitForFinished();
QString output = bcProcess.readAllStandardOutput().trimmed();
LOG_INFO("Bluetoothctl output: " << output);
if (output.contains("Connection successful")) {
connectToDevice(device);
QThread::sleep(1);
if (socket && socket->isOpen()) {
LOG_INFO("Successfully connected to device: " << address.toString());
return;
}
} else {
LOG_WARN("Connection attempt failed, retrying...");
}
}
LOG_ERROR("Failed to connect to device within 10 seconds: " << address.toString());
}

void initializeBluetooth() {
connectToPhone();

Expand Down
160 changes: 94 additions & 66 deletions linux/mediacontroller.cpp → linux/media/mediacontroller.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "mediacontroller.h"
#include "logger.h"
#include "eardetection.hpp"
#include "playerstatuswatcher.h"

#include <QDebug>
#include <QProcess>
Expand All @@ -9,34 +10,6 @@
#include <QDBusConnectionInterface>

MediaController::MediaController(QObject *parent) : QObject(parent) {
// No additional initialization required here
}

void MediaController::initializeMprisInterface() {
QStringList services =
QDBusConnection::sessionBus().interface()->registeredServiceNames();
QString mprisService;

for (const QString &service : services) {
if (service.startsWith("org.mpris.MediaPlayer2.") &&
service != "org.mpris.MediaPlayer2") {
mprisService = service;
break;
}
}

if (!mprisService.isEmpty()) {
mprisInterface = new QDBusInterface(mprisService, "/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player",
QDBusConnection::sessionBus(), this);
if (!mprisInterface->isValid()) {
LOG_ERROR("Failed to initialize MPRIS interface for service: ") << mprisService;
} else {
LOG_INFO("Connected to MPRIS service: " << mprisService);
}
} else {
LOG_WARN("No active MPRIS media players found");
}
}

void MediaController::handleEarDetection(EarDetection *earDetection)
Expand Down Expand Up @@ -71,12 +44,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)

if (shouldPause && isActiveOutputDeviceAirPods())
{
QProcess process;
process.start("playerctl", QStringList() << "status");
process.waitForFinished();
QString playbackStatus = process.readAllStandardOutput().trimmed();
LOG_DEBUG("Playback status: " << playbackStatus);
if (playbackStatus == "Playing")
if (m_mediaState == Playing)
{
pause();
}
Expand All @@ -91,17 +59,7 @@ void MediaController::handleEarDetection(EarDetection *earDetection)
// Resume if conditions are met and we previously paused
if (shouldResume && wasPausedByApp && isActiveOutputDeviceAirPods())
{
int result = QProcess::execute("playerctl", QStringList() << "play");
LOG_DEBUG("Executed 'playerctl play' with result: " << result);
if (result == 0)
{
LOG_INFO("Resumed playback via Playerctl");
wasPausedByApp = false;
}
else
{
LOG_ERROR("Failed to resume playback via Playerctl");
}
play();
}
}
else
Expand All @@ -118,16 +76,14 @@ void MediaController::setEarDetectionBehavior(EarDetectionBehavior behavior)
}

void MediaController::followMediaChanges() {
playerctlProcess = new QProcess(this);
connect(playerctlProcess, &QProcess::readyReadStandardOutput, this,
[this]() {
QString output =
playerctlProcess->readAllStandardOutput().trimmed();
LOG_DEBUG("Playerctl output: " << output);
MediaState state = mediaStateFromPlayerctlOutput(output);
playerStatusWatcher = new PlayerStatusWatcher("", this);
connect(playerStatusWatcher, &PlayerStatusWatcher::playbackStatusChanged,
this, [this](const QString &status)
{
LOG_DEBUG("Playback status changed: " << status);
MediaState state = mediaStateFromPlayerctlOutput(status);
emit mediaStateChanged(state);
});
playerctlProcess->start("playerctl", QStringList() << "--follow" << "status");
}

bool MediaController::isActiveOutputDeviceAirPods() {
Expand Down Expand Up @@ -226,28 +182,100 @@ MediaController::MediaState MediaController::mediaStateFromPlayerctlOutput(
}
}

void MediaController::pause() {
int result = QProcess::execute("playerctl", QStringList() << "pause");
LOG_DEBUG("Executed 'playerctl pause' with result: " << result);
if (result == 0)
QDBusInterface *MediaController::getMediaPlayerInterface()
{
// List all media player services
QDBusConnection sessionBus = QDBusConnection::sessionBus();
QDBusInterface dbusInterface("org.freedesktop.DBus", "/org/freedesktop/DBus",
"org.freedesktop.DBus", sessionBus);
QDBusReply<QStringList> reply = dbusInterface.call("ListNames");

if (!reply.isValid())
{
LOG_INFO("Paused playback via Playerctl");
LOG_ERROR("Failed to list DBus services: " << reply.error().message());
return nullptr;
}

QStringList services = reply.value();
QString mediaPlayerService;

for (const QString &service : services)
{
if (service.startsWith("org.mpris.MediaPlayer2."))
{
mediaPlayerService = service;
break;
}
}

if (mediaPlayerService.isEmpty())
{
LOG_DEBUG("No active media player found on DBus");
return nullptr;
}

LOG_DEBUG("Found media player service: " << mediaPlayerService);
return new QDBusInterface(mediaPlayerService, "/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player", sessionBus, this);
}

bool MediaController::sendMediaPlayerCommand(const QString &method)
{
QDBusInterface *iface = getMediaPlayerInterface();
if (!iface)
{
LOG_ERROR("No media player interface available for " << method);
return false;
}

// Use QDBusMessage for more control and error handling
QDBusMessage message = QDBusMessage::createMethodCall(
iface->service(),
iface->path(),
iface->interface(),
method);

QDBusPendingCall call = iface->connection().asyncCall(message);
call.waitForFinished();

if (call.isError())
{
LOG_ERROR("Failed to execute " << method << ": " << call.error().message());
delete iface;
return false;
}

delete iface;
return true;
}

void MediaController::play()
{
if (sendMediaPlayerCommand("Play"))
{
LOG_INFO("Resumed playback via DBus");
wasPausedByApp = false;
}
else
{
LOG_ERROR("Failed to resume playback via DBus");
}
}

void MediaController::pause()
{
if (sendMediaPlayerCommand("Pause"))
{
LOG_INFO("Paused playback via DBus");
wasPausedByApp = true;
}
else
{
LOG_ERROR("Failed to pause playback via Playerctl");
LOG_ERROR("Failed to pause playback via DBus");
}
}

MediaController::~MediaController() {
if (playerctlProcess) {
playerctlProcess->terminate();
if (!playerctlProcess->waitForFinished()) {
playerctlProcess->kill();
playerctlProcess->waitForFinished(1000);
}
}
}

QString MediaController::getAudioDeviceName()
Expand Down
12 changes: 8 additions & 4 deletions linux/mediacontroller.h → linux/media/mediacontroller.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#ifndef MEDIACONTROLLER_H
#define MEDIACONTROLLER_H

#include <QDBusInterface>
#include <QObject>

class QProcess;
class EarDetection;
class PlayerStatusWatcher;
class QDBusInterface;

class MediaController : public QObject
{
Expand All @@ -29,7 +30,6 @@ class MediaController : public QObject
explicit MediaController(QObject *parent = nullptr);
~MediaController();

void initializeMprisInterface();
void handleEarDetection(EarDetection*);
void followMediaChanges();
bool isActiveOutputDeviceAirPods();
Expand All @@ -41,22 +41,26 @@ class MediaController : public QObject
void setEarDetectionBehavior(EarDetectionBehavior behavior);
inline EarDetectionBehavior getEarDetectionBehavior() const { return earDetectionBehavior; }

void play();
void pause();
MediaState getCurrentMediaState() const { return m_mediaState; };

Q_SIGNALS:
void mediaStateChanged(MediaState state);

private:
MediaState mediaStateFromPlayerctlOutput(const QString &output);
QString getAudioDeviceName();
bool sendMediaPlayerCommand(const QString &method);
QDBusInterface *getMediaPlayerInterface();

QDBusInterface *mprisInterface = nullptr;
QProcess *playerctlProcess = nullptr;
bool wasPausedByApp = false;
int initialVolume = -1;
QString connectedDeviceMacAddress;
EarDetectionBehavior earDetectionBehavior = PauseWhenOneRemoved;
QString m_deviceOutputName;
PlayerStatusWatcher *playerStatusWatcher = nullptr;
MediaState m_mediaState = Stopped;
};

#endif // MEDIACONTROLLER_H
47 changes: 47 additions & 0 deletions linux/media/playerstatuswatcher.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#include "playerstatuswatcher.h"
#include <QDBusConnection>
#include <QDBusPendingReply>
#include <QVariantMap>
#include <QDBusReply>

PlayerStatusWatcher::PlayerStatusWatcher(const QString &playerService, QObject *parent)
: QObject(parent),
m_playerService(playerService),
m_iface(new QDBusInterface(playerService, "/org/mpris/MediaPlayer2",
"org.mpris.MediaPlayer2.Player", QDBusConnection::sessionBus(), this)),
m_serviceWatcher(new QDBusServiceWatcher(playerService, QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForOwnerChange, this))
{
QDBusConnection::sessionBus().connect(
playerService, "/org/mpris/MediaPlayer2", "org.freedesktop.DBus.Properties",
"PropertiesChanged", this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList))
);
connect(m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged,
this, &PlayerStatusWatcher::onServiceOwnerChanged);
updateStatus();
}

void PlayerStatusWatcher::onPropertiesChanged(const QString &interface,
const QVariantMap &changed,
const QStringList &)
{
if (interface == "org.mpris.MediaPlayer2.Player" && changed.contains("PlaybackStatus")) {
emit playbackStatusChanged(changed.value("PlaybackStatus").toString());
}
}

void PlayerStatusWatcher::updateStatus() {
QVariant reply = m_iface->property("PlaybackStatus");
if (reply.isValid()) {
emit playbackStatusChanged(reply.toString());
}
}

void PlayerStatusWatcher::onServiceOwnerChanged(const QString &name, const QString &, const QString &newOwner)
{
if (name == m_playerService && newOwner.isEmpty()) {
emit playbackStatusChanged(""); // player disappeared
} else if (name == m_playerService && !newOwner.isEmpty()) {
updateStatus(); // player appeared/reappeared
}
}
Loading