#include "moonlight_session.h" #include #include #include #include #include #include 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::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(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