feat: refactor to use UxPlay

This commit is contained in:
uncor3
2026-01-04 22:30:44 +00:00
parent d7ebf06709
commit 2c767c0f8e
11 changed files with 326 additions and 78 deletions
+3 -3
View File
@@ -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
+5 -3
View File
@@ -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)
Submodule lib/airplay deleted from 9fe5788667
Symlink
+1
View File
@@ -0,0 +1 @@
/home/uncore/Desktop/clones/UxPlay
+172 -44
View File
@@ -21,9 +21,14 @@
#include <QApplication>
#include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox>
#include <QDebug>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFileInfo>
#include <QFont>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMediaPlayer>
@@ -31,10 +36,11 @@
#include <QPalette>
#include <QPixmap>
#include <QProcess>
#include <QPushButton>
#include <QSpinBox>
#include <QStackedWidget>
#include <QVBoxLayout>
#include <QVideoWidget>
#ifdef Q_OS_LINUX
// V4L2 includes
#include <cstring>
@@ -44,12 +50,63 @@
#include <sys/ioctl.h>
#include <unistd.h>
#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 <uxplay/renderers/video_renderer.h>
#include <uxplay/uxplay.h>
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<QLabel *>();
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
#endif
+63 -26
View File
@@ -23,10 +23,18 @@
#include "qprocessindicator.h"
#include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QMainWindow>
#include <QMediaPlayer>
#include <QMutex>
#include <QPushButton>
#include <QSpinBox>
#include <QStackedWidget>
#include <QThread>
#include <QTimer>
@@ -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<QByteArray> m_argData;
QVector<char *> 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
+25 -1
View File
@@ -390,4 +390,28 @@ void SettingsManager::setIconSizeBaseMultiplier(double multiplier)
{
m_settings->setValue("iconSizeBaseMultiplier", multiplier);
m_settings->sync();
}
}
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
+7
View File
@@ -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();
+32
View File
@@ -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);
}
+7
View File
@@ -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;
+11
View File
@@ -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);