Phase K — Surface Isolation, Multi-Monitor & Display Management
Architectural Plan — Mcaster1Studio v0.2.0 — Created 2026-03-10
Goal: Transform Mcaster1Studio from a single-database, single-window application
into a true multi-surface broadcast suite where each surface operates as an independent broadcast
entity with its own database, settings, media library, and display output — while sharing
system hardware resources (audio devices, capture cards, monitors).
1. Architecture Overview
Current Architecture (Pre-K)
- Single global database (SQLite or MySQL)
- All surfaces share one
QSettings instance
- All surfaces embedded in one
QTabWidget
MediaLibraryModule::createDatabase() reads global settings
- No monitor awareness or multi-window support
- No video capture device enumeration
Target Architecture (Post-K)
- Database Server Registry — N servers defined in System Preferences
- Per-surface database isolation — each surface gets its own DB/schema
- Pop-out surface windows — any surface can detach to its own
QMainWindow
- Monitor management — detect all displays, resolution, GPU; "Send to Monitor"
- Video capture device registry — enumerate webcams, capture cards, virtual cameras
- YAML failover — all surface config mirrored to local YAML for DB-down redundancy
- Shared system resources — audio devices, hardware remain global
Conceptual Diagram
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SYSTEM LAYER (shared) │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │PortAudio │ │ Monitor │ │ Capture │ │ DB Server Registry │ │
│ │ Devices │ │ Manager │ │ Device Mgr │ │ [Server A] [Server B]... │ │
│ └──────────┘ └──────────┘ └──────────────┘ └───────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ SURFACE LAYER (isolated per surface) │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Surface Alpha │ │ Surface "GRR FM" │ ...Nth │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ DB: Server A │ │ │ │ DB: Server B │ │ │
│ │ │ Schema: alpha_db │ │ │ │ Schema: grrfm_db │ │ │
│ │ ├─────────────────┤ │ │ ├─────────────────┤ │ │
│ │ │ Media Library │ │ │ │ Media Library │ │ │
│ │ │ Playlists │ │ │ │ Playlists │ │ │
│ │ │ Encoder Configs │ │ │ │ Encoder Configs │ │ │
│ │ │ Metadata Presets │ │ │ │ Metadata Presets │ │ │
│ │ │ Surface Settings │ │ │ │ Surface Settings │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ │ Window: Embedded Tab │ │ Window: Monitor 2 │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
2. Deliverable K.1 — Database Server Registry
Concept: System Preferences gains a "Database Servers" management panel where
users can define multiple named database connections. Each server entry has a unique name (e.g.
"Local SQLite", "Production MySQL", "Backup MariaDB") and connection parameters. Surfaces
reference servers by name, not by raw connection strings.
K.1a — DbServerEntry struct
File: Core/include/DbServerEntry.h (NEW)
#pragma once
#include <QString>
#include <QList>
#include <QSettings>
#include <QUuid>
namespace M1 {
struct DbServerEntry {
QString id; ///< Unique ID (UUID, generated on creation)
QString displayName; ///< "Local SQLite", "Production MySQL", etc.
enum class Backend { SQLite, MySQL };
Backend backend = Backend::SQLite;
// SQLite-specific
QString sqlitePath; ///< Empty = AppData default location
// MySQL-specific
QString host = "127.0.0.1";
int port = 3306;
QString username = "root";
QString password;
bool isSQLite() const { return backend == Backend::SQLite; }
bool isMySQL() const { return backend == Backend::MySQL; }
/// Generate a unique ID for a new server entry
static QString newId() { return QUuid::createUuid().toString(QUuid::WithoutBraces); }
};
/// DbServerRegistry — singleton managing all configured database servers.
/// Persisted in QSettings under "dbservers/" prefix.
class DbServerRegistry {
public:
static DbServerRegistry& instance();
QList<DbServerEntry> servers() const;
const DbServerEntry* findById(const QString& id) const;
const DbServerEntry* findByName(const QString& name) const;
const DbServerEntry* defaultServer() const; ///< First entry, or built-in SQLite
void addServer(const DbServerEntry& entry);
void updateServer(const DbServerEntry& entry);
void removeServer(const QString& id);
void setDefaultServerId(const QString& id);
void loadFromSettings();
void saveToSettings() const;
/// Test connection to a server. Returns empty string on success, error message on failure.
QString testConnection(const DbServerEntry& entry) const;
private:
DbServerRegistry();
QList<DbServerEntry> m_servers;
QString m_defaultServerId;
};
} // namespace M1
K.1b — QSettings Key Layout
| Key | Type | Description |
dbservers/count | int | Number of configured servers |
dbservers/default | string | UUID of default server |
dbservers/{i}/id | string | Server UUID |
dbservers/{i}/name | string | Display name |
dbservers/{i}/backend | string | "sqlite" or "mysql" |
dbservers/{i}/sqlite/path | string | SQLite file path (empty=default) |
dbservers/{i}/mysql/host | string | MySQL hostname |
dbservers/{i}/mysql/port | int | MySQL port |
dbservers/{i}/mysql/user | string | MySQL username |
dbservers/{i}/mysql/pass | string | MySQL password (encrypted v2) |
K.1c — First-Run Default
On first launch (no dbservers/count key), the registry auto-creates one entry:
{ id: auto-uuid, name: "Local SQLite (Default)", backend: SQLite, sqlitePath: "" }
This preserves backward compatibility — existing single-DB users see no change.
K.1d — Migration from Old Settings
Upgrade path: If database/backend exists (old format) and dbservers/count
does not, migrate the old settings into a single DbServerEntry and remove the legacy keys.
This runs once at startup in DbServerRegistry::loadFromSettings().
3. Deliverable K.2 — Per-Surface Database Isolation
Concept: Each surface stores a dbServerId + dbSchemaName in its
session config. When MediaLibraryModule (or any DB-aware module) is created for a surface,
it receives the surface’s DB config — NOT the global default. Surfaces sharing the same
server but different schemas are fully isolated. Surfaces on different servers are even more isolated.
K.2a — Surface Database Config
New fields in session v2 QSettings:
| Key | Type | Description |
session/v2/{si}/dbServerId | string | UUID of assigned DB server |
session/v2/{si}/dbSchemaName | string | Unique schema/DB name (e.g. "mcaster1_alpha") |
session/v2/{si}/dbTablePrefix | string | Table prefix for shared-server isolation (e.g. "alpha_") |
K.2b — SurfaceDbContext
File: Core/include/SurfaceDbContext.h (NEW)
namespace M1 {
/// Encapsulates the database identity for a single surface.
/// Passed to modules at creation time so they connect to the right DB/schema.
struct SurfaceDbContext {
QString serverId; ///< DbServerEntry UUID
QString schemaName; ///< Database name (MySQL) or file stem (SQLite)
QString tablePrefix; ///< Optional prefix for table names in shared DBs
/// Create an IDatabase* connected to this surface's database.
/// Caller owns the returned pointer.
IDatabase* createConnection() const;
/// Generate default schema name from surface name.
/// "GRR FM Broadcast" → "mcaster1_grr_fm_broadcast"
static QString nameToSchema(const QString& surfaceName);
bool isValid() const { return !serverId.isEmpty() && !schemaName.isEmpty(); }
};
} // namespace M1
K.2c — Schema Naming Convention
| Backend | Schema Name Maps To | Example |
| SQLite | Separate .db file in AppData dir | AppData/Mcaster1Studio/mcaster1_alpha.db |
| MySQL | CREATE DATABASE IF NOT EXISTS `{schemaName}` | mcaster1_grr_fm_broadcast |
K.2d — Extended Schema (Beyond media_items)
Each surface database gets its own set of tables:
media_items — The media library (existing schema)
playlists — Saved playlists (id, name, created, modified)
playlist_items — Playlist entries (playlist_id, media_id, position)
encoder_configs — Per-surface encoder slot configurations
metadata_presets — ICY metadata templates / presets
broadcast_log — What was played, when, by whom
surface_settings — Key/value store for surface-specific prefs
surface_meta — Surface identity (name, type, created, last_modified)
K.2e — Module Database Injection
// In MainWindow::createModule(), before module->initialize():
if (auto* dbAware = qobject_cast<IDbAwareModule*>(mod)) {
dbAware->setSurfaceDbContext(surface->dbContext());
}
New interface: IDbAwareModule (optional mixin) — modules that need
database access implement this. MediaLibraryModule, PlaylistModule,
EncoderModule, MetadataModule, and PodcastModule variants
all become IDbAwareModule implementors.
K.2f — YAML Failover Mirror
Redundancy pattern: Every surface writes a shadow YAML file alongside its DB:
AppData/Mcaster1Studio/surfaces/{surfaceName}.yaml
Contains: surface identity, module list, encoder configs, last-known metadata presets.
On startup: if DB connection fails → read from YAML fallback → operate in read-only mode
with a banner warning “Database offline — running from local cache”.
YAML sync: written after every saveAllSessionState() call (debounced with the 1.5s timer).
4. Deliverable K.3 — Surface Creation Wizard
Concept: When creating a new custom surface, a multi-step wizard dialog guides
the user through: naming, database server selection, schema creation, and module selection.
Replaces the current simple CustomSurfaceDialog.
Wizard Steps
| Step | Title | Contents |
| 1 | Surface Identity | Surface name (used for tab label + schema name), surface type dropdown (or "Custom"), icon color picker |
| 2 | Database Server |
Dropdown of all servers from DbServerRegistry + "Add New Server..." button.
Schema name auto-generated from surface name (editable).
"Test Connection" button.
Checkbox: "Create database and tables now" (default: checked).
|
| 3 | Module Selection |
Dual-list or checkbox grid of available modules.
Pre-populated from surface type template (if not Custom).
"Select All" / "Clear" buttons.
|
| 4 | Confirmation |
Summary: surface name, DB server, schema, selected modules.
"Create Surface" button → creates DB schema + tables, creates surface, adds modules.
|
File: UI/SurfaceCreationWizard.h/.cpp (NEW)
class SurfaceCreationWizard : public QWizard {
Q_OBJECT
public:
struct Result {
QString surfaceName;
M1::SurfaceType surfaceType;
QString dbServerId;
QString dbSchemaName;
QStringList moduleIds;
QColor tabColor;
};
explicit SurfaceCreationWizard(QWidget* parent = nullptr);
Result result() const;
private:
QWizardPage* buildIdentityPage();
QWizardPage* buildDatabasePage();
QWizardPage* buildModulesPage();
QWizardPage* buildConfirmPage();
};
5. Deliverable K.4 — Pop-Out Surface Windows & Multi-Monitor
Concept: Right-clicking a surface tab shows "Pop Out Window" which detaches the
surface into its own QMainWindow that can be resized, maximized, minimized, moved
to any monitor, and made fullscreen (F11). The surface tab in the main window becomes a
placeholder with a "Bring Back" button. When the popped-out window is closed, the surface
returns to its tab.
K.4a — SurfaceWindow class
File: UI/SurfaceWindow.h/.cpp (NEW)
/// A standalone QMainWindow hosting a single popped-out SurfaceWidget.
/// Fully resizable, movable to any monitor, supports maximize/minimize/fullscreen.
class SurfaceWindow : public QMainWindow {
Q_OBJECT
public:
explicit SurfaceWindow(SurfaceWidget* surface, QWidget* parent = nullptr);
~SurfaceWindow() override;
SurfaceWidget* surface() const { return m_surface; }
/// Move this window to a specific monitor by QScreen index.
void moveToScreen(int screenIndex);
/// Snap to a monitor's full geometry (maximized on that monitor).
void maximizeOnScreen(int screenIndex);
signals:
void dockBackRequested(SurfaceWidget* surface); ///< User wants to re-embed in main tab
protected:
void closeEvent(QCloseEvent* event) override;
void keyPressEvent(QKeyEvent* event) override; ///< F11 = fullscreen toggle
private:
SurfaceWidget* m_surface;
QAction* m_fullscreenAction = nullptr;
bool m_isFullscreen = false;
};
K.4b — Pop-out / dock-back flow
// In SurfaceTabBar right-click menu:
auto* popOutAct = menu.addAction("Pop Out Window");
auto* sendToMonAct = menu.addMenu("Send to Monitor");
// Pop out:
1. SurfaceWidget* sw = surfaceAt(tabIndex);
2. m_surfaceBar->removeTab(tabIndex); // Remove from tab widget
3. Insert placeholder tab: "⬒ {name} (popped out)" with "Bring Back" button
4. auto* win = new SurfaceWindow(sw);
5. win->show();
6. m_poppedOutWindows[sw] = win;
// Dock back (close window or right-click "Dock Back"):
1. SurfaceWindow emits dockBackRequested(sw)
2. Remove placeholder tab
3. Re-insert sw into m_surfaceBar at original index
4. Delete SurfaceWindow
5. m_poppedOutWindows.remove(sw)
K.4c — "Send to Monitor" submenu
Dynamically populated from
QGuiApplication::screens():
- Monitor 1: DELL U2722D — 2560×1440 @ 60Hz (Primary)
- Monitor 2: Samsung LU28E570 — 3840×2160 @ 60Hz
- Monitor 3: AVerMedia Live Gamer — 1920×1080 @ 60Hz
Clicking a monitor: pops out the surface AND moves the window to that monitor, maximized.
K.4d — Window Flags & Behavior
| Feature | Implementation |
| Resizable | Default QMainWindow flags (all edges + corners) |
| Maximize/Minimize | Standard title bar buttons |
| Fullscreen (F11) | showFullScreen() / showNormal() toggle |
| Multi-monitor drag | Native OS window management (no custom handling needed) |
| Title bar | "Mcaster1Studio — {Surface Name}" |
| Close behavior | Emits dockBackRequested — does NOT destroy the surface |
| Session save | Popped-out state + monitor index + geometry saved to QSettings |
| Session restore | On app startup, restore popped-out surfaces to their saved monitors |
K.4e — Session Persistence for Pop-Out Windows
| QSettings Key | Type | Description |
session/v2/{si}/poppedOut | bool | Whether this surface is in a pop-out window |
session/v2/{si}/window/screenIndex | int | Which monitor the window is on |
session/v2/{si}/window/x | int | Window X position |
session/v2/{si}/window/y | int | Window Y position |
session/v2/{si}/window/width | int | Window width |
session/v2/{si}/window/height | int | Window height |
session/v2/{si}/window/maximized | bool | Whether window was maximized |
session/v2/{si}/window/fullscreen | bool | Whether window was fullscreen |
6. Deliverable K.5 — Monitor & Display Management
Concept: System Preferences gains a "Displays & Capture" page that shows all
attached monitors, their properties, and video capture devices. This enables intelligent surface
placement, video input selection for church/entertainment surfaces, and virtual camera output.
K.5a — MonitorManager class
File: Core/include/MonitorManager.h (NEW)
namespace M1 {
struct MonitorInfo {
int index; ///< QScreen index (0-based)
QString name; ///< "DELL U2722D", "\\.\DISPLAY1"
QString manufacturer; ///< "Dell Inc."
QSize resolution; ///< Native resolution (e.g. 2560x1440)
QSize physicalSizeMm; ///< Physical size in millimeters
double refreshRate; ///< Hz (e.g. 60.0, 144.0)
double dpi; ///< Effective DPI
double scaleFactor; ///< Qt device pixel ratio
QRect geometry; ///< Virtual desktop position + size
QRect availableGeometry; ///< Geometry minus taskbar
bool isPrimary; ///< Primary display?
QString gpuName; ///< GPU driving this display (e.g. "NVIDIA RTX 4070")
qint64 gpuVramBytes; ///< GPU VRAM in bytes
};
struct CaptureDeviceInfo {
QString deviceId; ///< System device path / ID
QString displayName; ///< "Logitech C920", "AVerMedia Live Gamer"
QSize maxResolution; ///< Maximum capture resolution
double maxFps; ///< Maximum framerate
bool isVirtualCamera; ///< OBS Virtual Camera, etc.
enum class Type { Webcam, CaptureCard, ScreenCapture, VirtualCamera };
Type type;
};
class MonitorManager : public QObject {
Q_OBJECT
public:
static MonitorManager& instance();
QList<MonitorInfo> monitors() const;
QList<CaptureDeviceInfo> captureDevices() const;
const MonitorInfo* monitorAt(int index) const;
const MonitorInfo* primaryMonitor() const;
/// Refresh monitor list (call after display configuration changes)
void refresh();
/// Refresh capture device list (Windows: DirectShow/MediaFoundation enumeration)
void refreshCaptureDevices();
signals:
void monitorsChanged(); ///< Display added/removed/resolution changed
void captureDevicesChanged(); ///< USB device plugged/unplugged
private:
MonitorManager(QObject* parent = nullptr);
void enumerateMonitors();
void enumerateCaptureDevices();
void enumerateGpuInfo(); ///< DXGI adapter enumeration (Windows)
QList<MonitorInfo> m_monitors;
QList<CaptureDeviceInfo> m_captureDevices;
};
} // namespace M1
K.5b — GPU Enumeration (Windows)
// DXGI adapter enumeration for GPU info
#include <dxgi1_4.h>
#pragma comment(lib, "dxgi.lib")
IDXGIFactory1* factory = nullptr;
CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory);
IDXGIAdapter1* adapter = nullptr;
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i) {
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
// desc.Description → GPU name (wchar_t[128])
// desc.DedicatedVideoMemory → VRAM in bytes
// desc.VendorId → 0x10DE (NVIDIA), 0x1002 (AMD), 0x8086 (Intel)
adapter->Release();
}
factory->Release();
K.5c — Capture Device Enumeration (Windows)
Primary: MediaFoundation (MFEnumDeviceSources with
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID).
Fallback: DirectShow (ICreateDevEnum +
CLSID_VideoInputDeviceCategory).
For each device: read friendly name, max output resolution, max FPS.
Virtual cameras detected by name heuristic ("OBS Virtual Camera", "ManyCam", etc.)
or by checking for KSCATEGORY_VIDEO_CAMERA vs custom categories.
K.5d — PreferencesDialog "Displays & Capture" Tab
┌────────────────────────────────────────────────────────────────────────┐
│ DISPLAYS & CAPTURE │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Monitors [↻ Refresh] │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ # │ Name │ Resolution │ Hz │ DPI │ GPU │ │
│ │ 1 │ DELL U2722D ★ │ 2560×1440 │ 60 │ 109 │ RTX 4070 │ │
│ │ 2 │ Samsung LU28E570 │ 3840×2160 │ 60 │ 157 │ RTX 4070 │ │
│ │ 3 │ AVerMedia C1 │ 1920×1080 │ 60 │ 96 │ Intel UHD │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Video Capture Devices [↻ Refresh] │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Device │ Type │ Max Res │ Max FPS │ Virt │ │
│ │ Logitech C920 │ Webcam │ 1920×1080 │ 30 │ │ │
│ │ AVerMedia Live G. │ Capture Card │ 1920×1080 │ 60 │ │ │
│ │ OBS Virtual Camera │ Virtual Cam │ 1920×1080 │ 30 │ ✓ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ GPU Information │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ NVIDIA GeForce RTX 4070 — 12,288 MB VRAM │ │
│ │ Intel UHD Graphics 770 — 512 MB shared │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
K.5e — QScreen Change Detection
MonitorManager connects to QGuiApplication::screenAdded,
screenRemoved, and per-screen geometryChanged signals to auto-refresh
the monitor list. If a popped-out surface’s monitor is disconnected, the window is
automatically moved back to the primary monitor with a notification toast.
7. Deliverable K.6 — PreferencesDialog Redesign
Updated Tab Structure
| Tab | Contents | Status |
| 1. General | Theme, language, startup behavior | Existing |
| 2. Audio Devices | PortAudio device selection, sample rate, buffer size | Existing |
| 3. Database Servers | NEW: Server registry list + Add/Edit/Remove/Test/Set Default | New |
| 4. Displays & Capture | NEW: Monitor list, capture devices, GPU info | New |
| 5. Metrics / Grafana | Prometheus endpoint config, Grafana Cloud push | Existing |
Database Servers Tab Layout
┌────────────────────────────────────────────────────────────────────────┐
│ DATABASE SERVERS │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ [+ Add Server] [✎ Edit] [✕ Remove] [★ Set Default] [⚡ Test] │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ★ │ Name │ Backend │ Host │ Status │ │
│ │ ★ │ Local SQLite (Default) │ SQLite │ (embedded) │ ● OK │ │
│ │ │ Production MySQL │ MySQL │ db.grr.fm │ ● OK │ │
│ │ │ Backup MariaDB │ MySQL │ backup.lan │ ● Offline│ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ [Add Server] dialog: │
│ ┌──────────────────────────────────────────────┐ │
│ │ Name: [________________________] │ │
│ │ Backend: ( ) SQLite (•) MySQL/MariaDB │ │
│ │ │ │
│ │ ── MySQL Settings ────────────────────── │ │
│ │ Host: [db.grr.fm_____________] │ │
│ │ Port: [3306___] │ │
│ │ User: [mcaster1_______________] │ │
│ │ Pass: [••••••••_______________] │ │
│ │ │ │
│ │ [Test Connection] [Cancel] [Save] │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ Surface Assignments: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Surface Alpha → Local SQLite │ mcaster1_alpha │ │
│ │ GRR FM Broadcast → Production MySQL │ mcaster1_grr_fm │ │
│ │ Church AV → Local SQLite │ mcaster1_church_av │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
8. Deliverable K.7 — Surface Tab Context Menu Enhancement
Updated Right-Click Menu Structure
┌─────────────────────────────────────┐
│ Add Module ▸ │
│ Create new Sub Surface ▸ │
│ ────────────────────────────────── │
│ Pop Out Window │ ◄── NEW: detach to QMainWindow
│ Send to Monitor ▸ │ ◄── NEW: submenu of detected monitors
│ ────────────────────────────────── │
│ Database... │ ◄── NEW: view/change surface DB assignment
│ Surface Settings... │ ◄── NEW: per-surface preferences
│ ────────────────────────────────── │
│ Save Session │
│ Rename... │
│ Close Surface │
└─────────────────────────────────────┘
"Send to Monitor" Submenu
Dynamically built from
MonitorManager::monitors():
┌──────────────────────────────────────────────────────┐
│ ● Monitor 1: DELL U2722D — 2560×1440 (Primary) │
│ ● Monitor 2: Samsung LU28E570 — 3840×2160 │
│ ● Monitor 3: AVerMedia C1 — 1920×1080 │
└──────────────────────────────────────────────────────┘
Clicking a monitor:
popOutSurface(sw) +
win->maximizeOnScreen(idx).
9. Shared vs Isolated Resources
| Resource | Scope | Notes |
| Audio Devices (PortAudio) | System (shared) | All surfaces share the same audio I/O device |
| Audio Engine (AudioMixer) | System (shared) | Single RT callback dispatches to all surface modules |
| Monitors / Displays | System (shared) | Any surface can be sent to any monitor |
| Capture Devices | System (shared) | Available to any surface; exclusive access per device |
| Theme / QSS | System (shared) | One active theme across all surfaces |
| Plugin Registry | System (shared) | DLLs loaded once, available to all surfaces |
| Isolated per surface |
| Database Connection | Surface | Each surface has its own IDatabase* instance |
| Media Library | Surface | Own media_items table in own schema |
| Playlists | Surface | Own playlists + playlist_items tables |
| Encoder Configurations | Surface | Own encoder_configs table |
| Metadata Presets | Surface | Own metadata_presets table |
| Broadcast Log | Surface | Own broadcast_log table |
| Surface Settings | Surface | Own surface_settings key/value table |
| YAML Failover Cache | Surface | Own YAML file in AppData/surfaces/ |
| Window State | Surface | Own pop-out geometry, monitor, fullscreen state |
10. File Manifest
| File | Action | Description |
| New Files |
Core/include/DbServerEntry.h | NEW | DbServerEntry struct + DbServerRegistry singleton |
Core/src/DbServerRegistry.cpp | NEW | Registry implementation: CRUD, QSettings I/O, test, migration |
Core/include/SurfaceDbContext.h | NEW | Per-surface DB identity (serverId + schemaName + prefix) |
Core/src/SurfaceDbContext.cpp | NEW | createConnection() factory, nameToSchema() helper |
Core/include/IDbAwareModule.h | NEW | Optional mixin interface for DB-aware modules |
UI/SurfaceCreationWizard.h/.cpp | NEW | Multi-step QWizard for new surface creation |
UI/SurfaceWindow.h/.cpp | NEW | Pop-out QMainWindow hosting a SurfaceWidget |
UI/DbServerDialog.h/.cpp | NEW | Add/Edit dialog for a single DB server entry |
Core/include/MonitorManager.h | NEW | Monitor + capture device + GPU enumeration |
Core/src/MonitorManager.cpp | NEW | DXGI + MF enumeration, QScreen tracking |
| Modified Files |
Core/include/SurfaceConfig.h | MODIFY | Update DbConfig to reference DbServerEntry by ID |
Core/CMakeLists.txt | MODIFY | Add new source files, link dxgi.lib |
UI/PreferencesDialog.h/.cpp | MODIFY | Replace Database tab with DB Servers; add Displays & Capture tab |
UI/SurfaceTabBar.cpp | MODIFY | Add Pop Out / Send to Monitor / Database context menu items |
UI/SurfaceWidget.h/.cpp | MODIFY | Add dbContext() accessor, YAML failover write |
UI/MainWindow.h/.cpp | MODIFY | SurfaceWindow management, DB injection into modules, wizard wiring |
Modules/MediaLibraryModule/MediaLibraryModule.cpp | MODIFY | Implement IDbAwareModule, use SurfaceDbContext |
UI/MainWindow.cpp (session save/load) | MODIFY | Save/restore pop-out window state + DB context per surface |
Core/include/IDatabase.h | MODIFY | Add extended schema methods (playlists, encoder_configs, etc.) |
Modules/MediaLibraryModule/SqliteManager.h/.cpp | MODIFY | Extended schema creation, table prefix support |
Modules/MediaLibraryModule/DatabaseManager.h/.cpp | MODIFY | Extended schema creation, table prefix support |
11. Implementation Order
| Step | Deliverable | Dependencies | Est. Files |
| K.1 | DbServerEntry + DbServerRegistry | None | 2 new + 1 mod |
| K.2 | MonitorManager + GPU/capture enum | None | 2 new |
| K.3 | PreferencesDialog DB Servers tab + Displays tab | K.1, K.2 | 3 mod + 1 new |
| K.4 | SurfaceDbContext + IDbAwareModule | K.1 | 3 new |
| K.5 | Per-surface DB injection into modules | K.4 | 4 mod |
| K.6 | SurfaceCreationWizard | K.1, K.4 | 1 new + 1 mod |
| K.7 | SurfaceWindow (pop-out) | K.2 | 1 new + 2 mod |
| K.8 | Tab context menu (Pop Out + Send to Monitor + Database) | K.7, K.2 | 1 mod |
| K.9 | YAML failover mirror | K.5 | 2 mod |
| K.10 | Session save/restore for pop-out + DB context | K.7, K.5 | 1 mod |
| K.11 | Extended DB schema (playlists, encoder_configs, etc.) | K.5 | 3 mod |
| K.12 | Old settings migration + first-run defaults | K.1 | 1 mod |
12. Verification Checklist
- DB Server Registry: Add 3 servers (1 SQLite, 2 MySQL) in Preferences → all persist across restart
- Test Connection: Green checkmark on valid connection, red error on bad credentials/host
- Surface Creation: Wizard creates schema + all 8 tables on selected server
- DB Isolation: Surface Alpha media library has 100 tracks; Surface Beta has 0 — completely separate
- Pop Out: Right-click → Pop Out Window → resizable window with full surface functionality
- Send to Monitor: Right-click → Send to Monitor 2 → window appears maximized on monitor 2
- Multi-Monitor Drag: Drag popped-out window from monitor 1 to monitor 3 → works natively
- F11 Fullscreen: Press F11 in popped-out window → goes fullscreen; F11 again → back to windowed
- Dock Back: Close popped-out window → surface returns to main tab bar at same position
- Session Persist: Pop out Surface Beta to monitor 2 → close app → reopen → Beta auto-pops to monitor 2
- Monitor Hot-Plug: Disconnect monitor 2 while surface is there → window moves to primary with toast
- YAML Failover: Kill MySQL server → reopen app → surface loads from YAML cache with banner warning
- Upgrade Path: Delete dbservers/ settings → old database/ keys auto-migrate on startup
- Display Prefs: All monitors + capture devices show with correct name/resolution/GPU
- Capture Devices: Webcam and capture card enumerated; virtual cameras flagged with ✓
13. Risk Mitigation
| Risk | Impact | Mitigation |
| MySQL server down at startup | Surface can’t load media library | YAML failover cache provides read-only operation with warning banner |
| Monitor disconnected while surface popped | Window becomes invisible | QScreen::destroyed signal → auto-move to primary |
| Multiple surfaces on same MySQL server | Schema name collision | UUID-suffixed schema names + uniqueness check in wizard |
| Old settings migration | Existing users lose DB config | Auto-migrate database/* keys to dbservers/0/* on first run |
| Pop-out window audio routing | Audio might not work in popped window | Audio callback chain is surface-independent — modules are still in the RT chain regardless of window state |
| DXGI enumeration on non-Windows | Compile error on Linux/macOS | #ifdef Q_OS_WIN guard; stub returns empty list on other platforms |