LReplay Reference Manual & Instant Replay Guide
The LReplay class is the high-performance multi-channel instant replay and slow-motion playout engine of the now2sdk framework. Because it acts as both a downstream stream listener (LSink) and an active playout stream source (LObject), it supports simultaneous raw A/V ingestion and slow-motion playout rendering.
LReplay writes incoming video frames to custom binary container formats using high-bitrate MJPEG (Intra-only) compression, enabling instant $O(1)$ seeks, jog-stepping, reverse plays, and fast exports without transcoding.
1. Dual Ingest-Playout Architecture
graph TD
subgraph Raw Ingestion Phase (LSink)
A[Live Video Source] -->|pushVideoFrame| B(MJPEG Encoding)
B -->|Licensing check| C{Licensed?}
C -->|No| D[Apply Watermark & 120s limit]
C -->|Yes| E[Write to _v.bin]
E --> F[Register FrameIndex offset in RAM]
G[Live Audio Source] -->|pushAudioFrame| H[Write PCM to _a.bin]
H --> I[Register AudioIndex offset in RAM]
end
subgraph Playback & Navigation Phase (LObject)
J[playbackLoop Thread] -->|Read FrameIndex| K{Mode?}
K -->|Live Mode| L[Read latest frame index]
K -->|Trim Mode| M[Navigate using Pos / Jog Steps]
L --> N[Seek offset on disk & read MJPEG chunk]
M --> N
N --> O(MJPEG Decoding)
O --> P[Scale / Convert to YUVA420P]
P --> Q[Call distributeVideoFrame to Sinks]
J -->|Read AudioIndex| R{Speed == 1.0?}
R -->|Yes| S[Fseek & read PCM chunk]
S --> T[Call distributeAudioFrame to Sinks]
end
Ingestion & Indexing Details:
- MJPEG Intra-Frame Compression: Video frames are compressed individually using JPEG. Since there are no inter-frame dependencies (IPB frames), the engine can jump to, decode, and render any frame index in milliseconds without scanning preceding frames.
- Dual Index Registries in RAM:
FrameIndex: Stores the precise byte offset, packet size, timestamp, and timecode string (HH:MM:SS:FF) for every recorded video frame.AudioIndex: Stores the byte offset, packet size, and audio timestamp for uncompressed PCM audio chunks.
- Master Clock PTS Duplication: If the target broadcast FPS is higher than the ingest source rate (e.g., target 50 FPS, source 25 FPS),
LReplayautomatically performs zero-overhead duplicate frame registration in theFrameIndexregistry to preserve alignment. - Licensing Enforcement: If no valid replay license is loaded, the engine automatically embeds the
"now2sdk"watermark into the video bin stream and ceases recording after120seconds of activity. - Zero-Transcoding Fast Export: Combines video MJPEG and audio PCM indexes of the selected trim boundaries, packaging them directly into a standard MOV container. This is extremely fast because it bypasses decompression and encoding passes.
2. API Reference & Public Methods
📌 Licensing Controls
bool setLicense(const std::string& licenseKey);
Registers a license key for the replay instance. Returns true if valid.
bool isLicensed() const;
Checks if the replay module is licensed on the host system.
📌 Recording & Ingest Setup
void setRecordPath(const std::string& directory, const std::string& prefix = "replay");
Configures the directory and name prefix for recording files (e.g. path/replay_v.bin and path/replay_a.bin).
void setExportPath(const std::string& directory, const std::string& prefix = "export");
Configures the directory and file prefix for exported video clips.
void setVideoFormat(const videoFormatProps& props);
Sets target resolution, frame rate, and colorspace specifications.
void setAudioFormat(const audioFormatProps& props);
Sets target audio format sample rates and channel layout.
bool startRecording();
Creates .bin files, initializes encoders, starts tracking timestamp offsets, and begins writing incoming A/V streams. Returns true if successful.
void stopRecording();
Stops writing streams, flushes encoder caches, and closes write file descriptors.
void clearRecording();
Deletes physical .bin temp files from the disk and resets internal indexing vectors.
📌 Playback & Navigation Controls
void play();
Starts playout timeline rendering at the current speed factor.
void pause();
Pauses playout on the current frame.
void setModeLive();
Switches playout to live monitoring mode, rendering frames as soon as they are written to disk.
void setModeTrim();
Switches playout to trim/navigational mode, allowing manual timeline seeks.
void setSpeed(double speed);
Configures playback speed. Supports slow-motion and reverse seeks (e.g. 1.0 for normal, 0.5 for half-speed, -1.0 for reverse).
void posSet(int frameIndex);
Seeks directly to a specific frame number.
void posSetMs(double ms);
Seeks to a position in milliseconds relative to the start of the recording.
void posSetTC(const std::string& tc);
Seeks to a position using a target SMPTE timecode string (HH:MM:SS:FF). Search is performed backwards from the most recent frame.
void stepForward(int frames = 1);
void stepBackward(int frames = 1);
Steps the playback head forward or backward by a specific frame count.
📌 Playout Status Queries
int getTotalFrames();
Returns the total number of video frames recorded in the active session.
double getDurationMs();
Returns the total recorded timeline duration in milliseconds.
int getCurrentFrame();
Returns the index of the frame currently being rendered.
double getPosMs();
Returns the current playhead position in milliseconds.
std::string getPosTC();
Returns the SMPTE timecode string (HH:MM:SS:FF) of the current frame.
void statusGet(int& status);
Returns: 1 if playing, 2 if recording (but paused/trim playout), 0 if idle.
📌 Zero-Transcoding Export
bool exportRange(int inFrame, int outFrame, const std::string& outFilePath = "");
Muxes recorded MJPEG/PCM frames between inFrame and outFrame directly into a QuickTime MOV file without re-encoding. If outFilePath is empty, it generates a file automatically using the export prefix and timestamp.
bool exportRangeMs(double inMs, double outMs, const std::string& outFilePath = "");
Muxes recorded frames using millisecond boundaries.
3. Properties & Configuration Options
All 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 |
|---|---|---|---|
quality |
int |
"86" |
MJPEG compression quality percentage (1 to 100). High quality requires more disk storage but reduces compression artifacts. |
timecode |
bool |
"false" |
Toggles timecode track creation/rendering. |
timecodeSource |
int |
"0" |
Selects timecode timing clock source:0: Current local system clock timecode.1: Inherits metadata timecode from the input source.2: Offsets timecode using the value set in timecodeStart. |
timecodeStart |
string |
"01:00:00:00" |
Starting timecode offset value used when timecodeSource = 2 (format: HH:MM:SS:FF). |
name |
string |
"" |
Configures a unique suffix string appended to .bin file names on disk. Useful for preventing file write conflicts when running multiple LReplay instances. |
audio.preview |
bool |
"true" |
Toggles audio playout during playback. Playout is only enabled during 1.0x forward speed or Live mode. |
4. Multi-Channel Ingest & Playout Replay Example
This Qt C++ window dashboard implementation (adapted from the Replay Example) shows how to configure LReplay alongside LLive and LPreview widgets. It demonstrates connecting live video captures, toggling instant recording loops, managing playback speeds, jog-stepping, and triggering fast range exports.
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include "LLive.h"
#include "LPreview.h"
#include "LReplay.h"
#include <QDir>
#include <QMessageBox>
#include <QTimer>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
ui(new Ui::MainWindow),
m_live(new LLive()),
m_replay(new LReplay()),
m_livePreview(new LPreview()),
m_replayPreview(new LPreview())
{
ui->setupUi(this);
// 1. Setup Live Camera Stream
m_live->setProps("gpu", "true");
m_live->setProps("timecode", "true");
// Connect Live source monitor
m_livePreview->previewEnable(ui->livePreviewWidget, true, true);
m_livePreview->previewObject(m_live);
// 2. Setup Instant Replay Engine
// Configure directories to current working path
m_replay->setRecordPath(QDir::currentPath().toStdString());
m_replay->setExportPath(QDir::currentPath().toStdString(), "replay_clip");
// Set recording properties
m_replay->setProps("quality", "85");
m_replay->setProps("timecode", "true");
m_replay->setProps("timecodeSource", "0"); // System clock source
m_replay->setProps("name", "REPLAY_CH1");
// Connect replay playout monitor
m_replayPreview->previewEnable(ui->replayPreviewWidget, true, true);
m_replayPreview->previewObject(m_replay);
// IMPORTANT: Attach replay engine as a sink to the live stream!
m_live->addSink(m_replay);
// 3. Connect navigation button events
connect(ui->liveBtn, &QPushButton::clicked, this, &MainWindow::onLiveClicked);
connect(ui->recordBtn, &QPushButton::clicked, this, &MainWindow::onRecordClicked);
connect(ui->playPauseBtn, &QPushButton::clicked, this, &MainWindow::onPlayPauseClicked);
connect(ui->forwardBtn, &QPushButton::clicked, this, &MainWindow::onForwardClicked);
connect(ui->backwardBtn, &QPushButton::clicked, this, &MainWindow::onBackwardClicked);
connect(ui->exportBtn, &QPushButton::clicked, this, &MainWindow::onExportClicked);
// Connect speed sliders (Slow-Mo controls)
connect(ui->speedSlider, &QSlider::valueChanged, this, &MainWindow::onSpeedChanged);
// Start UI update timer at 100ms
m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, &MainWindow::onUpdateUI);
m_timer->start(100);
}
MainWindow::~MainWindow() {
m_live->Stop();
m_replay->stopRecording();
delete m_timer;
delete m_livePreview;
delete m_replayPreview;
delete m_replay;
delete m_live;
delete ui;
}
// ─── Step 1: Ingest Live Source ───
void MainWindow::onLiveClicked() {
int status = 0;
m_live->statusGet(status);
if (status != 1) {
if (m_live->Start()) {
ui->liveBtn->setText("Stop Live Ingest");
}
} else {
m_live->Stop();
ui->liveBtn->setText("Start Live Ingest");
}
}
// ─── Step 2: Toggle Ingest Recording ───
void MainWindow::onRecordClicked() {
int status = 0;
m_replay->statusGet(status);
if (status != 2) { // Not recording/trim mode
if (m_replay->startRecording()) {
ui->recordBtn->setText("🛑 STOP RECORD");
ui->recordBtn->setStyleSheet("background-color: #dc2626; color: white;");
}
} else {
m_replay->stopRecording();
ui->recordBtn->setText("⚪ START RECORD");
ui->recordBtn->setStyleSheet("");
}
}
// ─── Step 3: Toggle Playout Modes (Live vs Trim Seeks) ───
void MainWindow::onPlayPauseClicked() {
int status = 0;
m_replay->statusGet(status);
if (status == 1) { // Playout active, pause it
m_replay->pause();
ui->playPauseBtn->setText("Play Playout");
} else {
// Resume playout in standard speed
m_replay->setSpeed(1.0);
m_replay->play();
ui->playPauseBtn->setText("⏸ PAUSE PLAYOUT");
}
}
void MainWindow::onSpeedChanged(int value) {
// Map slider to playback speeds: e.g. 50 -> 0.5x, 100 -> 1.0x, -100 -> -1.0x (Reverse)
double targetSpeed = (double)value / 100.0;
m_replay->setSpeed(targetSpeed);
m_replay->play();
}
// ─── Step 4: Jog-Step Frame Controls ───
void MainWindow::onForwardClicked() {
m_replay->stepForward(1); // Step forward 1 frame
}
void MainWindow::onBackwardClicked() {
m_replay->stepBackward(1); // Step backward 1 frame
}
// ─── Step 5: Fast Zero-Transcoding Trim Export ───
void MainWindow::onExportClicked() {
double totalDuration = m_replay->getDurationMs();
if (totalDuration < 5000.0) {
QMessageBox::warning(this, "Export", "Capture at least 5 seconds of footage before exporting.");
return;
}
// Export the last 4 seconds of recorded footage using millisecond bounds
double endMs = totalDuration;
double startMs = endMs - 4000.0;
if (startMs < 0.0) startMs = 0.0;
if (m_replay->exportRangeMs(startMs, endMs)) {
QMessageBox::information(this, "Export Success",
QString("Clip exported (last 4s) to QuickTime MOV in folder:\n%1")
.arg(QDir::currentPath()));
} else {
QMessageBox::critical(this, "Export Failed", "Muxer failed to export selected range.");
}
}
// ─── Step 6: Monitor Statistics ───
void MainWindow::onUpdateUI() {
int total = m_replay->getTotalFrames();
int current = m_replay->getCurrentFrame();
double duration = m_replay->getDurationMs();
std::string timecode = m_replay->getPosTC();
ui->infoLabel->setText(QString("Index Size: %1 frames\nPlayout head: %2\nTimeline: %3 ms\nSMPTE TC: %4")
.arg(total)
.arg(current)
.arg(duration, 0, 'f', 2)
.arg(QString::fromStdString(timecode)));
}