Restore Muninn OBS plugin with Odin discovery
This commit is contained in:
parent
d9005e340b
commit
e00458ff3b
11 changed files with 5684 additions and 0 deletions
33
native/obs_stem_source/CMakeLists.txt
Normal file
33
native/obs_stem_source/CMakeLists.txt
Normal 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)
|
||||
14
native/obs_stem_source/data/locale/en-US.ini
Normal file
14
native/obs_stem_source/data/locale/en-US.ini
Normal 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"
|
||||
4118
native/obs_stem_source/mimir_obs_muninn_source.cpp
Normal file
4118
native/obs_stem_source/mimir_obs_muninn_source.cpp
Normal file
File diff suppressed because it is too large
Load diff
628
native/obs_stem_source/mimir_obs_program_texture_source.cpp
Normal file
628
native/obs_stem_source/mimir_obs_program_texture_source.cpp
Normal 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);
|
||||
}
|
||||
43
native/obs_stem_source/mimir_obs_stem_ipc.h
Normal file
43
native/obs_stem_source/mimir_obs_stem_ipc.h
Normal 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];
|
||||
};
|
||||
219
native/obs_stem_source/mimir_obs_stem_source.cpp
Normal file
219
native/obs_stem_source/mimir_obs_stem_source.cpp
Normal 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;
|
||||
}
|
||||
267
native/obs_stem_source/muninn_media_wire.h
Normal file
267
native/obs_stem_source/muninn_media_wire.h
Normal 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
|
||||
54
native/obs_stem_source/muninn_rudp_ack.h
Normal file
54
native/obs_stem_source/muninn_rudp_ack.h
Normal 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
|
||||
127
native/obs_stem_source/muninn_rudp_fragments.h
Normal file
127
native/obs_stem_source/muninn_rudp_fragments.h
Normal 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
|
||||
123
native/obs_stem_source/muninn_rudp_url.h
Normal file
123
native/obs_stem_source/muninn_rudp_url.h
Normal 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
|
||||
58
native/obs_stem_source/tests/muninn_rudp_url_tests.cpp
Normal file
58
native/obs_stem_source/tests/muninn_rudp_url_tests.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue