#include #include #include #include #include #include "obs-app.hpp" #include "window-basic-main.hpp" #include "display-helpers.hpp" #include "qt-wrappers.hpp" #include "platform.hpp" static QList windowedProjectors; static QList multiviewProjectors; static bool updatingMultiview = false; static MultiviewLayout multiviewLayout; OBSProjector::OBSProjector(QWidget *widget, obs_source_t *source_, int monitor, QString title, ProjectorType type_) : OBSQTDisplay (widget, Qt::Window), source (source_), removedSignal (obs_source_get_signal_handler(source), "remove", OBSSourceRemoved, this) { projectorTitle = std::move(title); savedMonitor = monitor; isWindow = savedMonitor < 0; type = type_; if (isWindow) { setWindowIcon(QIcon(":/res/images/obs.png")); UpdateProjectorTitle(projectorTitle); windowedProjectors.push_back(this); resize(480, 270); } else { setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); QScreen *screen = QGuiApplication::screens()[savedMonitor]; setGeometry(screen->geometry()); QAction *action = new QAction(this); action->setShortcut(Qt::Key_Escape); addAction(action); connect(action, SIGNAL(triggered()), this, SLOT(EscapeTriggered())); } setAttribute(Qt::WA_DeleteOnClose, true); //disable application quit when last window closed setAttribute(Qt::WA_QuitOnClose, false); installEventFilter(CreateShortcutFilter()); auto addDrawCallback = [this] () { bool isMultiview = type == ProjectorType::Multiview; obs_display_add_draw_callback(GetDisplay(), isMultiview ? OBSRenderMultiview : OBSRender, this); obs_display_set_background_color(GetDisplay(), 0x000000); }; connect(this, &OBSQTDisplay::DisplayCreated, addDrawCallback); bool alwaysOnTop = config_get_bool(GetGlobalConfig(), "BasicWindow", "ProjectorAlwaysOnTop"); if (alwaysOnTop && !isWindow) SetAlwaysOnTop(this, true); bool hideCursor = config_get_bool(GetGlobalConfig(), "BasicWindow", "HideProjectorCursor"); if (hideCursor && !isWindow) { QPixmap empty(16, 16); empty.fill(Qt::transparent); setCursor(QCursor(empty)); } if (type == ProjectorType::Multiview) { obs_enter_graphics(); gs_render_start(true); gs_vertex2f(0.04f, 0.04f); gs_vertex2f(0.04f, 0.96f); gs_vertex2f(0.96f, 0.96f); gs_vertex2f(0.96f, 0.04f); gs_vertex2f(0.04f, 0.04f); innerBox = gs_render_save(); gs_render_start(true); gs_vertex2f(0.15f, 0.04f); gs_vertex2f(0.15f, 0.96f); leftVLine = gs_render_save(); gs_render_start(true); gs_vertex2f(0.85f, 0.04f); gs_vertex2f(0.85f, 0.96f); rightVLine = gs_render_save(); gs_render_start(true); gs_vertex2f(0.0f, 0.5f); gs_vertex2f(0.075f, 0.5f); leftLine = gs_render_save(); gs_render_start(true); gs_vertex2f(0.5f, 0.0f); gs_vertex2f(0.5f, 0.09f); topLine = gs_render_save(); gs_render_start(true); gs_vertex2f(0.925f, 0.5f); gs_vertex2f(1.0f, 0.5f); rightLine = gs_render_save(); obs_leave_graphics(); UpdateMultiview(); multiviewProjectors.push_back(this); } App()->IncrementSleepInhibition(); if (source) obs_source_inc_showing(source); ready = true; show(); // We need it here to allow keyboard input in X11 to listen to Escape if (!isWindow) activateWindow(); } OBSProjector::~OBSProjector() { bool isMultiview = type == ProjectorType::Multiview; obs_display_remove_draw_callback(GetDisplay(), isMultiview ? OBSRenderMultiview : OBSRender, this); if (source) obs_source_dec_showing(source); if (isMultiview) { for (OBSWeakSource &weakSrc : multiviewScenes) { OBSSource src = OBSGetStrongRef(weakSrc); if (src) obs_source_dec_showing(src); } obs_enter_graphics(); gs_vertexbuffer_destroy(innerBox); gs_vertexbuffer_destroy(leftVLine); gs_vertexbuffer_destroy(rightVLine); gs_vertexbuffer_destroy(leftLine); gs_vertexbuffer_destroy(topLine); gs_vertexbuffer_destroy(rightLine); obs_leave_graphics(); } if (type == ProjectorType::Multiview) multiviewProjectors.removeAll(this); if (isWindow) windowedProjectors.removeAll(this); App()->DecrementSleepInhibition(); } static OBSSource CreateLabel(const char *name, size_t h) { obs_data_t *settings = obs_data_create(); obs_data_t *font = obs_data_create(); std::string text; text += " "; text += name; text += " "; #if defined(_WIN32) obs_data_set_string(font, "face", "Arial"); #elif defined(__APPLE__) obs_data_set_string(font, "face", "Helvetica"); #else obs_data_set_string(font, "face", "Monospace"); #endif obs_data_set_int(font, "flags", 1); // Bold text obs_data_set_int(font, "size", int(h / 9.81)); obs_data_set_obj(settings, "font", font); obs_data_set_string(settings, "text", text.c_str()); obs_data_set_bool(settings, "outline", false); #ifdef _WIN32 const char *text_source_id = "text_gdiplus"; #else const char *text_source_id = "text_ft2_source"; #endif OBSSource txtSource = obs_source_create_private(text_source_id, name, settings); obs_source_release(txtSource); obs_data_release(font); obs_data_release(settings); return txtSource; } static inline void renderVB(gs_effect_t *effect, gs_vertbuffer_t *vb, int cx, int cy) { if (!vb) return; matrix4 transform; matrix4_identity(&transform); transform.x.x = cx; transform.y.y = cy; gs_load_vertexbuffer(vb); gs_matrix_push(); gs_matrix_mul(&transform); while (gs_effect_loop(effect, "Solid")) gs_draw(GS_LINESTRIP, 0, 0); gs_matrix_pop(); } static inline uint32_t labelOffset(obs_source_t *label, uint32_t cx) { uint32_t w = obs_source_get_width(label); w = uint32_t(float(w) * 0.5f); return (cx / 2) - w; } static inline void startRegion(int vX, int vY, int vCX, int vCY, float oL, float oR, float oT, float oB) { gs_projection_push(); gs_viewport_push(); gs_set_viewport(vX, vY, vCX, vCY); gs_ortho(oL, oR, oT, oB, -100.0f, 100.0f); } static inline void endRegion() { gs_viewport_pop(); gs_projection_pop(); } void OBSProjector::OBSRenderMultiview(void *data, uint32_t cx, uint32_t cy) { OBSProjector *window = (OBSProjector *)data; if (updatingMultiview || !window->ready) return; OBSBasic *main = (OBSBasic *)obs_frontend_get_main_window(); uint32_t targetCX, targetCY; int x, y; float fX, fY, halfCX, halfCY, sourceX, sourceY, labelX, labelY, quarterCX, quarterCY, scale, targetCXF, targetCYF, hiCX, hiCY, qiX, qiY, qiCX, qiCY, hiScaleX, hiScaleY, qiScaleX, qiScaleY; uint32_t offset; gs_effect_t *solid = obs_get_base_effect(OBS_EFFECT_SOLID); gs_eparam_t *color = gs_effect_get_param_by_name(solid, "color"); struct obs_video_info ovi; obs_get_video_info(&ovi); targetCX = ovi.base_width; targetCY = ovi.base_height; GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale); targetCXF = float(targetCX); targetCYF = float(targetCY); fX = float(x); fY = float(y); halfCX = (targetCXF + 1) / 2; halfCY = (targetCYF + 1) / 2; hiCX = (halfCX - 4.0); hiCY = (halfCY - 4.0); hiScaleX = hiCX / targetCXF; hiScaleY = hiCY / targetCYF; quarterCX = (halfCX + 1) / 2; quarterCY = (halfCY + 1) / 2; qiCX = (quarterCX - 8.0); qiCY = (quarterCY - 8.0); qiScaleX = qiCX / targetCXF; qiScaleY = qiCY / targetCYF; OBSSource previewSrc = main->GetCurrentSceneSource(); OBSSource programSrc = main->GetProgramSource(); bool studioMode = main->IsPreviewProgramMode(); auto renderVB = [solid, color](gs_vertbuffer_t *vb, int cx, int cy, uint32_t colorVal) { if (!vb) return; matrix4 transform; matrix4_identity(&transform); transform.x.x = cx; transform.y.y = cy; gs_load_vertexbuffer(vb); gs_matrix_push(); gs_matrix_mul(&transform); gs_effect_set_color(color, colorVal); while (gs_effect_loop(solid, "Solid")) gs_draw(GS_LINESTRIP, 0, 0); gs_matrix_pop(); }; auto drawBox = [solid, color](float cx, float cy, uint32_t colorVal) { gs_effect_set_color(color, colorVal); while (gs_effect_loop(solid, "Solid")) gs_draw_sprite(nullptr, 0, (uint32_t)cx, (uint32_t)cy); }; auto setRegion = [fX, fY, scale] (float x, float y, float cx, float cy) { float vX = int(fX + x * scale); float vY = int(fY + y * scale); float vCX = int(cx * scale); float vCY = int(cy * scale); float oL = x; float oT = y; float oR = (x + cx); float oB = (y + cy); startRegion(vX, vY, vCX, vCY, oL, oR, oT, oB); }; auto calcBaseSource = [&](size_t i) { switch (multiviewLayout) { case MultiviewLayout::VERTICAL_LEFT_8_SCENES: sourceX = halfCX; sourceY = (i / 2 ) * quarterCY; if (i % 2 != 0) sourceX = halfCX + quarterCX; break; case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: sourceX = 0; sourceY = (i / 2 ) * quarterCY; if (i % 2 != 0) sourceX = quarterCX; break; case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: if (i < 4) { sourceX = (float(i) * quarterCX); sourceY = 0; } else { sourceX = (float(i - 4) * quarterCX); sourceY = quarterCY; } break; default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES: if (i < 4) { sourceX = (float(i) * quarterCX); sourceY = halfCY; } else { sourceX = (float(i - 4) * quarterCX); sourceY = halfCY + quarterCY; } } }; auto calcPreviewProgram = [&](bool program) { switch (multiviewLayout) { case MultiviewLayout::VERTICAL_LEFT_8_SCENES: sourceX = 2.0f; sourceY = halfCY + 2.0f; labelX = offset; labelY = halfCY * 1.8f; if (program) { sourceY = 2.0f; labelY = halfCY * 0.8f; } break; case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: sourceX = halfCX + 2.0f; sourceY = halfCY + 2.0f; labelX = halfCX + offset; labelY = halfCY * 1.8f; if (program) { sourceY = 2.0f; labelY = halfCY * 0.8f; } break; case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: sourceX = 2.0f; sourceY = halfCY + 2.0f; labelX = offset; labelY = halfCY * 1.8f; if (program) { sourceX = halfCX + 2.0f; labelX = halfCX + offset; } break; default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES: sourceX = 2.0f; sourceY = 2.0f; labelX = offset; labelY = halfCY * 0.8f; if (program) { sourceX = halfCX + 2.0f; labelX = halfCX + offset; } } }; // Define the whole usable region for the multiview startRegion(x, y, targetCX * scale, targetCY * scale, 0.0f, targetCXF, 0.0f, targetCYF); // Change the background color to highlight all sources drawBox(targetCXF, targetCYF, outerColor); /* ----------------------------- */ /* draw sources */ for (size_t i = 0; i < 8; i++) { OBSSource src = OBSGetStrongRef(window->multiviewScenes[i]); obs_source *label = window->multiviewLabels[i + 2]; if (!src) continue; if (!label) continue; calcBaseSource(i); qiX = sourceX + 4.0f; qiY = sourceY + 4.0f; /* ----------- */ uint32_t colorVal = outerColor; if (src == programSrc) colorVal = programColor; else if (src == previewSrc) colorVal = studioMode ? previewColor : programColor; // Highlight preview/program sources with a proper color gs_matrix_push(); gs_matrix_translate3f(sourceX, sourceY, 0.0f); drawBox(quarterCX, quarterCY, colorVal); gs_matrix_pop(); // Change the background back of the source region gs_matrix_push(); gs_matrix_translate3f(qiX, qiY, 0.0f); drawBox(qiCX, qiCY, backgroundColor); gs_matrix_pop(); /* ----------- */ // Draw the actual source gs_matrix_push(); gs_matrix_translate3f(qiX, qiY, 0.0f); gs_matrix_scale3f(qiScaleX, qiScaleY, 1.0f); setRegion(qiX, qiY, qiCX, qiCY); obs_source_video_render(src); endRegion(); gs_matrix_pop(); /* ----------- */ offset = labelOffset(label, quarterCX); cx = obs_source_get_width(label); cy = obs_source_get_height(label); gs_matrix_push(); gs_matrix_translate3f(sourceX + offset, (quarterCY * 0.8f) + sourceY, 0.0f); drawBox(cx, cy + int(quarterCX * 0.015f), labelColor); obs_source_video_render(label); gs_matrix_pop(); } /* ----------------------------- */ /* draw preview */ obs_source_t *previewLabel = window->multiviewLabels[0]; offset = labelOffset(previewLabel, halfCX); calcPreviewProgram(false); gs_matrix_push(); gs_matrix_translate3f(sourceX, sourceY, 0.0f); gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); setRegion(sourceX, sourceY, hiCX, hiCY); if (studioMode) { // Change the background back of the source region drawBox(targetCXF, targetCYF, backgroundColor); obs_source_video_render(previewSrc); } else { obs_render_main_texture(); } endRegion(); renderVB(window->innerBox, targetCX, targetCY, outerColor); renderVB(window->leftVLine, targetCX, targetCY, outerColor); renderVB(window->rightVLine, targetCX, targetCY, outerColor); renderVB(window->leftLine, targetCX, targetCY, outerColor); renderVB(window->topLine, targetCX, targetCY, outerColor); renderVB(window->rightLine, targetCX, targetCY, outerColor); gs_matrix_pop(); /* ----------- */ cx = obs_source_get_width(previewLabel); cy = obs_source_get_height(previewLabel); gs_matrix_push(); gs_matrix_translate3f(labelX, labelY, 0.0f); drawBox(cx, cy + int(halfCX * 0.015f), labelColor); obs_source_video_render(previewLabel); gs_matrix_pop(); /* ----------------------------- */ /* draw program */ obs_source_t *programLabel = window->multiviewLabels[1]; offset = labelOffset(programLabel, halfCX); calcPreviewProgram(true); gs_matrix_push(); gs_matrix_translate3f(sourceX, sourceY, 0.0f); gs_matrix_scale3f(hiScaleX, hiScaleY, 1.0f); setRegion(sourceX, sourceY, hiCX, hiCY); obs_render_main_texture(); endRegion(); gs_matrix_pop(); /* ----------- */ cx = obs_source_get_width(programLabel); cy = obs_source_get_height(programLabel); gs_matrix_push(); gs_matrix_translate3f(labelX, labelY, 0.0f); drawBox(cx, cy + int(halfCX * 0.015f), labelColor); obs_source_video_render(programLabel); gs_matrix_pop(); endRegion(); } void OBSProjector::OBSRender(void *data, uint32_t cx, uint32_t cy) { OBSProjector *window = reinterpret_cast(data); if (!window->ready) return; OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); OBSSource source = window->source; uint32_t targetCX; uint32_t targetCY; int x, y; int newCX, newCY; float scale; if (source) { targetCX = std::max(obs_source_get_width(source), 1u); targetCY = std::max(obs_source_get_height(source), 1u); } else { struct obs_video_info ovi; obs_get_video_info(&ovi); targetCX = ovi.base_width; targetCY = ovi.base_height; } GetScaleAndCenterPos(targetCX, targetCY, cx, cy, x, y, scale); newCX = int(scale * float(targetCX)); newCY = int(scale * float(targetCY)); startRegion(x, y, newCX, newCY, 0.0f, float(targetCX), 0.0f, float(targetCY)); if (window->type == ProjectorType::Preview && main->IsPreviewProgramMode()) { OBSSource curSource = main->GetCurrentSceneSource(); if (source != curSource) { obs_source_dec_showing(source); obs_source_inc_showing(curSource); source = curSource; } } if (source) obs_source_video_render(source); else obs_render_main_texture(); endRegion(); } void OBSProjector::OBSSourceRemoved(void *data, calldata_t *params) { OBSProjector *window = reinterpret_cast(data); window->deleteLater(); UNUSED_PARAMETER(params); } static int getSourceByPosition(int x, int y) { struct obs_video_info ovi; obs_get_video_info(&ovi); float ratio = float(ovi.base_width) / float(ovi.base_height); QWidget *rec = QApplication::activeWindow(); int cx = rec->width(); int cy = rec->height(); int minX = 0; int minY = 0; int maxX = cx; int maxY = cy; int halfX = cx / 2; int halfY = cy / 2; int pos = -1; switch (multiviewLayout) { case MultiviewLayout::VERTICAL_LEFT_8_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; maxX = halfX + (validX / 2); } else { int validY = cx / ratio; minY = halfY - (validY / 2); maxY = halfY + (validY / 2); } minX = halfX; if (x < minX || x > maxX || y < minY || y > maxY) break; pos = 2 * ((y - minY) / ((maxY - minY) / 4)); if (x > minX + ((maxX - minX) / 2)) pos++; break; case MultiviewLayout::VERTICAL_RIGHT_8_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; minX = halfX - (validX / 2); } else { int validY = cx / ratio; minY = halfY - (validY / 2); maxY = halfY + (validY / 2); } maxX = halfX; if (x < minX || x > maxX || y < minY || y > maxY) break; pos = 2 * ((y - minY) / ((maxY - minY) / 4)); if (x > minX + ((maxX - minX) / 2)) pos++; break; case MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES: if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; minX = halfX - (validX / 2); maxX = halfX + (validX / 2); } else { int validY = cx / ratio; minY = halfY - (validY / 2); } maxY = halfY; if (x < minX || x > maxX || y < minY || y > maxY) break; pos = (x - minX) / ((maxX - minX) / 4); if (y > minY + ((maxY - minY) / 2)) pos += 4; break; default: // MultiviewLayout::HORIZONTAL_TOP_8_SCENES if (float(cx) / float(cy) > ratio) { int validX = cy * ratio; minX = halfX - (validX / 2); maxX = halfX + (validX / 2); } else { int validY = cx / ratio; maxY = halfY + (validY / 2); } minY = halfY; if (x < minX || x > maxX || y < minY || y > maxY) break; pos = (x - minX) / ((maxX - minX) / 4); if (y > minY + ((maxY - minY) / 2)) pos += 4; } return pos; } void OBSProjector::mouseDoubleClickEvent(QMouseEvent *event) { OBSQTDisplay::mouseDoubleClickEvent(event); if (!config_get_bool(GetGlobalConfig(), "BasicWindow", "TransitionOnDoubleClick")) return; OBSBasic *main = (OBSBasic*)obs_frontend_get_main_window(); if (!main->IsPreviewProgramMode()) return; if (event->button() == Qt::LeftButton) { int pos = getSourceByPosition(event->x(), event->y()); if (pos < 0) return; OBSSource src = OBSGetStrongRef(multiviewScenes[pos]); if (!src) return; if (main->GetProgramSource() != src) main->TransitionToScene(src); } } void OBSProjector::mousePressEvent(QMouseEvent *event) { OBSQTDisplay::mousePressEvent(event); if (event->button() == Qt::RightButton) { QMenu popup(this); popup.addAction(QTStr("Close"), this, SLOT(EscapeTriggered())); popup.exec(QCursor::pos()); } if (event->button() == Qt::LeftButton) { int pos = getSourceByPosition(event->x(), event->y()); if (pos < 0) return; OBSSource src = OBSGetStrongRef(multiviewScenes[pos]); if (!src) return; OBSBasic *main = (OBSBasic*)obs_frontend_get_main_window(); if (main->GetCurrentSceneSource() != src) main->SetCurrentScene(src, false); } } void OBSProjector::EscapeTriggered() { deleteLater(); } void OBSProjector::UpdateMultiview() { for (OBSWeakSource &val : multiviewScenes) val = nullptr; for (OBSSource &val : multiviewLabels) val = nullptr; struct obs_video_info ovi; obs_get_video_info(&ovi); uint32_t h = ovi.base_height; struct obs_frontend_source_list scenes = {}; obs_frontend_get_scenes(&scenes); int curIdx = 0; multiviewLabels[0] = CreateLabel(Str("StudioMode.Preview"), h / 2); multiviewLabels[1] = CreateLabel(Str("StudioMode.Program"), h / 2); for (size_t i = 0; i < scenes.sources.num && curIdx < 8; i++) { obs_source_t *src = scenes.sources.array[i]; OBSData data = obs_source_get_private_settings(src); obs_data_release(data); obs_data_set_default_bool(data, "show_in_multiview", true); if (!obs_data_get_bool(data, "show_in_multiview")) continue; multiviewScenes[curIdx] = OBSGetWeakRef(src); obs_source_inc_showing(src); std::string name; name += std::to_string(curIdx + 1); name += " - "; name += obs_source_get_name(src); multiviewLabels[curIdx + 2] = CreateLabel(name.c_str(), h / 3); curIdx++; } obs_frontend_source_list_free(&scenes); multiviewLayout = static_cast(config_get_int( GetGlobalConfig(), "BasicWindow", "MultiviewLayout")); } void OBSProjector::UpdateProjectorTitle(QString name) { projectorTitle = name; QString title = nullptr; switch (type) { case ProjectorType::Scene: title = QTStr("SceneWindow") + " - " + name; break; case ProjectorType::Source: title = QTStr("SourceWindow") + " - " + name; break; default: title = name; break; } setWindowTitle(title); } OBSSource OBSProjector::GetSource() { return source; } ProjectorType OBSProjector::GetProjectorType() { return type; } int OBSProjector::GetMonitor() { return savedMonitor; } void OBSProjector::UpdateMultiviewProjectors() { obs_enter_graphics(); updatingMultiview = true; obs_leave_graphics(); for (auto &projector : multiviewProjectors) projector->UpdateMultiview(); obs_enter_graphics(); updatingMultiview = false; obs_leave_graphics(); } void OBSProjector::RenameProjector(QString oldName, QString newName) { for (auto &projector : windowedProjectors) if (projector->projectorTitle == oldName) projector->UpdateProjectorTitle(newName); }