diff --git a/.gitmodules b/.gitmodules index a5f05f3..d581ea7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ -[submodule "lib/airplay"] - path = lib/airplay - url = https://github.com/uncor3/airplay +[submodule "lib/uxplay"] + path = lib/uxplay + url = https://github.com/iDescriptor/uxplay [submodule "lib/ipatool-go"] path = lib/ipatool-go url = https://github.com/uncor3/libipatool-go.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 327ce56..cc1c047 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,7 +217,7 @@ if (NOT ENABLE_RECOVERY_DEVICE_SUPPORT) ) endif() -add_subdirectory(lib/airplay) +add_subdirectory(lib/uxplay) add_subdirectory(lib/ipatool-go) add_subdirectory(lib/zupdater) @@ -283,7 +283,7 @@ target_link_libraries(iDescriptor PRIVATE PkgConfig::AVCODEC PkgConfig::AVUTIL PkgConfig::SWSCALE - airplay + uxplay ipatool-go ZUpdater ) @@ -294,7 +294,9 @@ if(ENABLE_RECOVERY_DEVICE_SUPPORT) endif() target_include_directories(iDescriptor PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src + ${CMAKE_CURRENT_SOURCE_DIR}/lib/zupdater/src + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/lib ) if(APPLE) diff --git a/lib/airplay b/lib/airplay deleted file mode 160000 index 9fe5788..0000000 --- a/lib/airplay +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fe5788667f0faab9722bbd9a7887eba7a2a4088 diff --git a/lib/uxplay b/lib/uxplay new file mode 120000 index 0000000..3e3e6e5 --- /dev/null +++ b/lib/uxplay @@ -0,0 +1 @@ +/home/uncore/Desktop/clones/UxPlay \ No newline at end of file diff --git a/src/airplaywindow.cpp b/src/airplaywindow.cpp index 80a4cf4..01a0670 100644 --- a/src/airplaywindow.cpp +++ b/src/airplaywindow.cpp @@ -21,9 +21,14 @@ #include #include #include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -31,10 +36,11 @@ #include #include #include +#include +#include #include #include #include - #ifdef Q_OS_LINUX // V4L2 includes #include @@ -44,12 +50,63 @@ #include #include #endif +#include "settingsmanager.h" -// Include the rpiplay server functions -#include "../lib/airplay/renderers/video_renderer.h" -extern "C" { -int start_server_qt(const char *name, void *callbacks); -int stop_server_qt(); +#include +#include + +AirPlaySettings::AirPlaySettings() + : fps(SettingsManager::sharedInstance()->airplayFps()) +{ +} + +QStringList AirPlaySettings::toArgs() const +{ + QStringList args; + + // FPS + args << "-fps" << QString::number(fps); + + return args; +} + +AirPlaySettingsDialog::AirPlaySettingsDialog(QWidget *parent) + : QDialog(parent), m_settings(AirPlaySettings()) +{ + setupUI(); + setWindowTitle("AirPlay Settings"); + resize(500, 600); +} + +void AirPlaySettingsDialog::setupUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Video Settings Group + QGroupBox *videoGroup = new QGroupBox("Video Settings"); + QFormLayout *videoLayout = new QFormLayout(videoGroup); + + m_fpsSpinBox = new QSpinBox(); + m_fpsSpinBox->setRange(1, 255); + m_fpsSpinBox->setValue(SettingsManager::sharedInstance()->airplayFps()); + m_fpsSpinBox->setToolTip("Set maximum allowed streaming framerate"); + videoLayout->addRow("Max FPS:", m_fpsSpinBox); + + mainLayout->addWidget(videoGroup); + + // Buttons + QDialogButtonBox *buttonBox = + new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + mainLayout->addWidget(buttonBox); +} + +AirPlaySettings AirPlaySettingsDialog::getSettings() const +{ + AirPlaySettings settings; + settings.fps = m_fpsSpinBox->value(); + return settings; } AirPlayWindow::AirPlayWindow(QWidget *parent) @@ -57,16 +114,15 @@ AirPlayWindow::AirPlayWindow(QWidget *parent) m_streamingWidget(nullptr), m_loadingIndicator(nullptr), m_loadingLabel(nullptr), m_tutorialPlayer(nullptr), m_tutorialVideoWidget(nullptr), m_videoLabel(nullptr), - m_tutorialLayout(nullptr), m_v4l2Checkbox(nullptr), - m_serverThread(nullptr), m_serverRunning(false) -#ifdef Q_OS_LINUX - , - m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), m_v4l2_enabled(false) + m_tutorialLayout(nullptr), m_settingsButton(nullptr), +#ifdef __linux__ + m_v4l2Checkbox(nullptr), m_v4l2_fd(-1), m_v4l2_width(0), m_v4l2_height(0), + m_v4l2_enabled(false), #endif + m_serverThread(nullptr), m_serverRunning(false), m_clientConnected(false) { setupUI(); - // Auto-start server after UI setup QTimer::singleShot(500, this, &AirPlayWindow::startAirPlayServer); } @@ -109,6 +165,17 @@ void AirPlayWindow::setupUI() m_tutorialLayout->addLayout(loadingLayout); m_tutorialLayout->addSpacing(1); + // Settings button (shown when no client connected) + m_settingsButton = new QPushButton("Settings"); + m_settingsButton->setVisible(false); + connect(m_settingsButton, &QPushButton::clicked, this, + &AirPlayWindow::showSettingsDialog); + QHBoxLayout *settingsLayout = new QHBoxLayout(); + settingsLayout->addStretch(); + settingsLayout->addWidget(m_settingsButton); + settingsLayout->addStretch(); + m_tutorialLayout->addLayout(settingsLayout); + QTimer::singleShot(100, this, &AirPlayWindow::setupTutorialVideo); m_streamingWidget = new QWidget(); @@ -116,7 +183,7 @@ void AirPlayWindow::setupUI() streamingLayout->setContentsMargins(10, 10, 10, 10); streamingLayout->setSpacing(10); -#ifdef Q_OS_LINUX +#ifdef __linux__ // Add V4L2 checkbox at the top of streaming view setupV4L2Checkbox(); if (m_v4l2Checkbox) { @@ -193,6 +260,22 @@ void AirPlayWindow::showStreamingView() } } +void AirPlayWindow::showSettingsDialog() +{ + AirPlaySettingsDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) { + AirPlaySettings newSettings = dialog.getSettings(); + + // Save settings + SettingsManager::sharedInstance()->setAirplayFps(newSettings.fps); + + QMessageBox::information(this, "Settings Saved", + "AirPlay will be restarted to apply the new " + "settings."); + emit restartRequested(); + } +} + void AirPlayWindow::startAirPlayServer() { if (m_serverRunning) @@ -205,15 +288,23 @@ void AirPlayWindow::startAirPlayServer() &AirPlayWindow::updateVideoFrame); connect(m_serverThread, &AirPlayServerThread::clientConnectionChanged, this, &AirPlayWindow::onClientConnectionChanged); + connect(m_serverThread, &AirPlayServerThread::errorOccurred, this, + [this](const QString &message) { + QMessageBox::critical(this, "AirPlay Server Error", message); + close(); + }); + QStringList args = m_settings.toArgs(); + m_serverThread->setArguments(args); m_serverThread->start(); } void AirPlayWindow::stopAirPlayServer() { if (m_serverThread) { - m_serverThread->stopServer(); - m_serverThread->wait(3000); + // m_serverThread->stopServer(); + // m_serverThread->wait(3000); + m_serverThread->quit(); m_serverThread->deleteLater(); m_serverThread = nullptr; } @@ -223,8 +314,10 @@ void AirPlayWindow::stopAirPlayServer() void AirPlayWindow::updateVideoFrame(QByteArray frameData, int width, int height) { - if (frameData.size() != width * height * 3) + if (frameData.size() != width * height * 3) { + qDebug() << "Invalid frame data size"; return; + } #ifdef __linux__ // V4L2 output if enabled @@ -258,6 +351,11 @@ void AirPlayWindow::onServerStatusChanged(bool running) // Show tutorial video and instructions m_tutorialVideoWidget->setVisible(true); + + // Show settings button when server is running but no client connected + m_settingsButton->setVisible(!m_clientConnected); + + // Show tutorial video and instructions QLabel *instructionLabel = m_tutorialWidget->findChild(); if (instructionLabel && !instructionLabel->text().contains("Follow")) { // Find the instruction label (not title or loading label) @@ -279,6 +377,10 @@ void AirPlayWindow::onServerStatusChanged(bool running) void AirPlayWindow::onClientConnectionChanged(bool connected) { m_clientConnected = connected; + + // Hide settings button when client is connected + m_settingsButton->setVisible(!connected && m_serverRunning); + if (connected) { m_loadingLabel->setText("Device connected - receiving stream..."); @@ -340,57 +442,80 @@ AirPlayServerThread::AirPlayServerThread(QObject *parent) AirPlayServerThread::~AirPlayServerThread() { - stopServer(); + // stopServer(); + uxplay_cleanup(); wait(); } -void AirPlayServerThread::stopServer() +void AirPlayServerThread::setArguments(const QStringList &args) { QMutexLocker locker(&m_mutex); - m_shouldStop = true; - m_waitCondition.wakeAll(); + + m_argData.clear(); + m_argv.clear(); + + m_argData.append("uxplay"); + + // Add all arguments + for (const QString &arg : args) { + m_argData.append(arg.toUtf8()); + } + + // Build argv array with persistent pointers + for (QByteArray &data : m_argData) { + m_argv.append(data.data()); + } } // Global pointer to current server thread for callbacks static AirPlayServerThread *g_currentServerThread = nullptr; -// Static callback wrappers for C interface -extern "C" void qt_video_callback(uint8_t *data, int width, int height) +void frame_callback(const unsigned char *data, int width, int height, + int stride, int format) { - if (g_currentServerThread) { - QByteArray frameData((const char *)data, width * height * 3); - emit g_currentServerThread->videoFrameReady(frameData, width, height); - } + if (!g_currentServerThread) + return; + QByteArray frameData((const char *)data, width * height * 3); + emit g_currentServerThread->videoFrameReady(frameData, width, height); } -extern "C" void qt_connection_callback(bool connected) +void connection_callback(bool connected) { - if (g_currentServerThread) { - emit g_currentServerThread->clientConnectionChanged(connected); - } + qDebug() << "Connection callback: " + << (connected ? "Connected" : "Disconnected"); + if (!g_currentServerThread) + return; + emit g_currentServerThread->clientConnectionChanged(connected); } void AirPlayServerThread::run() { g_currentServerThread = this; emit statusChanged(true); + callbacks_t callbacks; + callbacks.frame_callback = frame_callback; + callbacks.connection_callback = connection_callback; + uxplay_callbacks = &callbacks; - // Create callbacks structure - video_renderer_qt_callbacks_t callbacks; - callbacks.video_callback = qt_video_callback; - callbacks.connection_callback = qt_connection_callback; - - start_server_qt("iDescriptor", &callbacks); - - // Wait efficiently until stopServer() is called - QMutexLocker locker(&m_mutex); - while (!m_shouldStop) { - m_waitCondition.wait(&m_mutex); + qDebug() << "Starting AirPlay server with arguments:" << m_argv.size(); + for (int i = 0; i < m_argv.size(); ++i) { + qDebug() << " argv[" << i << "] =" << m_argv[i]; } - stop_server_qt(); + try { + int res = init_uxplay(m_argv.size(), m_argv.data()); + qDebug() << "AirPlay server exited with code: " << res; + if (res != 0) { + emit errorOccurred("AirPlay server exited unexpectedly."); + } + } catch (const std::exception &e) { + qDebug() << "Exception in AirPlay server thread: " << e.what(); + emit errorOccurred( + QString("AirPlay server encountered an error: %1").arg(e.what())); + } + + uxplay_callbacks = nullptr; g_currentServerThread = nullptr; - emit statusChanged(false); } #ifdef __linux__ @@ -511,6 +636,9 @@ bool AirPlayWindow::createV4L2Loopback() void AirPlayWindow::setupV4L2Checkbox() { + if (!SettingsManager::sharedInstance()->showV4L2()) + return; + try { m_v4l2Checkbox = new QCheckBox("Enable V4L2 Virtual Camera Output"); m_v4l2Checkbox->setToolTip("Enable output to virtual camera device " @@ -524,4 +652,4 @@ void AirPlayWindow::setupV4L2Checkbox() qWarning("Exception occurred while setting up V4L2 checkbox"); } } -#endif \ No newline at end of file +#endif diff --git a/src/airplaywindow.h b/src/airplaywindow.h index 0b94d7a..b1fcc0d 100644 --- a/src/airplaywindow.h +++ b/src/airplaywindow.h @@ -23,10 +23,18 @@ #include "qprocessindicator.h" #include #include +#include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include #include #include #include @@ -40,22 +48,49 @@ class AirPlayServerThread : public QThread public: explicit AirPlayServerThread(QObject *parent = nullptr); - ~AirPlayServerThread() override; + ~AirPlayServerThread(); - void stopServer(); + // void stopServer(); + void setArguments(const QStringList &args); signals: void statusChanged(bool running); void videoFrameReady(QByteArray frameData, int width, int height); void clientConnectionChanged(bool connected); + void errorOccurred(const QString &message); protected: void run() override; private: - bool m_shouldStop; QMutex m_mutex; QWaitCondition m_waitCondition; + bool m_shouldStop; + QVector m_argData; + QVector m_argv; +}; + +class AirPlaySettings +{ +public: + explicit AirPlaySettings(); + int fps; + + QStringList toArgs() const; +}; + +class AirPlaySettingsDialog : public QDialog +{ + Q_OBJECT +public: + explicit AirPlaySettingsDialog(QWidget *parent = nullptr); + AirPlaySettings getSettings() const; + +private: + void setupUI(); + + QSpinBox *m_fpsSpinBox; + AirPlaySettings m_settings; }; class AirPlayWindow : public QMainWindow @@ -66,22 +101,34 @@ public: explicit AirPlayWindow(QWidget *parent = nullptr); ~AirPlayWindow(); -public slots: - void updateVideoFrame(QByteArray frameData, int width, int height); - void onClientConnectionChanged(bool connected); - private slots: + void updateVideoFrame(QByteArray frameData, int width, int height); void onServerStatusChanged(bool running); + void onClientConnectionChanged(bool connected); + void showSettingsDialog(); #ifdef __linux__ void onV4L2CheckboxToggled(bool enabled); #endif + +signals: + void restartRequested(); + private: void setupUI(); - void startAirPlayServer(); - void stopAirPlayServer(); void setupTutorialVideo(); void showTutorialView(); void showStreamingView(); + void startAirPlayServer(); + void stopAirPlayServer(); + +#ifdef __linux__ + void initV4L2(int width, int height, const char *device = "/dev/video0"); + void closeV4L2(); + void writeFrameToV4L2(uint8_t *data, int width, int height); + bool checkV4L2LoopbackExists(); + bool createV4L2Loopback(); + void setupV4L2Checkbox(); +#endif // UI Components QStackedWidget *m_stackedWidget; @@ -94,30 +141,20 @@ private: QVideoWidget *m_tutorialVideoWidget; QLabel *m_videoLabel; QVBoxLayout *m_tutorialLayout; - QCheckBox *m_v4l2Checkbox; - - AirPlayServerThread *m_serverThread; - bool m_serverRunning; - bool m_clientConnected = false; + QPushButton *m_settingsButton; #ifdef __linux__ -public: - // V4L2 members - public for C callback access + QCheckBox *m_v4l2Checkbox; int m_v4l2_fd; int m_v4l2_width; int m_v4l2_height; bool m_v4l2_enabled = false; - - // V4L2 methods - void writeFrameToV4L2(uint8_t *data, int width, int height); - -private: - void initV4L2(int width, int height, const char *device); - void closeV4L2(); - bool checkV4L2LoopbackExists(); - bool createV4L2Loopback(); - void setupV4L2Checkbox(); #endif + + AirPlayServerThread *m_serverThread; + bool m_serverRunning; + bool m_clientConnected; + AirPlaySettings m_settings; }; #endif // AIRPLAYWINDOW_H diff --git a/src/settingsmanager.cpp b/src/settingsmanager.cpp index a8be4f6..8666dd6 100644 --- a/src/settingsmanager.cpp +++ b/src/settingsmanager.cpp @@ -390,4 +390,28 @@ void SettingsManager::setIconSizeBaseMultiplier(double multiplier) { m_settings->setValue("iconSizeBaseMultiplier", multiplier); m_settings->sync(); -} \ No newline at end of file +} + +int SettingsManager::airplayFps() const +{ + return m_settings->value("airplayFps", 60).toInt(); +} + +void SettingsManager::setAirplayFps(int fps) +{ + m_settings->setValue("airplayFps", fps); + m_settings->sync(); +} + +#ifdef __linux__ +bool SettingsManager::showV4L2() const +{ + return m_settings->value("showV4L2", false).toBool(); +} + +void SettingsManager::setShowV4L2(bool show) +{ + m_settings->setValue("showV4L2", show); + m_settings->sync(); +} +#endif \ No newline at end of file diff --git a/src/settingsmanager.h b/src/settingsmanager.h index 0e3b47a..4bc0239 100644 --- a/src/settingsmanager.h +++ b/src/settingsmanager.h @@ -105,6 +105,13 @@ public: double iconSizeBaseMultiplier() const; void setIconSizeBaseMultiplier(double multiplier); + + int airplayFps() const; + void setAirplayFps(int fps); +#ifdef __linux__ + bool showV4L2() const; + void setShowV4L2(bool show); +#endif signals: void favoritePlacesChanged(); void recentLocationsChanged(); diff --git a/src/settingswidget.cpp b/src/settingswidget.cpp index 1c06e78..5fa66cc 100644 --- a/src/settingswidget.cpp +++ b/src/settingswidget.cpp @@ -153,6 +153,30 @@ void SettingsWidget::setupUI() scrollLayout->addWidget(jailbrokenGroup); + // === AirPlay SETTINGS === + auto *airplayGroup = new QGroupBox("AirPlay"); + auto *airplayLayout = new QVBoxLayout(airplayGroup); + + auto *fpsLayout = new QHBoxLayout(); + + auto *fpsLabel = new QLabel("Fps:"); + m_airplayFpsSpinBox = new QSpinBox(); + m_airplayFpsSpinBox->setRange(1, 255); + m_airplayFpsSpinBox->setToolTip( + "Set the fps for AirPlay. Go with 30 fps if have an older device."); + + fpsLayout->addWidget(fpsLabel); + fpsLayout->addWidget(m_airplayFpsSpinBox); + fpsLayout->addStretch(); + airplayLayout->addLayout(fpsLayout); + +#ifdef __linux__ + m_showV4L2CheckBox = new QCheckBox("Show V4L2 Button on AirPlay Widget"); + airplayLayout->addWidget(m_showV4L2CheckBox); +#endif + + scrollLayout->addWidget(airplayGroup); + // === MISCELLANEOUS SETTINGS === auto *miscGroup = new QGroupBox("Miscellaneous"); auto *miscLayout = new QVBoxLayout(miscGroup); @@ -250,6 +274,10 @@ void SettingsWidget::loadSettings() m_applyButton->setEnabled(false); m_iconSizeBaseMultiplier->setValue(sm->iconSizeBaseMultiplier()); + m_airplayFpsSpinBox->setValue(sm->airplayFps()); +#ifdef __linux__ + m_showV4L2CheckBox->setChecked(sm->showV4L2()); +#endif } void SettingsWidget::connectSignals() @@ -380,6 +408,10 @@ void SettingsWidget::saveSettings() sm->setIconSizeBaseMultiplier(m_iconSizeBaseMultiplier->value()); + sm->setAirplayFps(m_airplayFpsSpinBox->value()); +#ifdef __linux__ + sm->setShowV4L2(m_showV4L2CheckBox->isChecked()); +#endif m_applyButton->setEnabled(false); } diff --git a/src/settingswidget.h b/src/settingswidget.h index 82276d9..bb4a1fc 100644 --- a/src/settingswidget.h +++ b/src/settingswidget.h @@ -68,6 +68,13 @@ private: QDoubleSpinBox *m_iconSizeBaseMultiplier; + // Airplay + QSpinBox *m_airplayFpsSpinBox; + +#ifdef __linux__ + QCheckBox *m_showV4L2CheckBox; +#endif + // Buttons QPushButton *m_checkUpdatesButton; QPushButton *m_resetButton; diff --git a/src/toolboxwidget.cpp b/src/toolboxwidget.cpp index d6afc3d..9fbf739 100644 --- a/src/toolboxwidget.cpp +++ b/src/toolboxwidget.cpp @@ -409,6 +409,17 @@ void ToolboxWidget::onToolboxClicked(iDescriptorTool tool) m_airplayWindow = new AirPlayWindow(); connect(m_airplayWindow, &QObject::destroyed, this, [this]() { m_airplayWindow = nullptr; }); + connect(m_airplayWindow, &AirPlayWindow::restartRequested, this, + [this]() { + if (m_airplayWindow) { + m_airplayWindow->close(); + } + m_airplayWindow = new AirPlayWindow(); + m_airplayWindow->setAttribute(Qt::WA_DeleteOnClose); + m_airplayWindow->setWindowFlag(Qt::Window); + m_airplayWindow->resize(400, 300); + m_airplayWindow->show(); + }); m_airplayWindow->setAttribute(Qt::WA_DeleteOnClose); m_airplayWindow->setWindowFlag(Qt::Window); m_airplayWindow->resize(400, 300);