2026-10-28 08:48:12 -04:00
|
|
|
|
#include "moonlight_session.h"
|
|
|
|
|
|
|
|
|
|
|
|
#include <QDate>
|
|
|
|
|
|
#include <QDateTime>
|
|
|
|
|
|
#include <QDir>
|
|
|
|
|
|
#include <QFile>
|
|
|
|
|
|
#include <QTextStream>
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-29 05:37:38 -04:00
|
|
|
|
qint64 MoonlightSession::processId() const {
|
|
|
|
|
|
return isRunning() ? process_->processId() : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-28 08:48:12 -04:00
|
|
|
|
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")));
|
|
|
|
|
|
|
2026-10-28 23:58:32 -04:00
|
|
|
|
// QProcess::finished 在 Qt5 里有两个重载((int) 已废弃),必须用 QOverload 消歧
|
|
|
|
|
|
connect(process_,
|
|
|
|
|
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
|
|
|
|
|
|
&MoonlightSession::onProcessFinished);
|
2026-10-28 08:48:12 -04:00
|
|
|
|
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<int>(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
|