Free now2sdk Core is completely free — no license key required   See what's included →
SDK Modules Docs Release Notes Pricing GitHub
Get the SDK →

LSwitcher Reference Manual & Vision Mixer Guide

The LSwitcher class is the central production switcher and vision mixer engine of the now2sdk framework, derived from the LObject base class. It operates as a real-time vision mixer, receiving two source inputs (Preview and Program buses) through downstream SwitcherSink objects, transitioning them using standard broadcast effects, overlaying transparent Character Generator (CG) layers, and outputting the synchronized program stream.

Since LSwitcher inherits from LObject, its mixed video/audio output can be routed directly to any downstream target (e.g. LPreview preview windows, LRecorder capture engines, or LStream network publishers).


1. Vision Mixer & Transition Architecture

graph TD
    subgraph A/B Input Buses
        SourceA[Preview/Program Source A - LObject] -->|pushVideoFrame / pushAudioFrame| SinkA[SwitcherSink A]
        SourceB[Preview/Program Source B - LObject] -->|pushVideoFrame / pushAudioFrame| SinkB[SwitcherSink B]
    end
    
    subgraph Mixing Thread Loop
        SinkA -->|getLatestFrame / popAudioSamplesForPts| MixEngine[switcherLoop Thread]
        SinkB -->|getLatestFrame / popAudioSamplesForPts| MixEngine
        
        MixEngine -->|Apply scale_mode & scaling_quality| Scaler[SwsContext Scale & Fit]
        Scaler -->|Normalized YUV420p Frames| Blender[Transitions Blending Processor]
        
        subgraph Blending Processor (Transition Type)
            Blender -->|fade| FadeProc[Cross-opacity fade]
            Blender -->|wipe_lr / wipe_rl / wipe_tb / wipe_bt / diagonal / box / circle| WipeProc[Geometrical pixel-wipe split]
            Blender -->|slide_lr / slide_rl / slide_tb / slide_bt| SlideProc[A/B coordinate offset slide]
            Blender -->|squeeze_lr / squeeze_rl / squeeze_tb / squeeze_bt| SqueezeProc[Anamorphic compress]
            Blender -->|dip| DipProc[Interpolation to Custom HEX color]
        end
    end
    
    subgraph Downstream Overlays
        Stinger[LStinger Video Cache] -->|stingerActive| StingerOverlay[Mathematical Alpha Blend]
        CG[LCharacter Graphic Templates] -->|YUVA Layer| CGOverlay[YUVA Blend Processor]
        
        FadeProc --> StingerOverlay
        WipeProc --> StingerOverlay
        SlideProc --> StingerOverlay
        SqueezeProc --> StingerOverlay
        DipProc --> StingerOverlay
        
        StingerOverlay --> CGOverlay
    end
    
    subgraph Playout
        CGOverlay -->|Mixed YUV420p AVFrame*| OutV[distributeVideoFrame]
        MixEngine -->|Mixed S16 Audio PCM| OutA[distributeAudioFrame]
    end

Key Technical Details:


2. API Reference & Public Methods

📌 Bus Routing & Controls

void setPreview(LObject* obj);

Binds the input object to the Preview bus.

void setProgram(LObject* obj);

Binds the input object to the Program bus.

void setCutTransition();

Performs an instantaneous cut switch. To prevent T-Bar slider jump glitches, this logically swaps the underlying A and B layer pointers without resetting transition progress.


📌 Transition Configurations

void setTransition(float progress);

Manually positions the transition blending state (where 0.0f shows Program source exclusively and 1.0f shows Preview).

void setAutoTransition(double durationMs);

Asynchronously animates the transition between Program and Preview over a defined duration. When finished, it swaps the layer pointers and resets the progress values.

void setTransitionType(const std::string& type, const std::string& arg = "");

Sets the active transition type. Transition options are detailed in Section 3.


📌 Stinger & LStinger Class Integration

A stinger is a short, transparent graphic animation (usually a PNG image sequence or transparent ProRes 4444 video) that covers the entire screen at a specific midpoint, allowing the mixer to switch the background feeds invisibly.

Rather than making developers instantiate and handle sync manually, LSwitcher abstracts this by managing an internal LStinger instance:

sequenceDiagram
    participant App as C++ Application
    participant Sw as LSwitcher
    participant St as LStinger (Internal)
    participant Sink as SwitcherSink (Stinger)

    App->>Sw: setStinger("/path/to/stinger_%05d.png")
    Sw->>St: load("/path/to/stinger_%05d.png", targetW, targetH, targetFps)
    St->>St: Parse PNG sequence & cache YUVA frames in RAM
    App->>Sw: setStingerDuration(1200) // 1.2 seconds
    Sw->>St: setFPS(totalFrames / 1.2) // Recalculate playback speed
    App->>Sw: triggerStinger()
    Sw->>St: playOnce()
    Note over Sw,St: Stinger playback loop starts distributing frames...
    St->>Sink: pushVideoFrame(YUVA Frame)
    Note over Sw: switcherLoop detects stingerActive...
    Sw->>Sw: Check if stingerElapsed >= cutPoint (e.g. 600ms)
    Sw->>Sw: Swaps Preview & Program buses invisibly
    Sw->>Sw: Alpha-blends YUVA stinger frame over output YUV420p frame

Stinger Interface Methods:


📌 Character Generator (CG) Layers


📌 Audio Mixer Controls


📌 Switcher Metrics

struct SwitcherStats {
    double fps;             // Live output frame rate
    std::string onAir;      // Current state: "SubA", "SubB", or "Transition"
    float progress;         // Current T-bar progress percentage (0.0 to 100.0)
    int64_t frameCount;     // Total elapsed video frames mixed
    std::string timecode;   // Formatted HH:MM:SS:FF timecode string
    float audioLevelA;      // Preview source volume peak (0.0 to 1.0)
    float audioLevelB;      // Program source volume peak (0.0 to 1.0)
    float audioLevelOut;    // Mixed program output volume peak (0.0 to 1.0)
    int audioBufferMs;      // Maximum latency buffer queue size in ms
    double avSyncMs;        // Output A/V synchronization drift in ms
};

void getStats(SwitcherStats& stats);

Chain Pull-Rendering & Dynamic Fallback Lifecycle

now2sdk v0.0.2 introduces a high-performance Chain Pull-Rendering architecture that aligns the render cycles of composition layers to avoid phase offsets, latency build-up, and frame-drops.

virtual AVFrame* renderVideoFrame() override;

Called on-demand by downstream consumers (such as LMixer or video outputs). It triggers a single synchronized blending/rendering cycle and returns the final composite AVFrame pointer instantly.

void addPullConsumer();
void removePullConsumer();

Tracks the active count of pull-based downstream objects.


3. Transitions List & Algorithms

The following table lists all transition types supported via setTransitionType(const std::string& type, const std::string& arg):

Transition Type (type) Argument (arg) Blending Algorithm / Visual Effect
"cut" Instantaneous transition. Swaps Preview/Program layers.
"fade" Linear cross-fade. Computes output pixel value as:
PixelOut = PixelA * (1.0 - Progress) + PixelB * Progress
"wipe_lr" Horizontal Wipe (Left to Right). Displays Source B on the left and Source A on the right split by X = TargetWidth * Progress.
"wipe_rl" Horizontal Wipe (Right to Left). Splits screen at X = TargetWidth * (1.0 - Progress).
"wipe_tb" Vertical Wipe (Top to Bottom). Splits screen at Y = TargetHeight * Progress.
"wipe_bt" Vertical Wipe (Bottom to Top). Splits screen at Y = TargetHeight * (1.0 - Progress).
"wipe_diagonal" Diagonal Wipe. Split boundary progresses diagonally from bottom-left to top-right using X + Y < (TargetWidth + TargetHeight) * Progress.
"wipe_box" Expanding rectangular box wipe centered on screen.
"wipe_circle" Expanding circular wipe centered on screen.
"slide_lr" Slide transition (Left to Right). Source B slides in from the left over the stationary Source A.
"slide_rl" Slide transition (Right to Left). Source B slides in from the right.
"slide_tb" Slide transition (Top to Bottom). Source B slides down from the top.
"slide_bt" Slide transition (Bottom to Top). Source B slides up from the bottom.
"squeeze_lr" Horizontal Squeeze. Source B is compressed from 0 to full width on the left while Source A is squeezed out to the right.
"squeeze_rl" Horizontal Squeeze (Right to Left). Source B is squeezed from the right.
"squeeze_tb" Vertical Squeeze. Squeezes Source B from the top down.
"squeeze_bt" Vertical Squeeze (Bottom to Top). Squeezes Source B from the bottom up.
"dip" Hex Color (e.g. "#000000") Dip to color transition. Fades Source A completely down to the specified hex color, then fades up from the color to Source B.
"stinger" Runs internal LStinger sequence player over A/B and triggers a cut switch at the midpoint.

4. Properties & Configuration Options

All switcher options listed below are configured at runtime via the setProps(const std::string& key, const std::string& value) interface:

Key Value Type Default Value Description
timecode bool "false" Toggles timecode calculation and metadata insertion on output frames.
timecodeSource int "1" Timecode calculation source:
"0": System Clock (Time of Day - TOD).
"1": Frame Counter (Internal monotonic offset).
scaling_quality int "0" Resolution scaling algorithms:
"0" / Others: Bicubic (Default)
"1": Point (Nearest Neighbor)
"2": Bilinear
"3": Bicubic
"4": Lanczos
scale_mode int "0" Resolution fit behaviors:
"0": Stretch to fit target width/height.
"1": Fit (Letterbox with black borders).
"2": Fill (Crop borders to preserve aspect ratio).
audioGain int "100" Master output audio volume scaling gain percentage.

5. Vision Switcher Panel Integration Example

This example adapts the Switcher Example project to demonstrate how to configure LSwitcher, route media/capture inputs, select transition patterns, perform auto-fades, configure and load stingers, and display output statistics in real time.

#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "LFile.h"
#include "LLive.h"
#include "LSwitcher.h"
#include <QTimer>
#include <QFileDialog>
#include <QMessageBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      ui(new Ui::MainWindow),
      m_filePlayer(new LFile()),
      m_cameraInput(new LLive()),
      m_switcher(new LSwitcher()),
      m_uiTimer(new QTimer(this)),
      m_activeProg(0),
      m_activePrev(1)
{
    ui->setupUi(this);

    // 1. Setup global formats (1080p50 Broadcast standard)
    videoFormatProps format;
    format.setVideoFormat = vF::HD1080_50p;
    m_switcher->setVideoFormat(format);
    
    // Apply normalization properties
    m_switcher->setProps("scale_mode", "1");        // Letterbox fit
    m_switcher->setProps("scaling_quality", "3");   // Bicubic quality
    m_switcher->setProps("timecode", "true");
    m_switcher->setProps("timecodeSource", "1");    // Frame counter timecode

    // 2. Setup inputs
    m_filePlayer->fileNameSet("/media/clips/background_loop.mp4");
    m_filePlayer->setProps("loop", "true");
    m_filePlayer->play();

    m_cameraInput->setProps("device_type", "decklink");
    m_cameraInput->DeviceSet(0); // Select first decklink card
    m_cameraInput->Start();

    // 3. Connect inputs to Switcher buses (0 = File, 1 = Camera)
    m_switcher->setProgram(m_filePlayer);
    m_switcher->setPreview(m_cameraInput);

    // 4. Default transition configuration
    m_switcher->setTransitionType("fade");
    m_switcher->setProgAudioGain(100);
    m_switcher->setPrevAudioGain(0); // Mute preview audio channel

    // 5. Connect UI Triggers
    connect(ui->cutBtn, &QPushButton::clicked, this, &MainWindow::onCutClicked);
    connect(ui->autoBtn, &QPushButton::clicked, this, &MainWindow::onAutoClicked);
    connect(ui->stingerSelectBtn, &QPushButton::clicked, this, &MainWindow::onSelectStingerClicked);
    connect(ui->stingerBtn, &QPushButton::clicked, this, &MainWindow::onStingerClicked);
    connect(ui->tbarSlider, &QSlider::valueChanged, this, &MainWindow::onTBarSliderMoved);
    connect(ui->transTypeCombo, &QComboBox::currentTextChanged, this, &MainWindow::onTransTypeChanged);

    // 6. Connect dynamic update timers
    connect(m_uiTimer, &QTimer::timeout, this, &MainWindow::onUIUpdate);
    m_uiTimer->start(40); // 25 fps UI refreshes
}

MainWindow::~MainWindow() {
    m_uiTimer->stop();
    m_filePlayer->stop();
    m_cameraInput->Stop();
    delete m_uiTimer;
    delete m_filePlayer;
    delete m_cameraInput;
    delete m_switcher;
    delete ui;
}

// ─── Instant Cut Switch ───
void MainWindow::onCutClicked() {
    m_switcher->setCutTransition();
    std::swap(m_activeProg, m_activePrev);
    
    // Re-bind source routing to prevent lockups
    m_switcher->setProgram(getSourceObject(m_activeProg));
    m_switcher->setPreview(getSourceObject(m_activePrev));
    m_switcher->setTransition(0.0f);
}

// ─── Timed Crossfade transition ───
void MainWindow::onAutoClicked() {
    m_switcher->setAutoTransition(1000.0); // 1-second auto cross-fade
    std::swap(m_activeProg, m_activePrev);
}

// ─── Select Stinger Assets Sequence ───
void MainWindow::onSelectStingerClicked() {
    QString dirPath = QFileDialog::getExistingDirectory(
        this, "Select Stinger PNG Sequence Directory", "/home/user");
    if (!dirPath.isEmpty()) {
        // Format naming: frame_%05d.png
        QString formatPattern = dirPath + "/logo_wipe_%05d.png";
        m_stingerPath = formatPattern.toStdString();
        
        // Load sequence into the switcher's internal LStinger cache
        m_switcher->setStinger(m_stingerPath);
        m_switcher->setStingerDuration(1200); // Set speed to cover 1.2s
        
        ui->stingerPathLabel->setText(QFileInfo(dirPath).fileName());
    }
}

// ─── Transparent Stinger Wipe Trigger ───
void MainWindow::onStingerClicked() {
    if (m_stingerPath.empty()) {
        QMessageBox::warning(this, "Stinger", "Select a stinger PNG sequence folder first.");
        return;
    }
    
    // Cue stinger mode and trigger internal LStinger playback loop
    m_switcher->setTransitionType("stinger");
    m_switcher->triggerStinger();
    
    std::swap(m_activeProg, m_activePrev);
}

// ─── Manual T-Bar mixing ───
void MainWindow::onTBarSliderMoved(int value) {
    float progress = static_cast<float>(value) / 100.0f;
    m_switcher->setTransition(progress);

    // If fully transition to other end, cut and switch buses
    if (progress >= 1.0f) {
        m_switcher->setCutTransition();
        m_switcher->setTransition(0.0f);
        std::swap(m_activeProg, m_activePrev);
        
        m_switcher->setProgram(getSourceObject(m_activeProg));
        m_switcher->setPreview(getSourceObject(m_activePrev));
        
        // Block signals to avoid slider infinite loops
        ui->tbarSlider->blockSignals(true);
        ui->tbarSlider->setValue(0);
        ui->tbarSlider->blockSignals(false);
    }
}

// ─── Transition type selection ───
void MainWindow::onTransTypeChanged(const QString& typeName) {
    std::string type = typeName.toLower().toStdString();
    if (type == "dip to black") {
        m_switcher->setTransitionType("dip", "#000000");
    } else if (type == "dip to white") {
        m_switcher->setTransitionType("dip", "#ffffff");
    } else {
        m_switcher->setTransitionType(type);
    }
}

// Helper routing
LObject* MainWindow::getSourceObject(int id) {
    return (id == 0) ? m_filePlayer : m_cameraInput;
}

// ─── Refresh stats panel labels ───
void MainWindow::onUIUpdate() {
    LSwitcher::SwitcherStats stats;
    m_switcher->getStats(stats);

    ui->fpsLabel->setText(QString("%1 FPS").arg(stats.fps, 0, 'f', 2));
    ui->timecodeLabel->setText(QString::fromStdString(stats.timecode));
    ui->onAirLabel->setText(QString::fromStdString(stats.onAir));

    // Update VU Audio levels
    ui->pgmVuBar->setValue(static_cast<int>(stats.audioLevelOut * 100));
}