vdi-cloud/terminal/kiosk-shell/src/moonlight_session.cpp

142 lines
4.6 KiB
C++
Raw Normal View History

#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;
}
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<int, QProcess::ExitStatus>::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<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