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

285 lines
9.2 KiB
C++
Raw Normal View History

#include "moonlight_session.h"
#include <QDate>
#include <QDir>
#include <QFile>
#include <QProcessEnvironment>
#include <QTextStream>
#include <QTimer>
namespace vdi {
namespace {
constexpr int kReadyStableMs = 750;
constexpr int kTerminateGraceMs = 2000;
bool isTestDecodeLine(const QByteArray& line) {
return line.toLower().contains("test decode successful");
}
bool isRendererLine(const QByteArray& line) {
const QByteArray lower = line.toLower();
return lower.contains("renderer '") && lower.contains(" chosen");
}
bool isTerminalErrorLine(const QByteArray& line) {
const QByteArray lower = line.toLower();
if (lower.contains("会话已终止")) {
return true;
}
const QByteArray marker("connection terminated:");
const int markerPos = lower.indexOf(marker);
if (markerPos < 0) {
return false;
}
bool ok = false;
const int code = lower.mid(markerPos + marker.size()).trimmed().toInt(&ok);
return !ok || code != 0;
}
} // namespace
void MoonlightOutputParser::reset() {
buffer_.clear();
hasTestDecode_ = false;
hasRenderer_ = false;
hasTerminalError_ = false;
}
void MoonlightOutputParser::append(const QByteArray& output) {
buffer_.append(output);
int newline = -1;
while ((newline = buffer_.indexOf('\n')) >= 0) {
const QByteArray line = buffer_.left(newline).trimmed();
buffer_.remove(0, newline + 1);
hasTestDecode_ = hasTestDecode_ || isTestDecodeLine(line);
hasRenderer_ = hasRenderer_ || isRendererLine(line);
hasTerminalError_ = hasTerminalError_ || isTerminalErrorLine(line);
}
}
bool MoonlightOutputParser::isReady() const {
return hasTestDecode_ && hasRenderer_ && !hasTerminalError_;
}
MoonlightSession::MoonlightSession(QObject* parent) : QObject(parent) {
process_ = new QProcess(this);
process_->setProcessChannelMode(QProcess::MergedChannels);
connect(process_, &QProcess::started, this, &MoonlightSession::onProcessStarted);
connect(process_,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
&MoonlightSession::onProcessFinished);
connect(process_, &QProcess::errorOccurred, this, &MoonlightSession::onProcessError);
connect(process_, &QProcess::readyReadStandardOutput, this,
&MoonlightSession::onProcessOutput);
readyTimer_ = new QTimer(this);
readyTimer_->setSingleShot(true);
connect(readyTimer_, &QTimer::timeout, this, [this]() {
if (!readyEventDelivered_ && !terminalEventDelivered_ && isRunning()) {
readyEventDelivered_ = true;
emitEvent(EventType::kStreamReady, QStringLiteral("串流画面已就绪"));
}
});
killTimer_ = new QTimer(this);
killTimer_->setSingleShot(true);
connect(killTimer_, &QTimer::timeout, this, [this]() {
if (isRunning()) {
process_->kill();
}
});
}
MoonlightSession::~MoonlightSession() {
callback_ = nullptr;
readyTimer_->stop();
killTimer_->stop();
if (isRunning()) {
process_->kill();
process_->waitForFinished(1000);
}
}
void MoonlightSession::setOptions(const Options& options) {
options_ = options;
}
bool MoonlightSession::isRunning() const {
return process_->state() != QProcess::NotRunning;
}
qint64 MoonlightSession::processId() const {
return isRunning() ? process_->processId() : 0;
}
bool MoonlightSession::activateStreamWindow() {
if (!isRunning()) {
return false;
}
QProcess finder;
finder.start(QStringLiteral("xdotool"),
{QStringLiteral("search"), QStringLiteral("--onlyvisible"),
QStringLiteral("--pid"), QString::number(process_->processId()),
QStringLiteral("--class"), QStringLiteral("com.moonlight_stream.Moonlight")});
if (!finder.waitForFinished(500)) {
finder.kill();
finder.waitForFinished();
return false;
}
if (finder.exitCode() != 0) {
return false;
}
const QString windowId = QString::fromUtf8(finder.readAllStandardOutput())
.split(QLatin1Char('\n'), Qt::SkipEmptyParts)
.value(0).trimmed();
if (windowId.isEmpty()) {
return false;
}
return QProcess::execute(QStringLiteral("xdotool"),
{QStringLiteral("windowactivate"), QStringLiteral("--sync"),
windowId}) == 0;
}
bool MoonlightSession::start(const ConnectionTicket& ticket, EventCallback callback) {
if (isRunning()) {
return false;
}
callback_ = std::move(callback);
stopRequested_ = false;
terminalEventDelivered_ = false;
readyEventDelivered_ = false;
outputParser_.reset();
readyTimer_->stop();
killTimer_->stop();
process_->setProgram(QStringLiteral("moonlight-qt"));
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert(QStringLiteral("GL_IS_SLOW"), QStringLiteral("0"));
environment.insert(QStringLiteral("SDL_RENDER_DRIVER"), QStringLiteral("opengles2"));
process_->setProcessEnvironment(environment);
const int bitrateKbps = ticket.width >= 2560 ? 30000 : 20000;
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(bitrateKbps)
<< QStringLiteral("--display-mode") << QStringLiteral("fullscreen")
<< QStringLiteral("--video-codec") << options_.videoCodec
<< QStringLiteral("--video-decoder") << options_.decoder
<< QStringLiteral("--absolute-mouse")
<< QStringLiteral("--keep-awake");
args << (options_.vsync ? QStringLiteral("--vsync") : QStringLiteral("--no-vsync"));
args << (options_.framePacing ? QStringLiteral("--frame-pacing")
: QStringLiteral("--no-frame-pacing"));
if (options_.performanceOverlay) {
args << QStringLiteral("--performance-overlay");
}
args << (options_.audioOnHost ? QStringLiteral("--audio-on-host")
: QStringLiteral("--no-audio-on-host"));
process_->setArguments(args);
const QString logDir = QDir::homePath() + QStringLiteral("/vdi-terminal/logs");
QDir().mkpath(logDir);
logPath_ = logDir + QStringLiteral("/moonlight-%1.log")
.arg(QDate::currentDate().toString(QStringLiteral("yyyyMMdd")));
process_->start();
return true;
}
void MoonlightSession::onProcessStarted() {
emitEvent(EventType::kStarted, QStringLiteral("串流进程已启动"));
}
void MoonlightSession::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
readyTimer_->stop();
killTimer_->stop();
if (stopRequested_) {
emitTerminalEvent(EventType::kFinished, QStringLiteral("串流已结束"));
return;
}
const QString message = exitStatus == QProcess::CrashExit
? QStringLiteral("串流异常退出 (code=%1)").arg(exitCode)
: QStringLiteral("串流已结束 (code=%1)").arg(exitCode);
emitTerminalEvent(exitStatus == QProcess::CrashExit ? EventType::kFailed
: EventType::kFinished,
message);
}
void MoonlightSession::onProcessError(QProcess::ProcessError error) {
if (error == QProcess::Crashed) {
return;
}
const QString message = error == QProcess::FailedToStart
? QStringLiteral("无法启动 moonlight-qt,请检查安装")
: QStringLiteral("串流进程错误 (%1)").arg(static_cast<int>(error));
emitTerminalEvent(EventType::kFailed, message);
}
void MoonlightSession::onProcessOutput() {
const QByteArray output = process_->readAllStandardOutput();
if (output.isEmpty()) {
return;
}
QFile log(logPath_);
if (log.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
log.write(output);
}
const bool wasReady = outputParser_.isReady();
outputParser_.append(output);
if (outputParser_.hasTerminalError()) {
readyTimer_->stop();
emitTerminalEvent(EventType::kFailed, QStringLiteral("云桌面连接已中断"));
stop();
return;
}
if (!wasReady && outputParser_.isReady()) {
scheduleReadyEvent();
}
}
void MoonlightSession::stop() {
if (!isRunning()) {
return;
}
stopRequested_ = true;
readyTimer_->stop();
process_->terminate();
killTimer_->start(kTerminateGraceMs);
}
void MoonlightSession::emitEvent(EventType type, const QString& message) {
if (callback_) {
callback_(Event{type, message});
}
}
void MoonlightSession::emitTerminalEvent(EventType type, const QString& message) {
if (terminalEventDelivered_) {
return;
}
terminalEventDelivered_ = true;
emitEvent(type, message);
}
void MoonlightSession::scheduleReadyEvent() {
if (!readyEventDelivered_ && !terminalEventDelivered_ && isRunning()) {
readyTimer_->start(kReadyStableMs);
}
}
} // namespace vdi