Restore Muninn OBS plugin with Odin discovery

This commit is contained in:
Meta Vens 2026-07-05 22:35:50 +01:00
commit e00458ff3b
11 changed files with 5684 additions and 0 deletions

View file

@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.20)
project(mimir_obs_stem_source LANGUAGES CXX)
find_package(libobs REQUIRED CONFIG)
include(CTest)
add_library(mimir_obs_stem_source MODULE
mimir_obs_muninn_source.cpp
mimir_obs_program_texture_source.cpp
mimir_obs_stem_source.cpp)
target_compile_features(mimir_obs_stem_source PRIVATE cxx_std_20)
target_compile_definitions(mimir_obs_stem_source PRIVATE
WIN32_LEAN_AND_MEAN
NOMINMAX
_WIN32_WINNT=0x0A00)
target_link_libraries(mimir_obs_stem_source PRIVATE OBS::libobs d3d11 d3d12 dxgi ws2_32)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../tools/muninn_media_decoder_probe.cpp")
add_executable(muninn_media_decoder_probe
../../tools/muninn_media_decoder_probe.cpp)
target_compile_features(muninn_media_decoder_probe PRIVATE cxx_std_20)
target_include_directories(muninn_media_decoder_probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR})
endif()
add_executable(muninn_rudp_url_tests
tests/muninn_rudp_url_tests.cpp)
target_compile_features(muninn_rudp_url_tests PRIVATE cxx_std_20)
target_include_directories(muninn_rudp_url_tests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR})
add_test(NAME muninn_rudp_url_tests COMMAND muninn_rudp_url_tests)

View file

@ -0,0 +1,14 @@
MimirAudioStem="Mimir Audio Stem"
MimirAudioStem.MapName="Shared memory map"
MimirAudioStem.StemId="Stem id"
MimirProgramTexture="Mimir Program Texture"
MuninnStream="Muninn Stream"
MuninnVideoSource="Muninn Video"
MuninnAudioSource="Muninn Audio"
MuninnStream.Stream="Muninn stream"
MuninnStream.Video="Video device"
MuninnStream.Audio="Audio device"
MuninnStream.VideoBitrateKbps="Video bitrate (kbps)"
MuninnStream.VideoLatencyMs="Video/transport latency (ms)"
MuninnStream.AudioReorderMs="Audio reorder latency (ms)"
MuninnStream.Refresh="Refresh Muninn streams"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,628 @@
#include <obs-module.h>
#include <util/platform.h>
#include <chrono>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include <d3d11_1.h>
#include <d3d12.h>
#include <dxgi1_2.h>
#include <wrl/client.h>
#include <windows.h>
using Microsoft::WRL::ComPtr;
namespace {
struct MimirProgramTextureSource {
obs_source_t *source = nullptr;
std::string texture_name = "Global\\MimirFensalirProgramTexture";
std::string fence_name = "Global\\MimirFensalirProgramFence";
std::string consumer_fence_name = "Global\\MimirObsProgramConsumerFence";
uint32_t ring_count = 1;
uint32_t width = 0;
uint32_t height = 0;
uint64_t fence_value = 0;
uint64_t last_producer_fence_value = 0;
uint64_t next_retry_ns = 0;
uint64_t next_failure_log_ns = 0;
HANDLE bridge_shared_handle = nullptr;
HANDLE consumer_fence_shared_handle = nullptr;
HANDLE fence_event = nullptr;
gs_texture_t *obs_texture = nullptr;
ComPtr<ID3D12Device> d3d12_device;
ComPtr<ID3D12CommandQueue> command_queue;
ComPtr<ID3D12CommandAllocator> command_allocator;
ComPtr<ID3D12GraphicsCommandList> command_list;
ComPtr<ID3D12Fence> fence;
ComPtr<ID3D12Fence> producer_fence;
ComPtr<ID3D12Fence> consumer_fence;
std::vector<ComPtr<ID3D12Resource>> source_textures;
ComPtr<ID3D12Resource> bridge_texture_d3d12;
ComPtr<ID3D11Device> d3d11_device;
ComPtr<ID3D11DeviceContext> d3d11_context;
ComPtr<ID3D11Texture2D> bridge_texture_d3d11;
};
static void log_retry_failure(MimirProgramTextureSource *ctx, const char *message, HRESULT hr = S_OK)
{
const uint64_t now = os_gettime_ns();
if (now < ctx->next_failure_log_ns) {
return;
}
ctx->next_failure_log_ns = now + 2000000000ULL;
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: %s: 0x%08lx", message, static_cast<unsigned long>(hr));
} else {
blog(LOG_WARNING, "Mimir OBS program texture: %s", message);
}
}
static std::wstring utf8_to_wide(const std::string &value)
{
if (value.empty()) {
return {};
}
const int count = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
if (count <= 0) {
return {};
}
std::wstring wide(static_cast<size_t>(count), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, wide.data(), count);
if (!wide.empty() && wide.back() == L'\0') {
wide.pop_back();
}
return wide;
}
static std::string obs_string(obs_data_t *settings, const char *key, const char *fallback)
{
const char *value = obs_data_get_string(settings, key);
return value && *value ? std::string(value) : std::string(fallback);
}
static uint32_t clamp_ring_count(long long value)
{
if (value < 1) {
return 1;
}
if (value > 4) {
return 4;
}
return static_cast<uint32_t>(value);
}
static std::string texture_name_for_slot(const MimirProgramTextureSource *ctx, uint32_t slot)
{
if (ctx->ring_count <= 1) {
return ctx->texture_name;
}
return ctx->texture_name + "." + std::to_string(slot);
}
static void release_bridge(MimirProgramTextureSource *ctx)
{
if (ctx->obs_texture) {
gs_texture_destroy(ctx->obs_texture);
ctx->obs_texture = nullptr;
}
ctx->bridge_texture_d3d12.Reset();
ctx->bridge_texture_d3d11.Reset();
ctx->source_textures.clear();
ctx->fence.Reset();
ctx->producer_fence.Reset();
ctx->consumer_fence.Reset();
ctx->command_list.Reset();
ctx->command_allocator.Reset();
ctx->command_queue.Reset();
ctx->d3d12_device.Reset();
ctx->d3d11_context.Reset();
ctx->d3d11_device.Reset();
ctx->width = 0;
ctx->height = 0;
ctx->fence_value = 0;
ctx->last_producer_fence_value = 0;
if (ctx->bridge_shared_handle) {
CloseHandle(ctx->bridge_shared_handle);
ctx->bridge_shared_handle = nullptr;
}
if (ctx->consumer_fence_shared_handle) {
CloseHandle(ctx->consumer_fence_shared_handle);
ctx->consumer_fence_shared_handle = nullptr;
}
if (ctx->fence_event) {
CloseHandle(ctx->fence_event);
ctx->fence_event = nullptr;
}
}
static bool wait_for_copy(MimirProgramTextureSource *ctx, uint64_t consumed_producer_value)
{
if (ctx->consumer_fence && consumed_producer_value > 0) {
HRESULT consumer_hr = ctx->command_queue->Signal(ctx->consumer_fence.Get(), consumed_producer_value);
if (FAILED(consumer_hr)) {
blog(LOG_WARNING,
"Mimir OBS program texture: consumer fence Signal failed: 0x%08lx",
static_cast<unsigned long>(consumer_hr));
}
}
const uint64_t value = ++ctx->fence_value;
HRESULT hr = ctx->command_queue->Signal(ctx->fence.Get(), value);
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: D3D12 Signal failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
if (ctx->fence->GetCompletedValue() >= value) {
return true;
}
hr = ctx->fence->SetEventOnCompletion(value, ctx->fence_event);
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: SetEventOnCompletion failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
WaitForSingleObject(ctx->fence_event, 100);
return ctx->fence->GetCompletedValue() >= value;
}
static bool create_devices(MimirProgramTextureSource *ctx)
{
HRESULT hr = D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&ctx->d3d12_device));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: D3D12CreateDevice failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
D3D12_COMMAND_QUEUE_DESC queue_desc = {};
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
hr = ctx->d3d12_device->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&ctx->command_queue));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateCommandQueue failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
hr = ctx->d3d12_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&ctx->command_allocator));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateCommandAllocator failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
hr = ctx->d3d12_device->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
ctx->command_allocator.Get(),
nullptr,
IID_PPV_ARGS(&ctx->command_list));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateCommandList failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
ctx->command_list->Close();
hr = ctx->d3d12_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&ctx->fence));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateFence failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
ctx->fence_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!ctx->fence_event) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateEvent failed: %lu", GetLastError());
return false;
}
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
D3D_FEATURE_LEVEL feature_levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
D3D_FEATURE_LEVEL selected = D3D_FEATURE_LEVEL_11_0;
hr = D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
feature_levels,
2,
D3D11_SDK_VERSION,
&ctx->d3d11_device,
&selected,
&ctx->d3d11_context);
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: D3D11CreateDevice failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
return true;
}
static void create_consumer_fence(MimirProgramTextureSource *ctx)
{
if (ctx->consumer_fence_name.empty()) {
return;
}
HRESULT hr = ctx->d3d12_device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&ctx->consumer_fence));
if (FAILED(hr)) {
log_retry_failure(ctx, "CreateFence consumer fence failed", hr);
return;
}
const auto wide_name = utf8_to_wide(ctx->consumer_fence_name);
if (wide_name.empty()) {
log_retry_failure(ctx, "consumer fence name is empty or invalid UTF-8");
ctx->consumer_fence.Reset();
return;
}
hr = ctx->d3d12_device->CreateSharedHandle(
ctx->consumer_fence.Get(),
nullptr,
GENERIC_ALL,
wide_name.c_str(),
&ctx->consumer_fence_shared_handle);
if (FAILED(hr)) {
log_retry_failure(ctx, "CreateSharedHandle consumer fence failed", hr);
ctx->consumer_fence.Reset();
return;
}
blog(LOG_INFO, "Mimir OBS program texture consumer fence published %s", ctx->consumer_fence_name.c_str());
}
static bool open_source_texture(MimirProgramTextureSource *ctx)
{
ctx->source_textures.clear();
ctx->source_textures.reserve(ctx->ring_count);
for (uint32_t slot = 0; slot < ctx->ring_count; ++slot) {
const auto slot_name = texture_name_for_slot(ctx, slot);
const auto wide_name = utf8_to_wide(slot_name);
if (wide_name.empty()) {
log_retry_failure(ctx, "shared texture name is empty or invalid UTF-8");
return false;
}
HANDLE source_handle = nullptr;
HRESULT hr = ctx->d3d12_device->OpenSharedHandleByName(wide_name.c_str(), GENERIC_ALL, &source_handle);
if (FAILED(hr)) {
log_retry_failure(ctx, "OpenSharedHandleByName failed", hr);
return false;
}
ComPtr<ID3D12Resource> source_texture;
hr = ctx->d3d12_device->OpenSharedHandle(source_handle, IID_PPV_ARGS(&source_texture));
CloseHandle(source_handle);
if (FAILED(hr)) {
log_retry_failure(ctx, "OpenSharedHandle failed", hr);
return false;
}
const D3D12_RESOURCE_DESC desc = source_texture->GetDesc();
if (desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D || desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM) {
log_retry_failure(ctx, "expected BGRA8 D3D12 Texture2D");
return false;
}
const uint32_t slot_width = static_cast<uint32_t>(desc.Width);
const uint32_t slot_height = desc.Height;
if (slot_width == 0 || slot_height == 0) {
log_retry_failure(ctx, "shared texture dimensions are empty");
return false;
}
if (slot == 0) {
ctx->width = slot_width;
ctx->height = slot_height;
} else if (slot_width != ctx->width || slot_height != ctx->height) {
log_retry_failure(ctx, "program output ring texture dimensions do not match");
return false;
}
ctx->source_textures.push_back(std::move(source_texture));
}
return ctx->source_textures.size() == ctx->ring_count;
}
static bool open_producer_fence(MimirProgramTextureSource *ctx)
{
const auto wide_name = utf8_to_wide(ctx->fence_name);
if (wide_name.empty()) {
return true;
}
HANDLE fence_handle = nullptr;
HRESULT hr = ctx->d3d12_device->OpenSharedHandleByName(wide_name.c_str(), GENERIC_ALL, &fence_handle);
if (FAILED(hr)) {
log_retry_failure(ctx, "OpenSharedHandleByName producer fence failed", hr);
return true;
}
hr = ctx->d3d12_device->OpenSharedHandle(fence_handle, IID_PPV_ARGS(&ctx->producer_fence));
CloseHandle(fence_handle);
if (FAILED(hr)) {
log_retry_failure(ctx, "OpenSharedHandle producer fence failed", hr);
ctx->producer_fence.Reset();
return true;
}
ctx->last_producer_fence_value = ctx->producer_fence->GetCompletedValue();
if (ctx->last_producer_fence_value == UINT64_MAX) {
log_retry_failure(ctx, "producer fence opened in device-removed state");
ctx->producer_fence.Reset();
return false;
}
blog(LOG_INFO,
"Mimir OBS program texture producer fence opened %s value=%llu",
ctx->fence_name.c_str(),
static_cast<unsigned long long>(ctx->last_producer_fence_value));
return true;
}
static bool read_producer_fence(MimirProgramTextureSource *ctx, uint64_t *completed)
{
*completed = 0;
if (!ctx->producer_fence) {
return true;
}
*completed = ctx->producer_fence->GetCompletedValue();
if (*completed == UINT64_MAX) {
log_retry_failure(ctx, "producer fence reported device removal");
return false;
}
if (*completed > ctx->last_producer_fence_value) {
ctx->last_producer_fence_value = *completed;
}
return true;
}
static bool create_obs_bridge_texture(MimirProgramTextureSource *ctx)
{
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = ctx->width;
desc.Height = ctx->height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
HRESULT hr = ctx->d3d11_device->CreateTexture2D(&desc, nullptr, &ctx->bridge_texture_d3d11);
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: CreateTexture2D bridge failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
ComPtr<IDXGIResource> legacy_resource;
hr = ctx->bridge_texture_d3d11.As(&legacy_resource);
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: bridge texture is not IDXGIResource: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
hr = legacy_resource->GetSharedHandle(&ctx->bridge_shared_handle);
if (FAILED(hr) || !ctx->bridge_shared_handle) {
blog(LOG_WARNING, "Mimir OBS program texture: GetSharedHandle bridge failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
hr = ctx->d3d12_device->OpenSharedHandle(ctx->bridge_shared_handle, IID_PPV_ARGS(&ctx->bridge_texture_d3d12));
if (FAILED(hr)) {
blog(LOG_WARNING, "Mimir OBS program texture: D3D12 open legacy bridge handle failed: 0x%08lx", static_cast<unsigned long>(hr));
return false;
}
ctx->obs_texture = gs_texture_open_shared(static_cast<uint32_t>(reinterpret_cast<uintptr_t>(ctx->bridge_shared_handle)));
if (!ctx->obs_texture) {
blog(LOG_WARNING, "Mimir OBS program texture: OBS failed to open legacy bridge shared texture");
return false;
}
return true;
}
static bool ensure_bridge(MimirProgramTextureSource *ctx)
{
if (ctx->obs_texture) {
return true;
}
const uint64_t now = os_gettime_ns();
if (now < ctx->next_retry_ns) {
return false;
}
ctx->next_retry_ns = now + 1000000000ULL;
release_bridge(ctx);
if (!create_devices(ctx)) {
release_bridge(ctx);
return false;
}
create_consumer_fence(ctx);
if (!open_source_texture(ctx) || !create_obs_bridge_texture(ctx)) {
release_bridge(ctx);
return false;
}
if (!open_producer_fence(ctx)) {
release_bridge(ctx);
return false;
}
blog(LOG_INFO, "Mimir OBS program texture source opened %s (%ux%u ring=%u)",
ctx->texture_name.c_str(),
ctx->width,
ctx->height,
ctx->ring_count);
return true;
}
static bool copy_source_to_bridge(MimirProgramTextureSource *ctx)
{
HRESULT hr = ctx->command_allocator->Reset();
if (FAILED(hr)) {
return false;
}
uint64_t completed_producer_value = 0;
if (!read_producer_fence(ctx, &completed_producer_value)) {
release_bridge(ctx);
return false;
}
const uint32_t source_slot = ctx->source_textures.empty()
? 0
: static_cast<uint32_t>(completed_producer_value % ctx->source_textures.size());
ID3D12Resource *source_texture = ctx->source_textures[source_slot].Get();
hr = ctx->command_list->Reset(ctx->command_allocator.Get(), nullptr);
if (FAILED(hr)) {
return false;
}
D3D12_RESOURCE_BARRIER barriers[2] = {};
barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barriers[0].Transition.pResource = source_texture;
barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON;
barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barriers[1].Transition.pResource = ctx->bridge_texture_d3d12.Get();
barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COMMON;
barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST;
barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
ctx->command_list->ResourceBarrier(2, barriers);
ctx->command_list->CopyResource(ctx->bridge_texture_d3d12.Get(), source_texture);
std::swap(barriers[0].Transition.StateBefore, barriers[0].Transition.StateAfter);
std::swap(barriers[1].Transition.StateBefore, barriers[1].Transition.StateAfter);
ctx->command_list->ResourceBarrier(2, barriers);
hr = ctx->command_list->Close();
if (FAILED(hr)) {
return false;
}
ID3D12CommandList *lists[] = {ctx->command_list.Get()};
ctx->command_queue->ExecuteCommandLists(1, lists);
return wait_for_copy(ctx, completed_producer_value);
}
static const char *mimir_program_texture_get_name(void *)
{
return obs_module_text("MimirProgramTexture");
}
static void mimir_program_texture_update(void *data, obs_data_t *settings)
{
auto *ctx = static_cast<MimirProgramTextureSource *>(data);
ctx->texture_name = obs_string(settings, "texture_name", "Global\\MimirFensalirProgramTexture");
ctx->fence_name = obs_string(settings, "fence_name", "Global\\MimirFensalirProgramFence");
ctx->consumer_fence_name = obs_string(settings, "consumer_fence_name", "Global\\MimirObsProgramConsumerFence");
ctx->ring_count = clamp_ring_count(obs_data_get_int(settings, "ring_count"));
release_bridge(ctx);
}
static void *mimir_program_texture_create(obs_data_t *settings, obs_source_t *source)
{
auto *ctx = new MimirProgramTextureSource();
ctx->source = source;
mimir_program_texture_update(ctx, settings);
return ctx;
}
static void mimir_program_texture_destroy(void *data)
{
auto *ctx = static_cast<MimirProgramTextureSource *>(data);
release_bridge(ctx);
delete ctx;
}
static uint32_t mimir_program_texture_width(void *data)
{
auto *ctx = static_cast<MimirProgramTextureSource *>(data);
ensure_bridge(ctx);
return ctx->width;
}
static uint32_t mimir_program_texture_height(void *data)
{
auto *ctx = static_cast<MimirProgramTextureSource *>(data);
ensure_bridge(ctx);
return ctx->height;
}
static void mimir_program_texture_defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "texture_name", "Global\\MimirFensalirProgramTexture");
obs_data_set_default_string(settings, "fence_name", "Global\\MimirFensalirProgramFence");
obs_data_set_default_string(settings, "consumer_fence_name", "Global\\MimirObsProgramConsumerFence");
obs_data_set_default_int(settings, "ring_count", 1);
}
static obs_properties_t *mimir_program_texture_properties(void *)
{
obs_properties_t *props = obs_properties_create();
obs_properties_add_text(props, "texture_name", "Shared D3D12 texture name", OBS_TEXT_DEFAULT);
obs_properties_add_text(props, "fence_name", "Shared D3D12 fence name", OBS_TEXT_DEFAULT);
obs_properties_add_text(props, "consumer_fence_name", "Consumer D3D12 fence name", OBS_TEXT_DEFAULT);
obs_properties_add_int(props, "ring_count", "Texture ring slots", 1, 4, 1);
return props;
}
static void mimir_program_texture_render(void *data, gs_effect_t *)
{
auto *ctx = static_cast<MimirProgramTextureSource *>(data);
if (!ensure_bridge(ctx)) {
return;
}
if (copy_source_to_bridge(ctx)) {
obs_source_draw(ctx->obs_texture, 0, 0, ctx->width, ctx->height, false);
}
}
static obs_source_info mimir_program_texture_source_info = {
.id = "mimir_program_texture_source",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_VIDEO,
.get_name = mimir_program_texture_get_name,
.create = mimir_program_texture_create,
.destroy = mimir_program_texture_destroy,
.get_width = mimir_program_texture_width,
.get_height = mimir_program_texture_height,
.get_defaults = mimir_program_texture_defaults,
.get_properties = mimir_program_texture_properties,
.update = mimir_program_texture_update,
.video_render = mimir_program_texture_render,
};
} // namespace
void mimir_register_program_texture_source()
{
obs_register_source(&mimir_program_texture_source_info);
}

View file

@ -0,0 +1,43 @@
#pragma once
#include <cstdint>
constexpr uint64_t MIMIR_OBS_STEM_MAGIC = 0x4D545352494D494Dull;
constexpr int MIMIR_OBS_STEM_VERSION = 1;
constexpr int MIMIR_OBS_STEM_MAX_STEMS = 8;
constexpr int MIMIR_OBS_STEM_MAX_FRAMES = 4096;
constexpr int MIMIR_OBS_STEM_ID_BYTES = 64;
constexpr int MIMIR_OBS_STEM_DISPLAY_BYTES = 64;
constexpr int MIMIR_OBS_STEM_SOURCE_BYTES = 64;
constexpr int MIMIR_OBS_STEM_HEADER_BYTES = 64;
constexpr int MIMIR_OBS_STEM_RECORD_BYTES = 224;
constexpr int MIMIR_OBS_STEM_MAP_BYTES =
MIMIR_OBS_STEM_HEADER_BYTES +
(MIMIR_OBS_STEM_MAX_STEMS * MIMIR_OBS_STEM_RECORD_BYTES) +
(MIMIR_OBS_STEM_MAX_STEMS * MIMIR_OBS_STEM_MAX_FRAMES * static_cast<int>(sizeof(float)));
struct MimirObsStemHeader {
uint64_t magic;
int32_t version;
uint32_t generation;
int32_t stem_count;
int32_t max_stems;
int32_t max_frames;
int32_t header_bytes;
int32_t record_bytes;
int32_t map_bytes;
};
struct MimirObsStemRecord {
int32_t channel_index;
int32_t configured;
int32_t sample_rate;
int32_t frame_count;
int32_t sample_offset;
int32_t reserved0;
int32_t reserved1;
int32_t reserved2;
char stem_id[MIMIR_OBS_STEM_ID_BYTES];
char display_name[MIMIR_OBS_STEM_DISPLAY_BYTES];
char source_id[MIMIR_OBS_STEM_SOURCE_BYTES];
};

View file

@ -0,0 +1,219 @@
#include <obs-module.h>
#include <util/platform.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
#include <windows.h>
#include "mimir_obs_stem_ipc.h"
OBS_DECLARE_MODULE()
OBS_MODULE_USE_DEFAULT_LOCALE("mimir-obs-stem-source", "en-US")
void mimir_register_program_texture_source();
void mimir_register_muninn_source();
struct MimirStemSource {
obs_source_t *source = nullptr;
std::string map_name = "Local\\MimirObsStemBus";
std::string stem_id = "aligned_source_0";
HANDLE mapping = nullptr;
const uint8_t *view = nullptr;
std::thread worker;
std::atomic_bool running = false;
uint32_t last_generation = 0;
};
static std::string obs_string(obs_data_t *settings, const char *key, const char *fallback)
{
const char *value = obs_data_get_string(settings, key);
return value && *value ? std::string(value) : std::string(fallback);
}
static void close_mapping(MimirStemSource *ctx)
{
if (ctx->view) {
UnmapViewOfFile(ctx->view);
ctx->view = nullptr;
}
if (ctx->mapping) {
CloseHandle(ctx->mapping);
ctx->mapping = nullptr;
}
}
static bool open_mapping(MimirStemSource *ctx)
{
if (ctx->mapping) {
return true;
}
ctx->mapping = OpenFileMappingA(FILE_MAP_READ, FALSE, ctx->map_name.c_str());
if (!ctx->mapping) {
return false;
}
ctx->view = static_cast<const uint8_t *>(MapViewOfFile(ctx->mapping, FILE_MAP_READ, 0, 0, MIMIR_OBS_STEM_MAP_BYTES));
if (!ctx->view) {
close_mapping(ctx);
return false;
}
return true;
}
static const MimirObsStemRecord *find_record(const uint8_t *view, const std::string &stem_id, int stem_count)
{
const auto count = std::min(stem_count, MIMIR_OBS_STEM_MAX_STEMS);
for (int index = 0; index < count; ++index) {
const auto *record = reinterpret_cast<const MimirObsStemRecord *>(view + MIMIR_OBS_STEM_HEADER_BYTES + (index * MIMIR_OBS_STEM_RECORD_BYTES));
if (record->frame_count <= 0 || record->sample_rate <= 0 || record->sample_offset <= 0) {
continue;
}
if (stem_id == record->stem_id) {
return record;
}
}
return nullptr;
}
static void publish_if_available(MimirStemSource *ctx)
{
if (!open_mapping(ctx)) {
return;
}
const auto *header = reinterpret_cast<const MimirObsStemHeader *>(ctx->view);
const auto generation_a = header->generation;
if ((generation_a & 1u) != 0 || generation_a == ctx->last_generation) {
return;
}
if (header->magic != MIMIR_OBS_STEM_MAGIC ||
header->version != MIMIR_OBS_STEM_VERSION ||
header->map_bytes != MIMIR_OBS_STEM_MAP_BYTES) {
return;
}
const auto *record = find_record(ctx->view, ctx->stem_id, header->stem_count);
if (!record) {
return;
}
const auto frame_count = std::min(record->frame_count, MIMIR_OBS_STEM_MAX_FRAMES);
const auto sample_rate = record->sample_rate;
const auto byte_count = static_cast<size_t>(frame_count) * sizeof(float);
if (record->sample_offset < 0 || record->sample_offset + static_cast<int>(byte_count) > MIMIR_OBS_STEM_MAP_BYTES) {
return;
}
std::vector<float> samples(frame_count);
std::memcpy(samples.data(), ctx->view + record->sample_offset, byte_count);
const auto generation_b = header->generation;
if (generation_a != generation_b || (generation_b & 1u) != 0) {
return;
}
ctx->last_generation = generation_b;
uint8_t *planes[MAX_AV_PLANES] = {};
planes[0] = reinterpret_cast<uint8_t *>(samples.data());
obs_source_audio audio = {};
audio.data[0] = planes[0];
audio.frames = static_cast<uint32_t>(frame_count);
audio.speakers = SPEAKERS_MONO;
audio.samples_per_sec = static_cast<uint32_t>(sample_rate);
audio.format = AUDIO_FORMAT_FLOAT_PLANAR;
audio.timestamp = os_gettime_ns();
obs_source_output_audio(ctx->source, &audio);
}
static void worker_loop(MimirStemSource *ctx)
{
while (ctx->running.load()) {
publish_if_available(ctx);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
static const char *mimir_stem_get_name(void *)
{
return obs_module_text("MimirAudioStem");
}
static void mimir_stem_update(void *data, obs_data_t *settings)
{
auto *ctx = static_cast<MimirStemSource *>(data);
ctx->map_name = obs_string(settings, "map_name", "Local\\MimirObsStemBus");
ctx->stem_id = obs_string(settings, "stem_id", "aligned_source_0");
ctx->last_generation = 0;
close_mapping(ctx);
}
static void *mimir_stem_create(obs_data_t *settings, obs_source_t *source)
{
auto *ctx = new MimirStemSource();
ctx->source = source;
mimir_stem_update(ctx, settings);
ctx->running = true;
ctx->worker = std::thread(worker_loop, ctx);
return ctx;
}
static void mimir_stem_destroy(void *data)
{
auto *ctx = static_cast<MimirStemSource *>(data);
ctx->running = false;
if (ctx->worker.joinable()) {
ctx->worker.join();
}
close_mapping(ctx);
delete ctx;
}
static void mimir_stem_defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "map_name", "Local\\MimirObsStemBus");
obs_data_set_default_string(settings, "stem_id", "aligned_source_0");
}
static obs_properties_t *mimir_stem_properties(void *)
{
obs_properties_t *props = obs_properties_create();
obs_properties_add_text(props, "map_name", obs_module_text("MimirAudioStem.MapName"), OBS_TEXT_DEFAULT);
obs_properties_add_text(props, "stem_id", obs_module_text("MimirAudioStem.StemId"), OBS_TEXT_DEFAULT);
return props;
}
static obs_source_info mimir_stem_source_info = {
.id = "mimir_audio_stem_source",
.type = OBS_SOURCE_TYPE_INPUT,
.output_flags = OBS_SOURCE_AUDIO,
.get_name = mimir_stem_get_name,
.create = mimir_stem_create,
.destroy = mimir_stem_destroy,
.get_defaults = mimir_stem_defaults,
.get_properties = mimir_stem_properties,
.update = mimir_stem_update,
};
bool obs_module_load(void)
{
obs_register_source(&mimir_stem_source_info);
mimir_register_program_texture_source();
mimir_register_muninn_source();
blog(
LOG_INFO,
"Mimir OBS sources loaded: mimir_audio_stem_source, mimir_program_texture_source, "
"muninn_stream_source, muninn_video_source, muninn_audio_source");
return true;
}

View file

@ -0,0 +1,267 @@
#pragma once
#include <chrono>
#include <climits>
#include <cstdint>
#include <string>
#include <vector>
namespace muninn_media_wire {
inline constexpr const char *ReceiverFeedbackSchema = "muninn.media_receiver_feedback.v1";
class MsgpackWriter {
public:
const std::vector<uint8_t> &bytes() const { return bytes_; }
void write_raw(const std::vector<uint8_t> &value)
{
bytes_.insert(bytes_.end(), value.begin(), value.end());
}
void write_nil() { bytes_.push_back(0xc0); }
void write_bool(bool value) { bytes_.push_back(value ? 0xc3 : 0xc2); }
void write_u64(uint64_t value)
{
if (value <= 0x7f) {
bytes_.push_back(static_cast<uint8_t>(value));
} else if (value <= 0xff) {
bytes_.push_back(0xcc);
bytes_.push_back(static_cast<uint8_t>(value));
} else if (value <= 0xffff) {
bytes_.push_back(0xcd);
write_be16_value(static_cast<uint16_t>(value));
} else if (value <= 0xffffffffull) {
bytes_.push_back(0xce);
write_be32_value(static_cast<uint32_t>(value));
} else {
bytes_.push_back(0xcf);
write_be64_value(value);
}
}
void write_i64(int64_t value)
{
if (value >= 0) {
write_u64(static_cast<uint64_t>(value));
return;
}
if (value >= -32) {
bytes_.push_back(static_cast<uint8_t>(0xe0 | (value + 32)));
} else if (value >= INT8_MIN) {
bytes_.push_back(0xd0);
bytes_.push_back(static_cast<uint8_t>(value));
} else if (value >= INT16_MIN) {
bytes_.push_back(0xd1);
write_be16_value(static_cast<uint16_t>(value));
} else if (value >= INT32_MIN) {
bytes_.push_back(0xd2);
write_be32_value(static_cast<uint32_t>(value));
} else {
bytes_.push_back(0xd3);
write_be64_value(static_cast<uint64_t>(value));
}
}
void write_string(const std::string &value)
{
const auto length = value.size();
if (length <= 31) {
bytes_.push_back(static_cast<uint8_t>(0xa0 | length));
} else if (length <= 0xff) {
bytes_.push_back(0xd9);
bytes_.push_back(static_cast<uint8_t>(length));
} else if (length <= 0xffff) {
bytes_.push_back(0xda);
write_be16_value(static_cast<uint16_t>(length));
} else {
bytes_.push_back(0xdb);
write_be32_value(static_cast<uint32_t>(length));
}
bytes_.insert(bytes_.end(), value.begin(), value.end());
}
void write_binary(const std::vector<uint8_t> &value)
{
const auto length = value.size();
if (length <= 0xff) {
bytes_.push_back(0xc4);
bytes_.push_back(static_cast<uint8_t>(length));
} else if (length <= 0xffff) {
bytes_.push_back(0xc5);
write_be16_value(static_cast<uint16_t>(length));
} else {
bytes_.push_back(0xc6);
write_be32_value(static_cast<uint32_t>(length));
}
bytes_.insert(bytes_.end(), value.begin(), value.end());
}
void write_array_len(uint32_t length)
{
if (length <= 15) {
bytes_.push_back(static_cast<uint8_t>(0x90 | length));
} else if (length <= 0xffff) {
bytes_.push_back(0xdc);
write_be16_value(static_cast<uint16_t>(length));
} else {
bytes_.push_back(0xdd);
write_be32_value(length);
}
}
void write_map_len(uint32_t length)
{
if (length <= 15) {
bytes_.push_back(static_cast<uint8_t>(0x80 | length));
} else if (length <= 0xffff) {
bytes_.push_back(0xde);
write_be16_value(static_cast<uint16_t>(length));
} else {
bytes_.push_back(0xdf);
write_be32_value(length);
}
}
private:
std::vector<uint8_t> bytes_;
void write_be16_value(uint16_t value)
{
bytes_.push_back(static_cast<uint8_t>((value >> 8) & 0xff));
bytes_.push_back(static_cast<uint8_t>(value & 0xff));
}
void write_be32_value(uint32_t value)
{
bytes_.push_back(static_cast<uint8_t>((value >> 24) & 0xff));
bytes_.push_back(static_cast<uint8_t>((value >> 16) & 0xff));
bytes_.push_back(static_cast<uint8_t>((value >> 8) & 0xff));
bytes_.push_back(static_cast<uint8_t>(value & 0xff));
}
void write_be64_value(uint64_t value)
{
write_be32_value(static_cast<uint32_t>((value >> 32) & 0xffffffffull));
write_be32_value(static_cast<uint32_t>(value & 0xffffffffull));
}
};
inline std::string feedback_observed_at()
{
const auto now = std::chrono::system_clock::now().time_since_epoch();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
return "unix-ms:" + std::to_string(millis);
}
inline void write_string_array(MsgpackWriter &writer, const std::vector<std::string> &values)
{
writer.write_array_len(static_cast<uint32_t>(values.size()));
for (const auto &value : values) {
writer.write_string(value);
}
}
inline void write_u64_array(MsgpackWriter &writer, const std::vector<uint64_t> &values)
{
writer.write_array_len(static_cast<uint32_t>(values.size()));
for (const auto value : values) {
writer.write_u64(value);
}
}
inline std::vector<uint8_t> encode_receiver_feedback_payload(
const std::string &stream_id,
const std::string &session_id,
uint64_t frame_id,
const std::vector<std::string> &missing_chunk_keys,
bool requested_keyframe,
const std::string &observed_at)
{
MsgpackWriter record;
record.write_array_len(11);
record.write_string(stream_id);
record.write_string(session_id);
record.write_string("starfire.obs");
if (frame_id > 0) {
record.write_u64(frame_id - 1);
} else {
record.write_nil();
}
write_u64_array(record, {});
write_u64_array(record, {frame_id});
record.write_bool(requested_keyframe);
record.write_i64(0);
record.write_i64(0);
record.write_string(observed_at);
write_string_array(record, missing_chunk_keys);
const std::string record_key = stream_id + ":" + session_id + ":feedback:starfire.obs";
const std::string message_id =
"muninn-media:" + std::string(ReceiverFeedbackSchema) + ":" + record_key;
MsgpackWriter document;
document.write_map_len(9);
document.write_string("schemaId");
document.write_string(ReceiverFeedbackSchema);
document.write_string("recordKey");
document.write_string(record_key);
document.write_string("storedAt");
document.write_string(observed_at);
document.write_string("payloadEncoding");
document.write_string("messagepack");
document.write_string("payload");
document.write_binary(record.bytes());
document.write_string("sourceRuntimeId");
document.write_string("starfire");
document.write_string("sourceAgentId");
document.write_nil();
document.write_string("sourceRole");
document.write_string("mimir.obs");
document.write_string("tags");
write_string_array(document, {"muninn.media"});
MsgpackWriter wire;
wire.write_map_len(3);
wire.write_string("schemaVersion");
wire.write_string("cultnet.document_put_raw.v0");
wire.write_string("messageId");
wire.write_string(message_id);
wire.write_string("document");
wire.write_raw(document.bytes());
return wire.bytes();
}
inline std::vector<uint8_t> encode_receiver_feedback_payload(
const std::string &stream_id,
const std::string &session_id,
uint64_t frame_id,
const std::vector<std::string> &missing_chunk_keys,
bool requested_keyframe)
{
return encode_receiver_feedback_payload(
stream_id,
session_id,
frame_id,
missing_chunk_keys,
requested_keyframe,
feedback_observed_at());
}
inline std::vector<uint8_t> encode_receiver_feedback_payload(
const std::string &stream_id,
const std::string &session_id,
uint64_t frame_id,
const std::vector<std::string> &missing_chunk_keys)
{
return encode_receiver_feedback_payload(
stream_id,
session_id,
frame_id,
missing_chunk_keys,
true);
}
} // namespace muninn_media_wire

View file

@ -0,0 +1,54 @@
#pragma once
#include <cstdint>
#include <optional>
#include <set>
#include <utility>
namespace muninn_rudp_ack {
class AckTracker {
public:
void reset()
{
received_sequences_.clear();
highest_received_sequence_.reset();
}
void remember(uint32_t sequence)
{
received_sequences_.insert(sequence);
if (!highest_received_sequence_ || sequence > *highest_received_sequence_) {
highest_received_sequence_ = sequence;
}
if (!highest_received_sequence_) {
return;
}
const uint32_t highest = *highest_received_sequence_;
for (auto iterator = received_sequences_.begin(); iterator != received_sequences_.end();) {
if (*iterator + 32 < highest) {
iterator = received_sequences_.erase(iterator);
} else {
++iterator;
}
}
}
std::pair<uint32_t, uint32_t> state() const
{
const uint32_t ack = highest_received_sequence_.value_or(0);
uint32_t ack_mask = 0;
for (uint32_t bit = 0; bit < 32; ++bit) {
if (ack > bit && received_sequences_.find(ack - bit - 1) != received_sequences_.end()) {
ack_mask |= 1u << bit;
}
}
return {ack, ack_mask};
}
private:
std::set<uint32_t> received_sequences_;
std::optional<uint32_t> highest_received_sequence_;
};
} // namespace muninn_rudp_ack

View file

@ -0,0 +1,127 @@
#pragma once
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace muninn_rudp_fragments {
constexpr uint32_t CompletedFragmentIdQuarantineMs = 2000;
struct FragmentResult {
uint32_t first_sequence = 0;
std::vector<uint8_t> payload;
};
class FragmentAssembler {
public:
void reset()
{
assemblies_.clear();
recently_closed_.clear();
expired_count_ = 0;
}
std::optional<FragmentResult> insert(const std::string &channel_id,
uint16_t fragment_id,
uint16_t fragment_index,
uint16_t fragment_count,
uint32_t sequence,
const uint8_t *payload,
uint32_t payload_len,
std::chrono::steady_clock::time_point now)
{
if (fragment_id == 0 || fragment_index >= fragment_count) {
return std::nullopt;
}
const std::string key = channel_id + ":" + std::to_string(fragment_id);
expire_recently_closed(now);
const auto existing = assemblies_.find(key);
if (existing == assemblies_.end() && recently_closed_.find(key) != recently_closed_.end()) {
return std::nullopt;
}
auto &assembly = assemblies_[key];
if (assembly.payloads.empty()) {
assembly.channel_id = channel_id;
assembly.fragment_count = fragment_count;
assembly.started_at = now;
}
if (assembly.channel_id != channel_id || assembly.fragment_count != fragment_count) {
assemblies_.erase(key);
return std::nullopt;
}
assembly.payloads.emplace(fragment_index, std::vector<uint8_t>(payload, payload + payload_len));
assembly.sequences.emplace(fragment_index, sequence);
if (assembly.payloads.size() < assembly.fragment_count) {
return std::nullopt;
}
FragmentResult result;
result.first_sequence = UINT32_MAX;
for (uint16_t index = 0; index < assembly.fragment_count; ++index) {
const auto payload_found = assembly.payloads.find(index);
const auto sequence_found = assembly.sequences.find(index);
if (payload_found == assembly.payloads.end() || sequence_found == assembly.sequences.end()) {
return std::nullopt;
}
result.payload.insert(result.payload.end(), payload_found->second.begin(), payload_found->second.end());
result.first_sequence = std::min(result.first_sequence, sequence_found->second);
}
assemblies_.erase(key);
recently_closed_[key] = now;
return result;
}
uint64_t expire(std::chrono::steady_clock::time_point now, uint32_t expire_after_ms)
{
expire_recently_closed(now);
uint64_t expired = 0;
for (auto iterator = assemblies_.begin(); iterator != assemblies_.end();) {
if (now - iterator->second.started_at < std::chrono::milliseconds(expire_after_ms)) {
++iterator;
continue;
}
recently_closed_[iterator->first] = now;
iterator = assemblies_.erase(iterator);
++expired;
}
expired_count_ += expired;
return expired;
}
uint64_t expired_count() const { return expired_count_; }
private:
void expire_recently_closed(std::chrono::steady_clock::time_point now)
{
for (auto iterator = recently_closed_.begin(); iterator != recently_closed_.end();) {
if (now - iterator->second < std::chrono::milliseconds(CompletedFragmentIdQuarantineMs)) {
++iterator;
continue;
}
iterator = recently_closed_.erase(iterator);
}
}
struct Assembly {
std::string channel_id;
uint16_t fragment_count = 0;
std::map<uint16_t, std::vector<uint8_t>> payloads;
std::map<uint16_t, uint32_t> sequences;
std::chrono::steady_clock::time_point started_at;
};
std::map<std::string, Assembly> assemblies_;
std::map<std::string, std::chrono::steady_clock::time_point> recently_closed_;
uint64_t expired_count_ = 0;
};
} // namespace muninn_rudp_fragments

View file

@ -0,0 +1,123 @@
#pragma once
#include <algorithm>
#include <cstdint>
#include <optional>
#include <sstream>
#include <string>
namespace muninn_rudp_url {
inline constexpr uint32_t DefaultMediaRudpConnectionId = 0x6d750001;
inline constexpr uint32_t DefaultAudioRudpConnectionId = 0x6d750004;
inline constexpr uint32_t DefaultReliableExpireAfterMs = 75;
inline constexpr uint32_t DefaultVideoAssemblyDeadlineMs = 75;
inline constexpr uint32_t DefaultGapWaitMs = 8;
inline constexpr uint32_t DefaultAudioReorderMs = 2000;
inline constexpr uint32_t DefaultSenderResendDelayMs = 5;
inline constexpr uint32_t MaxLatencyBudgetMs = 2000;
struct RudpUrlParts {
uint16_t port = 0;
uint16_t video_local_port = 0;
uint16_t audio_local_port = 0;
uint32_t connection_id = DefaultMediaRudpConnectionId;
uint32_t audio_connection_id = DefaultAudioRudpConnectionId;
uint32_t sender_resend_delay_ms = DefaultSenderResendDelayMs;
uint32_t reliable_expire_after_ms = DefaultReliableExpireAfterMs;
uint32_t video_assembly_deadline_ms = DefaultVideoAssemblyDeadlineMs;
uint32_t gap_wait_ms = DefaultGapWaitMs;
uint32_t audio_reorder_ms = DefaultAudioReorderMs;
};
inline bool starts_with(const std::string &value, const std::string &prefix)
{
return value.rfind(prefix, 0) == 0;
}
inline std::optional<uint32_t> parse_u32_hex_or_decimal(const std::string &value)
{
try {
size_t consumed = 0;
const int base = starts_with(value, "0x") || starts_with(value, "0X") ? 16 : 10;
const auto parsed = std::stoul(value, &consumed, base);
if (consumed == value.size()) {
return static_cast<uint32_t>(parsed);
}
} catch (...) {
}
return std::nullopt;
}
inline std::optional<RudpUrlParts> parse(const std::string &url)
{
if (!starts_with(url, "rudp://")) {
return std::nullopt;
}
const auto authority_start = std::string("rudp://").size();
const auto path_start = url.find('/', authority_start);
const auto query_start = url.find('?', authority_start);
const auto authority_end = std::min(path_start == std::string::npos ? url.size() : path_start,
query_start == std::string::npos ? url.size() : query_start);
const auto authority = url.substr(authority_start, authority_end - authority_start);
const auto colon = authority.rfind(':');
if (colon == std::string::npos || colon + 1 >= authority.size()) {
return std::nullopt;
}
RudpUrlParts parts;
parts.port = static_cast<uint16_t>(std::stoul(authority.substr(colon + 1)));
parts.video_local_port = static_cast<uint16_t>(parts.port + 10000);
parts.audio_local_port = static_cast<uint16_t>(parts.port + 10001);
if (query_start != std::string::npos) {
std::stringstream query(url.substr(query_start + 1));
std::string item;
while (std::getline(query, item, '&')) {
const auto equals = item.find('=');
if (equals == std::string::npos) {
continue;
}
const auto key = item.substr(0, equals);
const auto value = item.substr(equals + 1);
if (key == "connection") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.connection_id = *parsed;
}
} else if (key == "audio_connection" || key == "audio_connection_id") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.audio_connection_id = *parsed;
}
} else if (key == "local_port" || key == "video_local_port") {
parts.video_local_port = static_cast<uint16_t>(std::stoul(value));
} else if (key == "audio_local_port") {
parts.audio_local_port = static_cast<uint16_t>(std::stoul(value));
} else if (key == "reliable_expire_after_ms") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.reliable_expire_after_ms = std::clamp(*parsed, 1u, MaxLatencyBudgetMs);
}
} else if (key == "sender_resend_delay_ms") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.sender_resend_delay_ms = std::clamp(*parsed, 1u, 1000u);
}
} else if (key == "assembly_deadline_ms") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.video_assembly_deadline_ms = std::clamp(*parsed, 1u, MaxLatencyBudgetMs);
}
} else if (key == "gap_wait_ms") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.gap_wait_ms = std::clamp(*parsed, 0u, 1000u);
}
} else if (key == "audio_reorder_ms") {
if (const auto parsed = parse_u32_hex_or_decimal(value)) {
parts.audio_reorder_ms = std::clamp(*parsed, 0u, MaxLatencyBudgetMs);
}
}
}
}
return parts;
}
} // namespace muninn_rudp_url

View file

@ -0,0 +1,58 @@
#include "muninn_rudp_url.h"
#include <cstdlib>
#include <iostream>
#include <string>
namespace {
void require(bool condition, const std::string &message)
{
if (!condition) {
std::cerr << message << '\n';
std::exit(1);
}
}
} // namespace
int main()
{
const auto configured = muninn_rudp_url::parse(
"rudp://192.168.1.66:5204/muninn.raven.av.rudp?channel=media&format=muninn-typed-media&connection=0x6d750001&audio_connection=0x6d750004&sender_resend_delay_ms=5&reliable_expire_after_ms=1200&assembly_deadline_ms=1200&gap_wait_ms=16&audio_reorder_ms=700");
require(configured.has_value(), "configured RUDP URL did not parse");
require(configured->port == 5204, "configured RUDP URL port was not parsed");
require(configured->video_local_port == 15204, "configured RUDP video local port was not derived");
require(configured->audio_local_port == 15205, "configured RUDP audio local port was not derived");
require(configured->connection_id == 0x6d750001, "configured RUDP connection id was not parsed");
require(configured->audio_connection_id == 0x6d750004, "configured RUDP audio connection id was not parsed");
require(configured->reliable_expire_after_ms == 1200, "configured reliable expiry did not survive parse");
require(configured->video_assembly_deadline_ms == 1200, "configured assembly deadline did not survive parse");
require(configured->gap_wait_ms == 16, "configured gap wait did not survive parse");
require(configured->audio_reorder_ms == 700, "configured audio reorder wait did not survive parse");
const auto clamped = muninn_rudp_url::parse(
"rudp://192.168.1.66:5204/muninn.raven.av.rudp?reliable_expire_after_ms=9000&assembly_deadline_ms=9000");
require(clamped.has_value(), "oversized latency RUDP URL did not parse");
require(clamped->reliable_expire_after_ms == muninn_rudp_url::MaxLatencyBudgetMs,
"oversized reliable expiry did not clamp to the max latency budget");
require(clamped->video_assembly_deadline_ms == muninn_rudp_url::MaxLatencyBudgetMs,
"oversized assembly deadline did not clamp to the max latency budget");
const auto fallback = muninn_rudp_url::parse("rudp://192.168.1.66:5204/muninn.raven.av.rudp");
require(fallback.has_value(), "minimal RUDP URL did not parse");
require(fallback->connection_id == muninn_rudp_url::DefaultMediaRudpConnectionId,
"minimal RUDP URL did not keep the default connection id");
require(fallback->audio_connection_id == muninn_rudp_url::DefaultAudioRudpConnectionId,
"minimal RUDP URL did not keep the default audio connection id");
require(fallback->reliable_expire_after_ms == muninn_rudp_url::DefaultReliableExpireAfterMs,
"minimal RUDP URL did not keep the default reliable expiry");
require(fallback->video_assembly_deadline_ms == muninn_rudp_url::DefaultVideoAssemblyDeadlineMs,
"minimal RUDP URL did not keep the default assembly deadline");
require(fallback->audio_reorder_ms == muninn_rudp_url::DefaultAudioReorderMs,
"minimal RUDP URL did not keep the default audio reorder wait");
require(fallback->audio_reorder_ms == 2000,
"minimal RUDP URL did not use the full default audio reorder budget");
return 0;
}