|
|
|
|
@ -69,6 +69,15 @@ StreamPublishEngine::StreamPublishEngine(QObject* parent) : QObject(parent), m(s
|
|
|
|
|
m_timer.setTimerType(Qt::PreciseTimer); m_timer.setInterval(100); |
|
|
|
|
connect(&m_timer, &QTimer::timeout, this, &StreamPublishEngine::tick); m_timer.start(); |
|
|
|
|
#ifdef HAVE_STREAM_CAPTURE |
|
|
|
|
auto* devices = new QMediaDevices(this); |
|
|
|
|
connect(devices, &QMediaDevices::videoInputsChanged, this, &StreamPublishEngine::sourcesChanged); |
|
|
|
|
auto watchScreen = [this](QScreen* screen) { |
|
|
|
|
connect(screen, &QScreen::geometryChanged, this, &StreamPublishEngine::sourcesChanged); |
|
|
|
|
connect(screen, &QScreen::physicalDotsPerInchChanged, this, &StreamPublishEngine::sourcesChanged); |
|
|
|
|
}; |
|
|
|
|
for (auto* screen : QGuiApplication::screens()) watchScreen(screen); |
|
|
|
|
connect(qGuiApp, &QGuiApplication::screenAdded, this, [this, watchScreen](QScreen* screen) { watchScreen(screen); emit sourcesChanged(); }); |
|
|
|
|
connect(qGuiApp, &QGuiApplication::screenRemoved, this, &StreamPublishEngine::sourcesChanged); |
|
|
|
|
m_previewTimer.setTimerType(Qt::PreciseTimer); m_previewTimer.setInterval(33); |
|
|
|
|
connect(&m_previewTimer, &QTimer::timeout, this, [this] { |
|
|
|
|
if (!m->running) return; |
|
|
|
|
@ -124,12 +133,27 @@ void StreamPublishEngine::setMuted(bool muted) {
|
|
|
|
|
QList<StreamVideoSource> StreamPublishEngine::sources() { |
|
|
|
|
QList<StreamVideoSource> sources; |
|
|
|
|
#ifdef HAVE_STREAM_CAPTURE |
|
|
|
|
for (const auto& camera : QMediaDevices::videoInputs()) sources.push_back({StreamVideoSource::Camera, camera.description(), camera.id(), {}}); |
|
|
|
|
for (const auto& camera : QMediaDevices::videoInputs()) { |
|
|
|
|
StreamVideoSource source{StreamVideoSource::Camera, camera.description(), camera.id(), {}}; |
|
|
|
|
for (const auto& format : camera.videoFormats()) { |
|
|
|
|
stream_video_add_camera_modes(source.modes, format.resolution(), format.minFrameRate(), format.maxFrameRate()); |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop camera capability device=%s size=%dx%d pixel=%d fps=%.3f..%.3f", |
|
|
|
|
camera.id().constData(), format.resolution().width(), format.resolution().height(), format.pixelFormat(), |
|
|
|
|
format.minFrameRate(), format.maxFrameRate()); |
|
|
|
|
} |
|
|
|
|
if (source.modes.isEmpty()) DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "desktop camera has no usable modes device=%s", camera.id().constData()); |
|
|
|
|
sources.append(source); |
|
|
|
|
} |
|
|
|
|
if (QGuiApplication::platformName().contains("wayland") || qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland" |
|
|
|
|
|| qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { |
|
|
|
|
sources.push_back({StreamVideoSource::Screen, text("Screen (system selection)"), "portal", {}}); |
|
|
|
|
} else { |
|
|
|
|
for (auto* screen : QGuiApplication::screens()) sources.push_back({StreamVideoSource::Screen, screen->name(), screen->name().toUtf8(), {}}); |
|
|
|
|
for (auto* screen : QGuiApplication::screens()) { |
|
|
|
|
QSize size = screen->size() * screen->devicePixelRatio(); |
|
|
|
|
sources.push_back({StreamVideoSource::Screen, screen->name(), screen->name().toUtf8(), {}, size}); |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop screen capability source=%s logical=%dx%d scale=%.3f pixels=%dx%d", |
|
|
|
|
qPrintable(screen->name()), screen->size().width(), screen->size().height(), screen->devicePixelRatio(), size.width(), size.height()); |
|
|
|
|
} |
|
|
|
|
for (const auto& window : QWindowCapture::capturableWindows()) |
|
|
|
|
sources.push_back({StreamVideoSource::Window, window.description(), {}, QVariant::fromValue(window)}); |
|
|
|
|
} |
|
|
|
|
@ -177,15 +201,20 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) {
|
|
|
|
|
for (const auto& camera : QMediaDevices::videoInputs()) if (camera.id() == config.source.id) selected = camera; |
|
|
|
|
if (selected.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera disappeared"); return false; } |
|
|
|
|
m->camera = std::make_unique<QCamera>(selected); |
|
|
|
|
QCameraFormat best; int score = INT_MAX; |
|
|
|
|
QCameraFormat best; |
|
|
|
|
for (const auto& format : selected.videoFormats()) { |
|
|
|
|
if (format.maxFrameRate() + 0.1 < config.video.fps) continue; |
|
|
|
|
int candidate = std::abs(format.resolution().width() - config.video.width) |
|
|
|
|
+ std::abs(format.resolution().height() - config.video.height); |
|
|
|
|
if (candidate < score) { score = candidate; best = format; } |
|
|
|
|
if (!stream_video_camera_supports(format.resolution(), format.minFrameRate(), format.maxFrameRate(), |
|
|
|
|
{QSize(config.video.width, config.video.height), config.video.fps})) continue; |
|
|
|
|
if (best.isNull() || format.maxFrameRate() < best.maxFrameRate()) best = format; |
|
|
|
|
} |
|
|
|
|
if (best.isNull()) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera mode unavailable device=%s size=%dx%d fps=%d", |
|
|
|
|
config.source.id.constData(), config.video.width, config.video.height, config.video.fps); return false; |
|
|
|
|
} |
|
|
|
|
if (best.isNull()) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop camera lacks requested fps=%d", config.video.fps); return false; } |
|
|
|
|
m->camera->setCameraFormat(best); m->session->setCamera(m->camera.get()); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera selected device=%s size=%dx%d pixel=%d capture_fps=%.3f..%.3f output_fps=%d", |
|
|
|
|
config.source.id.constData(), best.resolution().width(), best.resolution().height(), best.pixelFormat(), |
|
|
|
|
best.minFrameRate(), best.maxFrameRate(), config.video.fps); |
|
|
|
|
connect(m->camera.get(), &QCamera::activeChanged, this, [this, generation](bool active) { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop camera active=%d generation=%llu", active, (unsigned long long)generation); |
|
|
|
|
if (!active && generation == m->sourceGeneration) sourceFailed(m->generation, text("Video capture stopped")); |
|
|
|
|
@ -259,7 +288,7 @@ bool StreamPublishEngine::start(uint64_t group, const StreamPublishConfig& confi
|
|
|
|
|
} |
|
|
|
|
m->running = true; |
|
|
|
|
try { |
|
|
|
|
m->videoThread = std::thread(&StreamPublishEngine::videoLoop, this); |
|
|
|
|
m->videoThread = std::thread(&StreamPublishEngine::videoLoop, this, config); |
|
|
|
|
if (config.audio) m->audioThread = std::thread(&StreamPublishEngine::audioLoop, this); |
|
|
|
|
} catch (const std::system_error& error) { publicationFailed(m->id, QString::fromUtf8(error.what())); return false; } |
|
|
|
|
if (!openSource(config) || (config.audio && !openMicrophone())) { |
|
|
|
|
@ -409,7 +438,7 @@ void StreamPublishEngine::audioLoop() {
|
|
|
|
|
opus_codec_encoder_destroy(encoder); audio_compressor_destroy(compressor); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void StreamPublishEngine::videoLoop() { |
|
|
|
|
void StreamPublishEngine::videoLoop(StreamPublishConfig initial) { |
|
|
|
|
#ifdef HAVE_STREAM_CAPTURE |
|
|
|
|
std::unique_ptr<LiveVideoEncoder> encoder; |
|
|
|
|
StreamPublishConfig encoding; |
|
|
|
|
@ -422,6 +451,31 @@ void StreamPublishEngine::videoLoop() {
|
|
|
|
|
if (next > now) { m->wake.wait_for(lock, std::chrono::microseconds(next - now)); continue; } |
|
|
|
|
if (m->frameGeneration == generation) { frame = m->frame; capture = m->framePts; m->frame = {}; } |
|
|
|
|
else capture = -1; } |
|
|
|
|
// Размер кадра точнее QScreen при HiDPI и единственный доступный после выбора Wayland portal.
|
|
|
|
|
if (config.source.kind == StreamVideoSource::Screen && frame.isValid() && config.source.size != frame.size()) { |
|
|
|
|
QSize size = stream_video_screen_size(frame.size(), config.nativeSize ? 4096 : std::min(config.video.width, config.video.height)); |
|
|
|
|
if (size.isEmpty()) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop screen invalid captured size=%dx%d", frame.width(), frame.height()); |
|
|
|
|
QMetaObject::invokeMethod(this, [this, generation] { sourceFailed(generation, text("Invalid stream video dimensions")); }); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
std::lock_guard<std::mutex> guard(m->lock); |
|
|
|
|
if (m->generation != generation) continue; |
|
|
|
|
config.source.size = frame.size(); |
|
|
|
|
if (config.video.width != size.width() || config.video.height != size.height()) { |
|
|
|
|
config.video.width = size.width(); config.video.height = size.height(); |
|
|
|
|
if (config.automaticBitrate) config.video.bitrate = std::clamp(size.width() * size.height() * config.video.fps / 11, 250000, 16000000); |
|
|
|
|
generation = ++m->generation; m->frameGeneration = generation; m->started = monotonicUs(); |
|
|
|
|
} |
|
|
|
|
m->wanted = config; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop screen size resolved source=%s raw=%dx%d target=%dx%d native=%d generation=%llu", |
|
|
|
|
qPrintable(config.source.name), frame.width(), frame.height(), size.width(), size.height(), config.nativeSize, |
|
|
|
|
(unsigned long long)generation); |
|
|
|
|
QMetaObject::invokeMethod(this, [this, generation] { if (generation == m->generation) emit sourcesChanged(); }); |
|
|
|
|
} |
|
|
|
|
if (config.source.kind == StreamVideoSource::Screen && config.source.size.isEmpty() && !frame.isValid()) { |
|
|
|
|
std::unique_lock<std::mutex> lock(m->lock); m->wake.wait_for(lock, std::chrono::milliseconds(5)); continue; |
|
|
|
|
} |
|
|
|
|
if (generation != current) { |
|
|
|
|
auto comparable = config.video; comparable.bitrate = encoding.video.bitrate; |
|
|
|
|
bool onlyBitrate = encoder && comparable == encoding.video && config.source.kind == encoding.source.kind |
|
|
|
|
@ -444,7 +498,9 @@ void StreamPublishEngine::videoLoop() {
|
|
|
|
|
}); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
encoder = std::move(candidate); current = generation; encoding = config; revision++; lastImage = {}; lastKey = -1000000; |
|
|
|
|
// Начальный формат publisher уже зарегистрирован; уточнение размера требует новой revision даже до первого KEY.
|
|
|
|
|
revision = revision ? revision + 1 : config.video == initial.video ? 1 : 2; |
|
|
|
|
encoder = std::move(candidate); current = generation; encoding = config; lastImage = {}; lastKey = -1000000; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "desktop video prepared generation=%llu revision=%u encoder=%s", |
|
|
|
|
(unsigned long long)generation, revision, qPrintable(encoder->name())); |
|
|
|
|
} |
|
|
|
|
@ -504,6 +560,8 @@ void StreamPublishEngine::videoLoop() {
|
|
|
|
|
report = now; count = skipped = 0; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
#else |
|
|
|
|
Q_UNUSED(initial); |
|
|
|
|
#endif |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|