#include "call_camera_capture.h" #include "call_camera_protocol.h" #include "call_camera_buffer.h" #include "../../../src/call/call_video_playout.h" #include "debug_config.h" #include #include #include #include #include #include #include #include using namespace CallCameraProtocol; struct CallCameraCapture::Session : QObject { QPointer owner; QLocalServer server; QProcess process; QLocalSocket* socket = nullptr; QTimer watchdog; CallCameraBuffer buffer; uchar* pixels = nullptr; Reader reader; quint64 generation = 0; bool enabled = false, retiring = false, failureReported = false, restartRequested = false, exiting = false; QByteArray device; int width = 640, height = 480, fps = 30; QString phase = QStringLiteral("launch"); qint64 progressAt = 0, frameAt = 0, stopAt = 0; explicit Session(CallCameraCapture* target) : owner(target) { server.setSocketOptions(QLocalServer::UserAccessOption); connect(&server, &QLocalServer::newConnection, this, [this] { auto* connection = server.nextPendingConnection(); if (socket) { connection->abort(); connection->deleteLater(); return; } socket = connection; socket->setParent(this); socket->setReadBufferSize(MaxPacket + 4); server.close(); progressAt = call_video_playout_now_us(); connect(socket, &QLocalSocket::readyRead, this, [this] { if (!reader.read(*socket, [this](quint8 type, quint64 revision, QDataStream& in) { return receive(type, revision, in); })) fail(QStringLiteral("Invalid camera worker response")); }); if (retiring) send(packet(Quit, generation)); else { send(packet(Buffer, 0, [this](QDataStream& out) { out << buffer.name(); })); sendConfiguration(); } }); process.setProcessChannelMode(QProcess::MergedChannels); connect(&process, &QProcess::readyReadStandardOutput, this, [this] { const auto log = process.readAllStandardOutput().trimmed(); for (const auto& line : log.split('\n')) { if (line.isEmpty()) continue; if (line.contains("[ERROR]")) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker: %s", line.constData()); else if (line.contains("[WARN]")) DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera worker: %s", line.constData()); else if (line.contains("[DEBUG]")) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "camera worker: %s", line.constData()); else DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera worker: %s", line.constData()); } }); connect(&process, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { fail(QStringLiteral("Cannot start camera worker: %1").arg(process.errorString())); if (retiring) deleteLater(); } }); connect(&process, QOverload::of(&QProcess::finished), this, [this](int code, QProcess::ExitStatus status) { DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera worker exited code=%d status=%d retiring=%d", code, int(status), retiring); if (socket) { socket->disconnect(this); socket->deleteLater(); socket = nullptr; } server.close(); watchdog.stop(); if (retiring) deleteLater(); else if (restartRequested) { restartRequested = false; launch(); } else if (!failureReported) fail(QStringLiteral("Camera worker stopped")); }); watchdog.setInterval(250); connect(&watchdog, &QTimer::timeout, this, [this] { const qint64 now = call_video_playout_now_us(); if (stopAt && now >= stopAt) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera stop timed out phase=%s; killing worker", qPrintable(phase)); stopAt = 0; if (!retiring) fail(QStringLiteral("Camera did not stop")); else process.kill(); } else if (!retiring && !failureReported && ((phase != "idle" && now - progressAt >= 5000000) || (enabled && now - frameAt >= 5000000))) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera stalled generation=%llu phase=%s operation_age_us=%lld frame_age_us=%lld", (unsigned long long)generation, qPrintable(phase), (long long)(now - progressAt), (long long)(now - frameAt)); fail(QStringLiteral("Camera stopped responding")); } }); // При выходе приложения не вызываем деструктор живого QProcess: он синхронно ждёт child. connect(qApp, &QCoreApplication::aboutToQuit, this, [this] { exiting = true; enabled = false; failureReported = true; restartRequested = false; process.kill(); buffer.close(); pixels = nullptr; }); QTimer::singleShot(0, this, [this] { if (!retiring && !generation && !failureReported) launch(); }); } void launch() { if (exiting || process.state() != QProcess::NotRunning) return; server.close(); reader = {}; failureReported = false; phase = QStringLiteral("launch"); progressAt = call_video_playout_now_us(); if (!pixels) { if (!buffer.create()) { fail(QStringLiteral("Cannot allocate camera frame buffer: %1").arg(buffer.errorString())); return; } pixels = buffer.data(); } const QString name = QStringLiteral("vibechat-camera-") + QUuid::createUuid().toString(QUuid::Id128); if (!server.listen(name)) { fail(QStringLiteral("Cannot create camera IPC: %1").arg(server.errorString())); return; } DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera worker starting; hardware isolated from GUI"); process.start(QCoreApplication::applicationFilePath(), {QStringLiteral("--call-camera-worker"), server.fullServerName()}); watchdog.start(); } void send(const QByteArray& data) { if (!socket || socket->state() != QLocalSocket::ConnectedState) return; if (socket->write(data) != data.size()) fail(QStringLiteral("Cannot write camera IPC")); socket->flush(); } void sendConfiguration() { send(packet(Configure, generation, [this](QDataStream& out) { out << enabled << device << qint32(width) << qint32(height) << qint32(fps); })); } void configure(bool active, const QByteArray& id, int w, int h, int rate) { if (exiting) return; if (!validSize(w, h) || rate < 1 || rate > 60) { fail(QStringLiteral("Invalid camera size/frame rate")); return; } if (enabled == active && device == id && width == w && height == h && fps == rate && !failureReported) return; enabled = active; device = id; width = w; height = h; fps = rate; ++generation; progressAt = call_video_playout_now_us(); frameAt = progressAt; stopAt = active ? 0 : progressAt + 1500000; DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera request generation=%llu enabled=%d size=%dx%d output_fps=%d device=%s", (unsigned long long)generation, active, w, h, rate, id.constData()); if (failureReported && active) restartRequested = true; if (process.state() == QProcess::NotRunning) { restartRequested = false; launch(); } if (socket) sendConfiguration(); } void shutdown() { owner = nullptr; retiring = true; enabled = false; ++generation; if (process.state() == QProcess::NotRunning) { deleteLater(); return; } stopAt = call_video_playout_now_us() + 1500000; watchdog.start(); send(packet(Quit, generation)); } void fail(const QString& error) { if (failureReported) return; failureReported = true; enabled = false; restartRequested = false; stopAt = 0; DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker failure phase=%s: %s", qPrintable(phase), qPrintable(error)); process.kill(); if (owner) emit owner->failed(error); } bool receive(quint8 type, quint64 revision, QDataStream& in) { if (type == Devices) { quint32 count = 0; in >> count; if (in.status() != QDataStream::Ok || count > 128) return false; CallCameraDevices devices; for (quint32 i = 0; i < count; ++i) { CallCameraDevice device; quint32 modes = 0; in >> device.name >> device.id >> modes; if (in.status() != QDataStream::Ok || modes > 256) return false; for (quint32 j = 0; j < modes; ++j) { qint32 w = 0, h = 0, rate = 0; in >> w >> h >> rate; if (!validSize(w, h) || rate < 1 || rate > 60) return false; device.modes.append({QSize(w, h), rate}); } devices.append(device); } if (owner) emit owner->devicesChanged(devices); } else if (type == Stage) { QString next; in >> next; phase = next; progressAt = call_video_playout_now_us(); if (revision == generation && next == "idle") stopAt = 0; } else if (type == Error) { QString error; in >> error; if (revision == generation) fail(error); } else if (type == Frame) { qint64 pts = -1, correction = 0; qint32 w = 0, h = 0; in >> pts >> correction >> w >> h; if (in.status() != QDataStream::Ok || pts < 0 || correction < 0 || !validSize(w, h) || !pixels) return false; const qint64 now = call_video_playout_now_us(); const bool current = owner && enabled && revision == generation && !failureReported; if (current && pts <= now && now - pts <= CallCameraMaxFrameAgeUs) { QImage image(w, h, QImage::Format_RGB32); if (image.isNull()) return false; std::atomic_thread_fence(std::memory_order_acquire); std::memcpy(image.bits(), pixels, size_t(w) * h * 4); progressAt = frameAt = now; send(packet(Ack, revision)); emit owner->frameReady(image, pts, correction); return true; } else { if (current) DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera dropped stale frame age_us=%lld", (long long)(now - pts)); } send(packet(Ack, revision)); } else return false; return true; } }; CallCameraCapture::CallCameraCapture(QObject* parent) : QObject(parent), m(new Session(this)) {} CallCameraCapture::~CallCameraCapture() { m->shutdown(); } void CallCameraCapture::configure(bool enabled, const QByteArray& device, int width, int height, int fps) { m->configure(enabled, device, width, height, fps); }