VLiva Documentation
OBS Shared Frame Plugin
Code-driven documentation of VLiva shared memory capture and OBS source integration, focused on behavior and design without full source dump.
OBS plugin demo
Runtime example for shared-frame ingestion inside the VLiva OBS source.
Official OBS resource: https://obsproject.com/forum/resources/vliva-capture.2434/
This page documents architecture and code behavior at a high level without disclosing full implementation.
Direct URL: https://vliva.tamkungz.me/documentation/obs-plugin
-
include/vliva/capture/shared_frame_protocol.hpp -
include/vliva/capture/shared_frame_publisher.hpp -
include/vliva/capture/shared_frame_consumer.hpp -
src/capture/shared_frame_publisher.cpp -
src/capture/shared_frame_consumer.cpp -
obs-plugin/vliva_obs_source.cpp -
obs-plugin/CMakeLists.txt -
obs-plugin/build.sh
In this page
Section 1
Shared Memory Header Contract
The transport begins with a strict header contract used by both publisher and consumer.
- magic/version/format are validated by consumer before accepting memory mapping.
- activeIndex selects which of two frame slots is currently publish-ready.
- ready, frameId, width, and height indicate availability and freshness of frame payload.
c
struct SharedFrameHeader {
uint32_t magic;
uint32_t version;
uint32_t maxWidth;
uint32_t maxHeight;
uint32_t stride;
uint32_t format;
uint32_t activeIndex;
uint32_t ready;
uint64_t frameId;
uint64_t timestampNs;
uint32_t width;
uint32_t height;
};
Section 2
Session Name + Region Size Helpers
Protocol helpers keep publisher/consumer mapping logic deterministic.
- Shared region size reserves header + 2 frame buffers (double buffering).
- Session name is normalized with leading slash for POSIX shm_open compatibility.
- Empty session name resolves to default /vliva_obs_main endpoint.
cpp
inline size_t calculateSharedRegionSize(uint32_t stride, uint32_t maxHeight)
{
const size_t singleFrameBytes = static_cast<size_t>(stride) * static_cast<size_t>(maxHeight);
return sizeof(SharedFrameHeader) + (singleFrameBytes * 2u);
}
inline std::string normalizeSharedMemoryName(const std::string& rawName)
{
if (rawName.empty()) {
return std::string(kDefaultSharedSessionName);
}
if (!rawName.empty() && rawName.front() == '/') {
return rawName;
}
return std::string("/") + rawName;
}
Section 3
Publisher Open + Header Initialization
Publisher creates fresh shared memory mapping and initializes protocol metadata.
- open() unlinks stale mapping first to prevent crash leftovers.
- shm_open + ftruncate + mmap allocate the shared region for header and payload.
- initializeHeader() writes magic/version/format and resets runtime fields.
cpp
bool SharedFramePublisher::open(const std::string& sessionName, uint32_t maxWidth, uint32_t maxHeight)
{
close();
m_sessionName = normalizeSharedMemoryName(sessionName);
m_stride = maxWidth * 4u;
::shm_unlink(m_sessionName.c_str());
m_fd = ::shm_open(m_sessionName.c_str(), O_CREAT | O_RDWR, 0666);
::ftruncate(m_fd, static_cast<off_t>(regionSize));
void* mapped = ::mmap(nullptr, regionSize, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
m_header = static_cast<SharedFrameHeader*>(mapped);
initializeHeader();
}
Section 4
Publisher Frame Commit Path
publishRgbaBottomLeft() converts and commits each frame with atomic ordering.
- Frame writes target the non-active slot: writeIndex = activeIndex ^ 1.
- Rows are vertically flipped because glReadPixels origin is bottom-left.
- Frame metadata is written before activeIndex/ready are released to readers.
cpp
const uint32_t currentIndex = __atomic_load_n(&m_header->activeIndex, __ATOMIC_RELAXED);
const uint32_t writeIndex = (currentIndex ^ 1u) & 1u;
for (uint32_t row = 0; row < height; ++row) {
const uint32_t srcRow = (height - 1u) - row; // GL bottom-left -> top-left
std::memcpy(dstLine, srcLine, srcStride);
}
__atomic_store_n(&m_header->width, width, __ATOMIC_RELAXED);
__atomic_store_n(&m_header->height, height, __ATOMIC_RELAXED);
__atomic_store_n(&m_header->timestampNs, timestampNs, __ATOMIC_RELAXED);
__atomic_thread_fence(__ATOMIC_RELEASE);
__atomic_store_n(&m_header->frameId, ++m_frameId, __ATOMIC_RELAXED);
__atomic_store_n(&m_header->activeIndex, writeIndex, __ATOMIC_RELEASE);
__atomic_store_n(&m_header->ready, 1u, __ATOMIC_RELEASE);
Section 5
Consumer Stable Read Strategy
readLatest() uses a copy-and-verify loop to avoid torn frame reads.
- Consumer exits early when ready == 0 or invalid dimensions are observed.
- Frame data is copied, then activeIndex/frameId are re-read to verify consistency.
- Two attempts are allowed before reporting no stable frame.
cpp
const uint32_t ready = __atomic_load_n(&m_header->ready, __ATOMIC_ACQUIRE);
if (ready == 0) {
return false;
}
for (int attempt = 0; attempt < 2; ++attempt) {
const uint32_t activeA = __atomic_load_n(&m_header->activeIndex, __ATOMIC_ACQUIRE) & 1u;
const uint64_t frameA = __atomic_load_n(&m_header->frameId, __ATOMIC_ACQUIRE);
copyFrameRows(activeA);
const uint32_t activeB = __atomic_load_n(&m_header->activeIndex, __ATOMIC_ACQUIRE) & 1u;
const uint64_t frameB = __atomic_load_n(&m_header->frameId, __ATOMIC_ACQUIRE);
if (activeA == activeB && frameA == frameB) {
return true;
}
}
return false;
Section 6
OBS Source Registration
obs_module_load() wires lifecycle callbacks for vliva_capture_source.
- Source id is vliva_capture_source and output is video input.
- Callback table connects create/update/tick/render/size handlers.
- This makes VLiva Capture appear as selectable source type in OBS.
cpp
sourceInfo.id = "vliva_capture_source";
sourceInfo.type = OBS_SOURCE_TYPE_INPUT;
sourceInfo.output_flags = OBS_SOURCE_VIDEO;
sourceInfo.create = vlivaObsCreate;
sourceInfo.update = vlivaObsUpdate;
sourceInfo.video_tick = vlivaObsTick;
sourceInfo.video_render = vlivaObsRender;
obs_register_source(&sourceInfo);
Section 7
OBS Tick Loop + Reconnect Heuristic
The per-frame OBS update path reads latest frame and self-recovers from stale mappings.
- When readLatest fails for too long, reconnect is triggered every ~1 second.
- Unchanged frameId is treated as potential stale consumer mapping.
- Successful read ensures texture size then uploads RGBA bytes via gs_texture_set_image.
cpp
if (!ctx->consumer.readLatest(ctx->frameBuffer, width, height, frameId)) {
if (ctx->reconnectTimer >= 1.0f) {
vlivaObsReconnect(ctx);
}
return;
}
if (frameId == ctx->lastFrameId) {
if (ctx->reconnectTimer >= 1.0f) {
vlivaObsReconnect(ctx);
}
return;
}
vlivaObsEnsureTexture(ctx, width, height);
gs_texture_set_image(ctx->texture, ctx->frameBuffer.data(), width * 4u, false);