#include "moonlight_session.h" #include #include #include #include #include namespace vdi { MoonlightSession::MoonlightSession(QObject* parent) : QObject(parent) {} MoonlightSession::~MoonlightSession() { stop(); } void MoonlightSession::setOptions(const Options& options) { options_ = options; } bool MoonlightSession::isRunning() const { return process_ && process_->state() != QProcess::NotRunning; } bool MoonlightSession::start(const ConnectionTicket& ticket, StateCallback callback) { if (isRunning()) { return false; } callback_ = std::move(callback); // moonlight-qt 需要 X11 会话环境;由 kiosk-shell 继承当前 DISPLAY/XAUTHORITY process_ = new QProcess(this); process_->setProgram(QStringLiteral("moonlight-qt")); process_->setProcessChannelMode(QProcess::MergedChannels); QStringList args; args << QStringLiteral("stream") << ticket.endpoint << ticket.app << QStringLiteral("--resolution") << QStringLiteral("%1x%2").arg(ticket.width).arg(ticket.height) << QStringLiteral("--fps") << QString::number(ticket.fps) << QStringLiteral("--bitrate") << QString::number(20000) << QStringLiteral("--display-mode") << QStringLiteral("fullscreen") << QStringLiteral("--video-codec") << options_.videoCodec << QStringLiteral("--video-decoder") << options_.decoder << QStringLiteral("--absolute-mouse") << QStringLiteral("--keep-awake"); if (options_.vsync) { args << QStringLiteral("--vsync"); } else { args << QStringLiteral("--no-vsync"); } if (options_.framePacing) { args << QStringLiteral("--frame-pacing"); } else { args << QStringLiteral("--no-frame-pacing"); } if (options_.performanceOverlay) { args << QStringLiteral("--performance-overlay"); } if (options_.audioOnHost) { args << QStringLiteral("--audio-on-host"); } else { args << QStringLiteral("--no-audio-on-host"); } process_->setArguments(args); // 日志追加到 moonlight-yyyyMMdd.log,便于现场排查 const QString logDir = QDir::homePath() + QStringLiteral("/vdi-terminal/logs"); QDir().mkpath(logDir); const QString logPath = logDir + QStringLiteral("/moonlight-%1.log") .arg(QDate::currentDate().toString(QStringLiteral("yyyyMMdd"))); // QProcess::finished 在 Qt5 里有两个重载((int) 已废弃),必须用 QOverload 消歧 connect(process_, QOverload::of(&QProcess::finished), this, &MoonlightSession::onProcessFinished); connect(process_, &QProcess::errorOccurred, this, &MoonlightSession::onProcessError); connect(process_, &QProcess::readyReadStandardOutput, this, [this, logPath]() { QFile log(logPath); if (log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { QTextStream(&log) << QString::fromUtf8(process_->readAllStandardOutput()); } }); if (callback_) { callback_(true, QStringLiteral("串流进程已启动")); } process_->start(); return true; } void MoonlightSession::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { QString message; if (exitStatus == QProcess::CrashExit) { message = QStringLiteral("串流异常退出 (code=%1)").arg(exitCode); } else { message = QStringLiteral("串流已结束 (code=%1)").arg(exitCode); } process_->deleteLater(); process_ = nullptr; if (callback_) { callback_(false, message); } } void MoonlightSession::onProcessError(QProcess::ProcessError error) { QString message; switch (error) { case QProcess::FailedToStart: message = QStringLiteral("无法启动 moonlight-qt,请检查安装"); break; case QProcess::Crashed: return; // 由 finished(CrashExit) 统一上报 default: message = QStringLiteral("串流进程错误 (%1)").arg(static_cast(error)); break; } if (callback_) { callback_(false, message); } } void MoonlightSession::stop() { if (!process_) { return; } QProcess* proc = process_; process_ = nullptr; // 先断开自身引用,避免 finished 回调再触发状态迁移 disconnect(proc, nullptr, this, nullptr); proc->terminate(); if (!proc->waitForFinished(2000)) { proc->kill(); proc->waitForFinished(1000); } proc->deleteLater(); } } // namespace vdi