Files
utun2/tools/chatgui/src/call_camera_capture.cpp
T

187 lines
10 KiB
C++

#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 <QCoreApplication>
#include <QLocalServer>
#include <QPointer>
#include <QProcess>
#include <QTimer>
#include <QUuid>
#include <atomic>
#include <cstring>
using namespace CallCameraProtocol;
struct CallCameraCapture::Session : QObject {
QPointer<CallCameraCapture> 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;
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<int, QProcess::ExitStatus>::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); }));
}
void configure(bool active, const QByteArray& id, int w, int h) {
if (exiting) return;
if (enabled == active && device == id && width == w && height == h && !failureReported) return;
enabled = active; device = id; width = w; height = h; ++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 device=%s",
(unsigned long long)generation, active, w, h, 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) { QString name; QByteArray id; in >> name >> id; devices.append({name, id}); }
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 || w < 1 || h < 1 || w > 1280 || h > 720 || !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) {
m->configure(enabled, device, width, height);
}