13 changed files with 915 additions and 115 deletions
@ -0,0 +1,79 @@ |
|||||||
|
#pragma once |
||||||
|
#include "call_camera_protocol.h" |
||||||
|
#include "debug_config.h" |
||||||
|
#include <QUuid> |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
#include <qt_windows.h> |
||||||
|
#else |
||||||
|
#include <QTemporaryFile> |
||||||
|
#include <memory> |
||||||
|
#endif |
||||||
|
|
||||||
|
// Владение пикселями задаётся FRAME/ACK. Закрытие mapping никогда не ждёт другой процесс.
|
||||||
|
class CallCameraBuffer { |
||||||
|
QString m_name, m_error; |
||||||
|
uchar* m_data = nullptr; |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
HANDLE m_mapping = nullptr; |
||||||
|
#else |
||||||
|
std::unique_ptr<QFile> m_file; |
||||||
|
#endif |
||||||
|
bool fail(const QString& error) { m_error = error; close(); return false; } |
||||||
|
bool map() { |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
m_data = static_cast<uchar*>(MapViewOfFile(m_mapping, FILE_MAP_WRITE, 0, 0, CallCameraProtocol::FrameBytes)); |
||||||
|
if (!m_data) return fail(QStringLiteral("MapViewOfFile error=%1").arg(GetLastError())); |
||||||
|
#else |
||||||
|
m_data = m_file->map(0, CallCameraProtocol::FrameBytes); |
||||||
|
if (!m_data) return fail(m_file->errorString()); |
||||||
|
#endif |
||||||
|
return true; |
||||||
|
} |
||||||
|
public: |
||||||
|
CallCameraBuffer() = default; |
||||||
|
CallCameraBuffer(const CallCameraBuffer&) = delete; |
||||||
|
CallCameraBuffer& operator=(const CallCameraBuffer&) = delete; |
||||||
|
~CallCameraBuffer() { close(); } |
||||||
|
bool create() { |
||||||
|
close(); |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
m_name = QStringLiteral("Local\\vibechat-camera-") + QUuid::createUuid().toString(QUuid::Id128); |
||||||
|
m_mapping = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, CallCameraProtocol::FrameBytes, |
||||||
|
reinterpret_cast<const wchar_t*>(m_name.utf16())); |
||||||
|
const DWORD error = GetLastError(); |
||||||
|
if (!m_mapping || error == ERROR_ALREADY_EXISTS) return fail(QStringLiteral("CreateFileMapping error=%1").arg(error)); |
||||||
|
#else |
||||||
|
auto file = std::make_unique<QTemporaryFile>(); |
||||||
|
if (!file->open() || !file->resize(CallCameraProtocol::FrameBytes)) return fail(file->errorString()); |
||||||
|
m_name = file->fileName(); m_file = std::move(file); |
||||||
|
#endif |
||||||
|
return map(); |
||||||
|
} |
||||||
|
bool attach(const QString& name) { |
||||||
|
close(); m_name = name; |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
m_mapping = OpenFileMappingW(FILE_MAP_WRITE, FALSE, reinterpret_cast<const wchar_t*>(name.utf16())); |
||||||
|
if (!m_mapping) return fail(QStringLiteral("OpenFileMapping error=%1").arg(GetLastError())); |
||||||
|
#else |
||||||
|
m_file = std::make_unique<QFile>(name); |
||||||
|
if (!m_file->open(QIODevice::ReadWrite | QIODevice::ExistingOnly)) return fail(m_file->errorString()); |
||||||
|
if (m_file->size() != CallCameraProtocol::FrameBytes) return fail(QStringLiteral("Invalid camera buffer size")); |
||||||
|
#endif |
||||||
|
return map(); |
||||||
|
} |
||||||
|
void close() { |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
if (m_data && !UnmapViewOfFile(m_data)) |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera buffer unmap error=%lu", (unsigned long)GetLastError()); |
||||||
|
if (m_mapping && !CloseHandle(m_mapping)) |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera buffer close error=%lu", (unsigned long)GetLastError()); |
||||||
|
m_mapping = nullptr; |
||||||
|
#else |
||||||
|
m_file.reset(); |
||||||
|
#endif |
||||||
|
m_data = nullptr; |
||||||
|
} |
||||||
|
uchar* data() const { return m_data; } |
||||||
|
QString name() const { return m_name; } |
||||||
|
QString errorString() const { return m_error; } |
||||||
|
}; |
||||||
@ -0,0 +1,186 @@ |
|||||||
|
#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); |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
#pragma once |
||||||
|
#include <QObject> |
||||||
|
#include <QImage> |
||||||
|
#include <QList> |
||||||
|
|
||||||
|
using CallCameraDevices = QList<QPair<QString, QByteArray>>; |
||||||
|
constexpr qint64 CallCameraMaxFrameAgeUs = 250000; |
||||||
|
|
||||||
|
// GUI получает только собственную CPU-память. Процесс захвата завершается асинхронно.
|
||||||
|
class CallCameraCapture : public QObject { |
||||||
|
Q_OBJECT |
||||||
|
public: |
||||||
|
explicit CallCameraCapture(QObject* parent = nullptr); |
||||||
|
~CallCameraCapture() override; |
||||||
|
void configure(bool enabled, const QByteArray& device = {}, int width = 640, int height = 480); |
||||||
|
signals: |
||||||
|
void devicesChanged(const CallCameraDevices& devices); |
||||||
|
void frameReady(const QImage& image, qint64 pts, qint64 correction); |
||||||
|
void failed(const QString& error); |
||||||
|
private: |
||||||
|
struct Session; |
||||||
|
Session* m; |
||||||
|
}; |
||||||
|
|
||||||
|
// Вызывается до обычной инициализации приложения, конфига, БД и single-instance lock.
|
||||||
|
int call_camera_worker_main(int argc, char** argv); |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
#pragma once |
||||||
|
#include <QDataStream> |
||||||
|
#include <QLocalSocket> |
||||||
|
#include <QByteArray> |
||||||
|
#include <functional> |
||||||
|
|
||||||
|
namespace CallCameraProtocol { |
||||||
|
enum Type : quint8 { Configure = 1, Quit, Ack, Devices, Frame, Stage, Error, Buffer }; |
||||||
|
constexpr quint32 FrameBytes = 1280 * 720 * 4; |
||||||
|
constexpr quint32 MaxPacket = 65536; |
||||||
|
|
||||||
|
// Только команды и метаданные. Пиксели — в mapping с владением Frame -> Ack, без shared mutex.
|
||||||
|
inline QByteArray packet(Type type, quint64 generation, const std::function<void(QDataStream&)>& write = {}) { |
||||||
|
QByteArray result; |
||||||
|
QDataStream out(&result, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_5_15); |
||||||
|
out << quint32(0) << quint8(type) << generation; |
||||||
|
if (write) write(out); |
||||||
|
out.device()->seek(0); out << quint32(result.size() - 4); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
class Reader { |
||||||
|
QByteArray pending; |
||||||
|
quint32 length = 0; |
||||||
|
public: |
||||||
|
bool read(QLocalSocket& socket, const std::function<bool(quint8, quint64, QDataStream&)>& consume) { |
||||||
|
while (socket.bytesAvailable()) { |
||||||
|
const qsizetype wanted = length ? length : 4; |
||||||
|
pending += socket.read(wanted - pending.size()); |
||||||
|
if (pending.size() != wanted) return true; |
||||||
|
QDataStream in(pending); in.setVersion(QDataStream::Qt_5_15); |
||||||
|
if (!length) { |
||||||
|
in >> length; pending.clear(); |
||||||
|
if (length < 9 || length > MaxPacket) return false; |
||||||
|
} else { |
||||||
|
quint8 type; quint64 generation; in >> type >> generation; |
||||||
|
bool valid = consume(type, generation, in) && in.status() == QDataStream::Ok && in.atEnd(); |
||||||
|
pending.clear(); length = 0; |
||||||
|
if (!valid) return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
#pragma once |
||||||
|
#include "call_camera_capture.h" |
||||||
|
#ifdef HAVE_STREAM_CAPTURE |
||||||
|
#include <QVideoFrame> |
||||||
|
#include <functional> |
||||||
|
#include <memory> |
||||||
|
|
||||||
|
// Backend принадлежит только процессу захвата; тесты подставляют неисправное устройство.
|
||||||
|
class CallCameraSource : public QObject { |
||||||
|
Q_OBJECT |
||||||
|
public: |
||||||
|
virtual CallCameraDevices devices() = 0; |
||||||
|
virtual bool start(const QByteArray& device, int width, int height) = 0; |
||||||
|
virtual void stop() = 0; |
||||||
|
signals: |
||||||
|
void frame(const QVideoFrame& frame); |
||||||
|
void error(const QString& message); |
||||||
|
}; |
||||||
|
int call_camera_worker_run(const QString& endpoint, std::unique_ptr<CallCameraSource> source); |
||||||
|
#endif |
||||||
@ -0,0 +1,216 @@ |
|||||||
|
#include "call_camera_source.h" |
||||||
|
#include "call_camera_protocol.h" |
||||||
|
#include "call_camera_buffer.h" |
||||||
|
#include "call_video_capture_clock.h" |
||||||
|
#include "call_camera_rate.h" |
||||||
|
#include "../../../src/call/call_video_playout.h" |
||||||
|
#include "debug_config.h" |
||||||
|
#include <QGuiApplication> |
||||||
|
#include <QTimer> |
||||||
|
#include <mutex> |
||||||
|
#include <climits> |
||||||
|
#include <algorithm> |
||||||
|
#include <atomic> |
||||||
|
#include <cstring> |
||||||
|
#ifdef HAVE_STREAM_CAPTURE |
||||||
|
#include <QCamera> |
||||||
|
#include <QMediaDevices> |
||||||
|
#include <QMediaCaptureSession> |
||||||
|
#include <QVideoSink> |
||||||
|
|
||||||
|
using namespace CallCameraProtocol; |
||||||
|
|
||||||
|
class QtCallCameraSource : public CallCameraSource { |
||||||
|
std::unique_ptr<QCamera> camera; |
||||||
|
std::unique_ptr<QMediaCaptureSession> session; |
||||||
|
std::unique_ptr<QVideoSink> sink; |
||||||
|
CallCameraRate rate; |
||||||
|
public: |
||||||
|
~QtCallCameraSource() override { stop(); } |
||||||
|
CallCameraDevices devices() override { |
||||||
|
CallCameraDevices result; |
||||||
|
for (const auto& device : QMediaDevices::videoInputs()) result.append({device.description(), device.id()}); |
||||||
|
return result; |
||||||
|
} |
||||||
|
bool start(const QByteArray& device, int width, int height) override { |
||||||
|
QCameraDevice selected; |
||||||
|
for (const auto& item : QMediaDevices::videoInputs()) if (item.id() == device) selected = item; |
||||||
|
if (selected.isNull()) { emit error(QStringLiteral("Camera is unavailable")); return false; } |
||||||
|
session = std::make_unique<QMediaCaptureSession>(); sink = std::make_unique<QVideoSink>(); |
||||||
|
camera = std::make_unique<QCamera>(selected); |
||||||
|
session->setCamera(camera.get()); session->setVideoSink(sink.get()); |
||||||
|
QCameraFormat best; int distance = INT_MAX; |
||||||
|
for (const auto& format : selected.videoFormats()) { |
||||||
|
if (format.maxFrameRate() < 29.9) continue; |
||||||
|
int score = std::abs(format.resolution().width() - width) + std::abs(format.resolution().height() - height); |
||||||
|
if (score < distance) { best = format; distance = score; } |
||||||
|
} |
||||||
|
if (!best.isNull()) camera->setCameraFormat(best); |
||||||
|
const auto format = camera->cameraFormat(); |
||||||
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera selected device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", |
||||||
|
device.constData(), format.resolution().width(), format.resolution().height(), format.pixelFormat(), |
||||||
|
format.minFrameRate(), format.maxFrameRate()); |
||||||
|
connect(camera.get(), &QCamera::activeChanged, this, [this, device](bool active) { |
||||||
|
if (active) rate.activate(device); else rate.restore(); |
||||||
|
}); |
||||||
|
connect(camera.get(), &QCamera::errorOccurred, this, [this](QCamera::Error, const QString& message) { emit error(message); }); |
||||||
|
connect(sink.get(), &QVideoSink::videoFrameChanged, this, &CallCameraSource::frame, Qt::DirectConnection); |
||||||
|
camera->start(); return true; |
||||||
|
} |
||||||
|
void stop() override { |
||||||
|
if (sink) disconnect(sink.get(), nullptr, this, nullptr); |
||||||
|
if (camera) { disconnect(camera.get(), nullptr, this, nullptr); camera->stop(); } |
||||||
|
rate.restore(); session.reset(); camera.reset(); sink.reset(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
class CameraWorker : public QObject { |
||||||
|
struct Pending { |
||||||
|
std::mutex lock; |
||||||
|
QVideoFrame raw; |
||||||
|
qint64 arrival = 0; |
||||||
|
bool posted = false; |
||||||
|
}; |
||||||
|
QLocalSocket socket; |
||||||
|
CallCameraBuffer buffer; |
||||||
|
uchar* pixels = nullptr; |
||||||
|
Reader reader; |
||||||
|
std::unique_ptr<CallCameraSource> source; |
||||||
|
std::shared_ptr<Pending> pending; |
||||||
|
QMetaObject::Connection captureConnection, errorConnection; |
||||||
|
CallVideoCaptureClock clock; |
||||||
|
quint64 generation = 0, inFlight = 0; |
||||||
|
bool sending = false; |
||||||
|
int width = 640, height = 480; |
||||||
|
qint64 lastPts = -1, reportAt = 0, convertMax = 0; |
||||||
|
unsigned frames = 0; |
||||||
|
|
||||||
|
void send(const QByteArray& data) { |
||||||
|
if (socket.write(data) != data.size()) { |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker IPC write failed: %s", qPrintable(socket.errorString())); |
||||||
|
QCoreApplication::exit(2); |
||||||
|
} |
||||||
|
socket.flush(); // Стадия должна уйти до потенциально блокирующего вызова драйвера.
|
||||||
|
} |
||||||
|
void stage(const QString& name) { send(packet(Stage, generation, [&](QDataStream& out) { out << name; })); } |
||||||
|
void stop() { |
||||||
|
stage(QStringLiteral("stop")); |
||||||
|
disconnect(captureConnection); disconnect(errorConnection); source->stop(); pending.reset(); |
||||||
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "camera stopped generation=%llu", (unsigned long long)generation); |
||||||
|
stage(QStringLiteral("idle")); |
||||||
|
} |
||||||
|
void convert(std::shared_ptr<Pending> slot, quint64 revision) { |
||||||
|
if (slot != pending || revision != generation || sending) return; |
||||||
|
QVideoFrame raw; qint64 arrival; |
||||||
|
{ std::lock_guard<std::mutex> guard(slot->lock); |
||||||
|
raw.swap(slot->raw); arrival = slot->arrival; slot->posted = false; } |
||||||
|
if (!raw.isValid()) return; |
||||||
|
const qint64 started = call_video_playout_now_us(), sourcePts = raw.startTime(); |
||||||
|
if (clock.source >= 0 && sourcePts < clock.source) |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera clock reset source=%lld previous=%lld", (long long)sourcePts, (long long)clock.source); |
||||||
|
const qint64 pts = clock.map(sourcePts, arrival); |
||||||
|
if (pts <= lastPts || started - pts > CallCameraMaxFrameAgeUs) { |
||||||
|
DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera skips old frame age_us=%lld pts=%lld last=%lld", |
||||||
|
(long long)(started - pts), (long long)pts, (long long)lastPts); return; |
||||||
|
} |
||||||
|
stage(QStringLiteral("convert")); |
||||||
|
QImage image = raw.toImage(); |
||||||
|
if (image.isNull()) { |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera frame conversion failed"); |
||||||
|
send(packet(Error, generation, [](QDataStream& out) { out << QStringLiteral("Camera frame conversion failed"); })); return; |
||||||
|
} |
||||||
|
if (image.width() > width || image.height() > height) image = image.scaled(width, height, Qt::KeepAspectRatio, Qt::FastTransformation); |
||||||
|
image = image.convertToFormat(QImage::Format_RGB32); |
||||||
|
const qint64 converted = call_video_playout_now_us(); |
||||||
|
if (converted - pts <= CallCameraMaxFrameAgeUs) { |
||||||
|
std::memcpy(pixels, image.constBits(), size_t(image.width()) * image.height() * 4); |
||||||
|
std::atomic_thread_fence(std::memory_order_release); |
||||||
|
auto bytes = packet(Frame, generation, [&](QDataStream& out) { |
||||||
|
out << pts << qint64(clock.correction) << qint32(image.width()) << qint32(image.height()); |
||||||
|
}); |
||||||
|
sending = true; inFlight = generation; lastPts = pts; send(bytes); ++frames; |
||||||
|
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "camera conversion exceeded frame age budget age_us=%lld", (long long)(converted - pts)); |
||||||
|
stage(QStringLiteral("release")); image = {}; raw = {}; stage(QStringLiteral("running")); |
||||||
|
convertMax = std::max(convertMax, call_video_playout_now_us() - started); |
||||||
|
if (started - reportAt >= 1000000) { |
||||||
|
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "camera flow frames=%u convert_max_us=%lld age_us=%lld clock_correction_us=%lld", |
||||||
|
frames, (long long)convertMax, (long long)(converted - pts), (long long)clock.correction); |
||||||
|
reportAt = started; convertMax = 0; frames = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
bool receive(quint8 type, quint64 revision, QDataStream& in) { |
||||||
|
if (type == Buffer) { |
||||||
|
QString name; in >> name; |
||||||
|
if (pixels) return false; |
||||||
|
if (!buffer.attach(name)) { |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera frame mapping failed: %s", qPrintable(buffer.errorString())); return false; |
||||||
|
} |
||||||
|
pixels = buffer.data(); |
||||||
|
} else if (type == Quit) { stop(); QCoreApplication::quit(); } |
||||||
|
else if (type == Ack) { |
||||||
|
if (sending && revision == inFlight) { sending = false; if (pending) convert(pending, generation); } |
||||||
|
} else if (type == Configure) { |
||||||
|
bool enabled = false; QByteArray device; qint32 w = 0, h = 0; in >> enabled >> device >> w >> h; |
||||||
|
if (in.status() != QDataStream::Ok || w < 1 || h < 1 || w > 1280 || h > 720 || !pixels) return false; |
||||||
|
stop(); generation = revision; width = w; height = h; clock = {}; lastPts = -1; |
||||||
|
if (!enabled) { stage(QStringLiteral("idle")); return true; } |
||||||
|
stage(QStringLiteral("start")); |
||||||
|
pending = std::make_shared<Pending>(); |
||||||
|
errorConnection = connect(source.get(), &CallCameraSource::error, this, [this, revision](const QString& error) { |
||||||
|
if (revision != generation) return; |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera backend: %s", qPrintable(error)); |
||||||
|
send(packet(Error, revision, [&](QDataStream& out) { out << error; })); |
||||||
|
}, Qt::QueuedConnection); |
||||||
|
captureConnection = connect(source.get(), &CallCameraSource::frame, this, |
||||||
|
[this, slot = pending, revision](const QVideoFrame& frame) { |
||||||
|
if (!frame.isValid()) return; |
||||||
|
const qint64 arrival = call_video_playout_now_us(); |
||||||
|
QVideoFrame previous; bool post; |
||||||
|
{ std::lock_guard<std::mutex> guard(slot->lock); |
||||||
|
previous.swap(slot->raw); slot->raw = frame; slot->arrival = arrival; |
||||||
|
post = !slot->posted; slot->posted = true; } |
||||||
|
if (post) QMetaObject::invokeMethod(this, [this, slot, revision] { convert(slot, revision); }, Qt::QueuedConnection); |
||||||
|
}, Qt::DirectConnection); |
||||||
|
if (source->start(device, width, height)) stage(QStringLiteral("running")); |
||||||
|
} else return false; |
||||||
|
return true; |
||||||
|
} |
||||||
|
public: |
||||||
|
explicit CameraWorker(std::unique_ptr<CallCameraSource> backend) : source(std::move(backend)) { |
||||||
|
connect(&socket, &QLocalSocket::connected, this, [this] { |
||||||
|
stage(QStringLiteral("enumerate")); |
||||||
|
const auto devices = source->devices(); |
||||||
|
send(packet(Devices, 0, [&](QDataStream& out) { |
||||||
|
out << quint32(devices.size()); for (const auto& device : devices) out << device.first << device.second; |
||||||
|
})); |
||||||
|
stage(QStringLiteral("idle")); |
||||||
|
}); |
||||||
|
connect(&socket, &QLocalSocket::readyRead, this, [this] { |
||||||
|
if (!reader.read(socket, [this](quint8 type, quint64 revision, QDataStream& in) { return receive(type, revision, in); })) { |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker invalid command"); QCoreApplication::exit(2); |
||||||
|
} |
||||||
|
}); |
||||||
|
connect(&socket, &QLocalSocket::disconnected, qApp, &QCoreApplication::quit); |
||||||
|
connect(&socket, &QLocalSocket::errorOccurred, this, [this](QLocalSocket::LocalSocketError) { |
||||||
|
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker IPC: %s", qPrintable(socket.errorString())); QCoreApplication::exit(2); |
||||||
|
}); |
||||||
|
} |
||||||
|
~CameraWorker() override { disconnect(captureConnection); source->stop(); } |
||||||
|
void connectTo(const QString& endpoint) { socket.connectToServer(endpoint); } |
||||||
|
}; |
||||||
|
|
||||||
|
int call_camera_worker_run(const QString& endpoint, std::unique_ptr<CallCameraSource> source) { |
||||||
|
CameraWorker worker(std::move(source)); worker.connectTo(endpoint); return QCoreApplication::exec(); |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
int call_camera_worker_main(int argc, char** argv) { |
||||||
|
#ifdef HAVE_STREAM_CAPTURE |
||||||
|
QGuiApplication app(argc, argv); debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||||
|
debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_DEBUG); |
||||||
|
if (argc != 3) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "camera worker endpoint missing"); return 2; } |
||||||
|
return call_camera_worker_run(QString::fromLocal8Bit(argv[2]), std::make_unique<QtCallCameraSource>()); |
||||||
|
#else |
||||||
|
Q_UNUSED(argc); Q_UNUSED(argv); return 2; |
||||||
|
#endif |
||||||
|
} |
||||||
@ -0,0 +1,217 @@ |
|||||||
|
#include "../src/call_camera_source.h" |
||||||
|
#include "../src/call_camera_buffer.h" |
||||||
|
#include "../../../src/call/call_video_playout.h" |
||||||
|
#include "debug_config.h" |
||||||
|
#include <QtTest> |
||||||
|
#include <QGuiApplication> |
||||||
|
#include <QFile> |
||||||
|
#include <QTemporaryDir> |
||||||
|
#include <QTimer> |
||||||
|
#include <QProcess> |
||||||
|
#ifdef TEST_CALL_VIDEO_ENGINE |
||||||
|
#include "../src/call_video_engine.h" |
||||||
|
#include "../../../src/call/call_video_port.h" |
||||||
|
#include "../../../src/call/call_audio.h" |
||||||
|
extern "C" int __wrap_call_audio_get_clock(uint64_t, int64_t, call_clock_snapshot* out) { *out = {}; return 0; } |
||||||
|
extern "C" int __wrap_call_video_port_open(uint64_t, const call_video_caps*) { return 0; } |
||||||
|
extern "C" void __wrap_call_video_port_close(uint64_t) {} |
||||||
|
extern "C" int __wrap_call_video_port_status(uint64_t, struct call_video_port_status* out) { *out = {}; return 0; } |
||||||
|
extern "C" int __wrap_call_video_port_pull(uint64_t, call_video_frame*) { return 0; } |
||||||
|
extern "C" void __wrap_call_video_port_request_key(uint64_t) {} |
||||||
|
extern "C" int __wrap_call_video_port_decoded(uint64_t, uint64_t, int) { return 0; } |
||||||
|
#endif |
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
||||||
|
#include <QAbstractVideoBuffer> |
||||||
|
#endif |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
#include <qt_windows.h> |
||||||
|
#else |
||||||
|
#include <signal.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
static void record(const QByteArray& event) { |
||||||
|
QFile file(qEnvironmentVariable("UTUN_CAMERA_TEST_LOG")); |
||||||
|
if (file.open(QIODevice::WriteOnly | QIODevice::Append)) |
||||||
|
file.write(QByteArray::number(QCoreApplication::applicationPid()) + ' ' + event + '\n'); |
||||||
|
} |
||||||
|
static bool alive(qint64 pid) { |
||||||
|
#ifdef Q_OS_WIN |
||||||
|
HANDLE handle = OpenProcess(SYNCHRONIZE, FALSE, DWORD(pid)); |
||||||
|
if (!handle) return false; |
||||||
|
bool running = WaitForSingleObject(handle, 0) == WAIT_TIMEOUT; CloseHandle(handle); return running; |
||||||
|
#else |
||||||
|
return kill(pid, 0) == 0; |
||||||
|
#endif |
||||||
|
} |
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
||||||
|
class StuckBuffer : public QAbstractVideoBuffer { |
||||||
|
public: |
||||||
|
MapData map(QVideoFrame::MapMode) override { record("convert-blocked"); QThread::sleep(60); return {}; } |
||||||
|
QVideoFrameFormat format() const override { return {QSize(16, 16), QVideoFrameFormat::Format_BGRX8888}; } |
||||||
|
}; |
||||||
|
#endif |
||||||
|
class TestSource : public CallCameraSource { |
||||||
|
QTimer timer; |
||||||
|
QByteArray device; |
||||||
|
int width = 16, height = 16; |
||||||
|
public: |
||||||
|
TestSource() { |
||||||
|
record("created"); timer.setInterval(33); timer.setTimerType(Qt::PreciseTimer); |
||||||
|
connect(&timer, &QTimer::timeout, this, [this] { |
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
||||||
|
if (device == "convert-stall") { |
||||||
|
QVideoFrame raw(std::make_unique<StuckBuffer>()); raw.setStartTime(call_video_playout_now_us()); emit frame(raw); return; |
||||||
|
} |
||||||
|
#endif |
||||||
|
QVideoFrame raw(QVideoFrameFormat(QSize(width, height), QVideoFrameFormat::Format_BGRX8888)); |
||||||
|
if (!raw.map(QVideoFrame::WriteOnly)) qFatal("test frame map failed"); |
||||||
|
QImage image(raw.bits(0), width, height, raw.bytesPerLine(0), QImage::Format_RGB32); |
||||||
|
image.fill(device == "blue" ? Qt::blue : Qt::red); raw.unmap(); |
||||||
|
raw.setStartTime(call_video_playout_now_us()); emit frame(raw); |
||||||
|
}); |
||||||
|
} |
||||||
|
CallCameraDevices devices() override { return {{QStringLiteral("Test camera"), "red"}}; } |
||||||
|
bool start(const QByteArray& id, int w, int h) override { |
||||||
|
device = id; width = w; height = h; record("start:" + id); |
||||||
|
if (id == "start-stall") QThread::sleep(60); |
||||||
|
timer.start(); return true; |
||||||
|
} |
||||||
|
void stop() override { |
||||||
|
timer.stop(); |
||||||
|
if (!device.isEmpty()) record("stop:" + device); |
||||||
|
if (device == "stop-stall") QThread::sleep(60); |
||||||
|
device.clear(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
class Heartbeat { |
||||||
|
QTimer timer; |
||||||
|
QElapsedTimer clock; |
||||||
|
qint64 previous = 0; |
||||||
|
public: |
||||||
|
qint64 maximum = 0; |
||||||
|
unsigned ticks = 0; |
||||||
|
Heartbeat() { |
||||||
|
clock.start(); timer.setInterval(5); timer.setTimerType(Qt::PreciseTimer); |
||||||
|
QObject::connect(&timer, &QTimer::timeout, [&] { |
||||||
|
const qint64 now = clock.elapsed(); maximum = std::max(maximum, now - previous); previous = now; ++ticks; |
||||||
|
}); timer.start(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
class CameraTest : public QObject { |
||||||
|
Q_OBJECT |
||||||
|
QTemporaryDir directory; |
||||||
|
QString path; |
||||||
|
QByteArray log() { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return {}; return file.readAll(); } |
||||||
|
qint64 pid() { return log().split(' ').value(0).toLongLong(); } |
||||||
|
private slots: |
||||||
|
void init() { |
||||||
|
path = directory.filePath(QString::number(QDateTime::currentMSecsSinceEpoch())); |
||||||
|
qputenv("UTUN_CAMERA_TEST_LOG", path.toUtf8()); |
||||||
|
} |
||||||
|
void latestFrameAndGeneration() { |
||||||
|
auto capture = std::make_unique<CallCameraCapture>(); |
||||||
|
unsigned frames = 0; qint64 maxAge = 0; QColor color; bool expectBlue = false, wrongGeneration = false; |
||||||
|
connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage& image, qint64 pts, qint64) { |
||||||
|
++frames; maxAge = std::max(maxAge, call_video_playout_now_us() - pts); color = image.pixelColor(0, 0); |
||||||
|
if (expectBlue && color != QColor(Qt::blue)) wrongGeneration = true; |
||||||
|
}); |
||||||
|
QSignalSpy errors(capture.get(), &CallCameraCapture::failed); |
||||||
|
capture->configure(true, "red", 1280, 720); |
||||||
|
{ Heartbeat heartbeat; |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(frames >= 15, 4000); |
||||||
|
qInfo("camera 720p latency max=%lldus GUI gap=%lldms", (long long)maxAge, (long long)heartbeat.maximum); |
||||||
|
QVERIFY(maxAge < 100000); QVERIFY(heartbeat.maximum < 150); } |
||||||
|
QCOMPARE(color, QColor(Qt::red)); |
||||||
|
// Пока GUI занят, разрешён лишь один передаваемый кадр; после паузы берётся свежий.
|
||||||
|
QThread::msleep(350); const unsigned before = frames; |
||||||
|
expectBlue = true; |
||||||
|
capture->configure(true, "blue", 1280, 720); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(frames > before && color == QColor(Qt::blue), 2000); |
||||||
|
QVERIFY(!wrongGeneration); |
||||||
|
const unsigned stopped = frames; capture->configure(false, "blue", 1280, 720); |
||||||
|
QTest::qWait(150); QCOMPARE(frames, stopped); QCOMPARE(errors.size(), 0); |
||||||
|
const qint64 child = pid(); QVERIFY(child > 0); capture.reset(); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); |
||||||
|
} |
||||||
|
void bufferLifetime() { |
||||||
|
CallCameraBuffer creator, peer; |
||||||
|
QVERIFY(creator.create()); const QString name = creator.name(); |
||||||
|
QVERIFY(peer.attach(name)); creator.data()[0] = 17; |
||||||
|
QCOMPARE(peer.data()[0], uchar(17)); |
||||||
|
creator.close(); peer.data()[0] = 23; QCOMPARE(peer.data()[0], uchar(23)); |
||||||
|
peer.close(); QVERIFY(!peer.attach(name)); |
||||||
|
} |
||||||
|
void blockedOperation_data() { |
||||||
|
QTest::addColumn<QByteArray>("device"); |
||||||
|
QTest::newRow("start") << QByteArray("start-stall"); |
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) |
||||||
|
QTest::newRow("conversion") << QByteArray("convert-stall"); |
||||||
|
#endif |
||||||
|
} |
||||||
|
void blockedOperation() { |
||||||
|
QFETCH(QByteArray, device); |
||||||
|
auto capture = std::make_unique<CallCameraCapture>(); |
||||||
|
QSignalSpy errors(capture.get(), &CallCameraCapture::failed); Heartbeat heartbeat; |
||||||
|
capture->configure(true, device); |
||||||
|
QTRY_COMPARE_WITH_TIMEOUT(errors.size(), 1, 7500); |
||||||
|
QVERIFY(heartbeat.ticks > 100); QVERIFY(heartbeat.maximum < 150); |
||||||
|
qInfo("blocked %s: GUI gap max=%lldms ticks=%u", device.constData(), (long long)heartbeat.maximum, heartbeat.ticks); |
||||||
|
if (device == "convert-stall") QVERIFY(log().contains("convert-blocked")); |
||||||
|
const qint64 child = pid(); QVERIFY(child > 0); QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3000); |
||||||
|
// Тот же владелец допускает явный повтор после отказа, без зависшего старого процесса.
|
||||||
|
unsigned frames = 0; |
||||||
|
connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage&, qint64, qint64) { ++frames; }); |
||||||
|
capture->configure(true, "blue"); QTRY_VERIFY_WITH_TIMEOUT(frames > 0, 3000); |
||||||
|
capture.reset(); |
||||||
|
} |
||||||
|
void destroyWhileStopping() { |
||||||
|
auto capture = std::make_unique<CallCameraCapture>(); |
||||||
|
unsigned frames = 0; |
||||||
|
connect(capture.get(), &CallCameraCapture::frameReady, this, [&](const QImage&, qint64, qint64) { ++frames; }); |
||||||
|
capture->configure(true, "stop-stall"); QTRY_VERIFY_WITH_TIMEOUT(frames > 0, 3000); |
||||||
|
const qint64 child = pid(); QVERIFY(child > 0); |
||||||
|
Heartbeat heartbeat; QElapsedTimer elapsed; elapsed.start(); capture.reset(); |
||||||
|
QVERIFY(elapsed.elapsed() < 50); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3500); |
||||||
|
QVERIFY(log().contains("stop:stop-stall")); QVERIFY(heartbeat.maximum < 150); |
||||||
|
} |
||||||
|
void applicationExit() { |
||||||
|
QProcess probe; probe.start(QCoreApplication::applicationFilePath(), {QStringLiteral("--exit-probe")}); |
||||||
|
QTRY_COMPARE_WITH_TIMEOUT(probe.state(), QProcess::NotRunning, 3000); |
||||||
|
QCOMPARE(probe.exitStatus(), QProcess::NormalExit); QCOMPARE(probe.exitCode(), 0); |
||||||
|
QVERIFY(!probe.readAllStandardError().contains("Destroyed while process")); |
||||||
|
} |
||||||
|
#ifdef TEST_CALL_VIDEO_ENGINE |
||||||
|
void engineKeepsCameraAcrossCodecChange() { |
||||||
|
auto engine = std::make_unique<CallVideoEngine>(); |
||||||
|
unsigned frames = 0; |
||||||
|
connect(engine.get(), &CallVideoEngine::localImageChanged, this, [&](const QImage& image) { if (!image.isNull()) ++frames; }); |
||||||
|
engine->start(7); engine->setVideo(true, "red", 640, 480, 1); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(frames >= 5, 4000); |
||||||
|
QCOMPARE(log().count("start:red"), 1); |
||||||
|
unsigned before = frames; engine->setVideo(true, "red", 640, 480, 2); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(frames >= before + 5, 2000); |
||||||
|
QCOMPARE(log().count("start:red"), 1); QVERIFY(!log().contains("stop:red")); |
||||||
|
engine->setVideo(true, "stop-stall", 640, 480, 2); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(log().contains("start:stop-stall"), 2000); |
||||||
|
const qint64 child = pid(); Heartbeat heartbeat; QElapsedTimer elapsed; elapsed.start(); |
||||||
|
engine.reset(); QVERIFY(elapsed.elapsed() < 150); |
||||||
|
QTRY_VERIFY_WITH_TIMEOUT(!alive(child), 3500); QVERIFY(heartbeat.maximum < 150); |
||||||
|
} |
||||||
|
#endif |
||||||
|
}; |
||||||
|
|
||||||
|
int main(int argc, char** argv) { |
||||||
|
QGuiApplication app(argc, argv); debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||||
|
if (app.arguments().value(1) == "--call-camera-worker") |
||||||
|
return call_camera_worker_run(app.arguments().value(2), std::make_unique<TestSource>()); |
||||||
|
if (app.arguments().value(1) == "--exit-probe") { |
||||||
|
auto capture = std::make_unique<CallCameraCapture>(); capture->configure(true, "stop-stall"); |
||||||
|
QTimer::singleShot(500, &app, &QCoreApplication::quit); |
||||||
|
const int result = app.exec(); capture.reset(); return result; |
||||||
|
} |
||||||
|
CameraTest test; return QTest::qExec(&test, argc, argv); |
||||||
|
} |
||||||
|
#include "test_call_camera_capture.moc" |
||||||
Loading…
Reference in new issue