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.

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

  1. Shared Memory Header Contract
  2. Session Name + Region Size Helpers
  3. Publisher Open + Header Initialization
  4. Publisher Frame Commit Path
  5. Consumer Stable Read Strategy
  6. OBS Source Registration
  7. OBS Tick Loop + Reconnect Heuristic
  8. Build + Install .so Module

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);

Section 8

Build + Install .so Module

Deployment flow from build.sh/CMake for producing and copying vliva_obs_source.so.

  • build.sh configures CMake with OBS include/lib and VLiva capture include path.
  • Resulting module is installed to OBS plugin path (default ~/.config/obs-studio/plugins/...).
  • Script verifies unresolved SharedFrameConsumer symbol is not left in deployed binary.

text

# helper script defaults
DEFAULT_DEST_PLUGIN_SO="$HOME/.config/obs-studio/plugins/vliva_obs_source/bin/64bit/vliva_obs_source.so"

cmake -S "${ROOT_DIR}" -B "${BUILD_DIR}"   -DVLIVA_REQUIRE_OBS=ON   -DVLIVA_REQUIRE_CAPTURE=ON   -DOBS_INCLUDE_DIR="${OBS_INCLUDE_DIR}"   -DOBS_LIBRARY="${OBS_LIBRARY}"   -DVLIVA_CAPTURE_INCLUDE_DIR="${VLIVA_CAPTURE_INCLUDE_DIR}"

cmake --build "${BUILD_DIR}" -j "${JOBS}"
install -Dm755 "${BUILD_DIR}/vliva_obs_source.so" "${DEST_PLUGIN_SO}"