#include "MxlSdkFeed.hpp" #include "MxlInstanceCache.hpp" #include "PixelFormat.hpp" #include #include #include #include #include #include #if MXL_MULTIVIEWER_ENABLE_MXL_SDK #include #include #include #include namespace { std::string unescapeJsonString( const char* begin, const char* end) { std::string value; value.reserve(static_cast(end - begin)); for (const char* p = begin; p < end; ++p) { if (*p != '\\' || p + 1 >= end) { value.push_back(*p); continue; } ++p; switch (*p) { case '"': value.push_back('"'); break; case '\\': value.push_back('\\'); break; case '/': value.push_back('/'); break; case 'b': value.push_back('\b'); break; case 'f': value.push_back('\f'); break; case 'n': value.push_back('\n'); break; case 'r': value.push_back('\r'); break; case 't': value.push_back('\t'); break; default: value.push_back(*p); break; } } return value; } std::string parseStringFromJson( const char* json, const char* key) { int depth = 0; for (const char* p = json; *p != '\0'; ++p) { if (*p == '"') { const char* keyBegin = p + 1; const char* keyEnd = keyBegin; bool keyEscaped = false; while (*keyEnd != '\0') { if (!keyEscaped && *keyEnd == '"') { break; } keyEscaped = !keyEscaped && *keyEnd == '\\'; if (*keyEnd != '\\') { keyEscaped = false; } ++keyEnd; } if (*keyEnd == '\0') { return ""; } const char* value = keyEnd + 1; while (*value == ' ' || *value == '\t' || *value == '\n' || *value == '\r') { ++value; } if (depth == 1 && *value == ':' && unescapeJsonString(keyBegin, keyEnd) == key) { ++value; while (*value == ' ' || *value == '\t' || *value == '\n' || *value == '\r') { ++value; } if (*value != '"') { return ""; } ++value; const char* valueBegin = value; bool valueEscaped = false; while (*value != '\0') { if (!valueEscaped && *value == '"') { return unescapeJsonString( valueBegin, value); } valueEscaped = !valueEscaped && *value == '\\'; if (*value != '\\') { valueEscaped = false; } ++value; } return ""; } p = keyEnd; continue; } if (*p == '{' || *p == '[') { ++depth; } else if ((*p == '}' || *p == ']') && depth > 0) { --depth; } } return ""; } std::string firstNonEmptyFlowLabel(const char* json) { const char* keys[] = { "name", "flow_name", "description", "label" }; for (const char* key : keys) { std::string value = parseStringFromJson(json, key); if (!value.empty()) { return value; } } return ""; } uint32_t parseUintFromJson(const char* json, const char* key) { std::string searchKey = std::string("\"") + key + "\""; const char* pos = std::strstr(json, searchKey.c_str()); if (!pos) { return 0; } pos += searchKey.size(); while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r' || *pos == ':') { pos++; } return static_cast(std::strtoul(pos, nullptr, 10)); } uint32_t deriveV210Width(uint32_t stride) { if (stride == 0 || stride % 16 != 0) { return 0; } return (stride / 16) * 6; } } #endif MxlSdkFeed::MxlSdkFeed( const std::string& domain, const std::string& flowId, uint32_t outputWidth, uint32_t outputHeight, bool verbose) : mDomain(domain) , mFlowId(flowId) { (void)verbose; mFrame.width = outputWidth; mFrame.height = outputHeight; mFrame.pixels.resize( static_cast(outputWidth) * outputHeight, 0); #if MXL_MULTIVIEWER_ENABLE_MXL_SDK mVerbose = verbose; mGrainRate.numerator = 30000; mGrainRate.denominator = 1001; auto& cache = MxlInstanceCache::instance(); mInstanceHandle = cache.acquire(mDomain, mVerbose); if (!mInstanceHandle) { std::cerr << "MxlSdkFeed: failed to create MXL instance" << " for domain: " << mDomain << std::endl; } #endif } MxlSdkFeed::~MxlSdkFeed() { #if MXL_MULTIVIEWER_ENABLE_MXL_SDK closeFlow(); if (mInstanceHandle) { auto& cache = MxlInstanceCache::instance(); cache.release(mInstanceHandle); mInstanceHandle = nullptr; } #endif } #if MXL_MULTIVIEWER_ENABLE_MXL_SDK bool MxlSdkFeed::openFlow() { if (mFlowOpened) { return true; } if (mOpenRetryCount >= 300) { mFlowInvalid = true; return false; } if (!mInstanceHandle || !mInstanceHandle->instance) { mFlowInvalid = true; return false; } if (mOpenRetryCount > 0) { int delayMs = 100; if (mOpenRetryCount > 5) { delayMs = 500; } if (mOpenRetryCount > 20) { delayMs = 2000; } std::this_thread::sleep_for( std::chrono::milliseconds(delayMs)); } ++mOpenRetryCount; mxlInstance instance = mInstanceHandle->instance; mxlStatus status = mxlCreateFlowReader( instance, mFlowId.c_str(), nullptr, &mReader); if (status == MXL_ERR_FLOW_NOT_FOUND || status == MXL_ERR_CONFLICT) { mFlowInvalid = true; return false; } if (status != MXL_STATUS_OK) { std::cerr << "MxlSdkFeed: failed to open flow " << mFlowId << " (status=" << status << ")" << std::endl; mFlowInvalid = true; return false; } mxlFlowConfigInfo configInfo{}; std::memset(&configInfo, 0, sizeof(configInfo)); status = mxlFlowReaderGetConfigInfo(mReader, &configInfo); if (status != MXL_STATUS_OK) { std::cerr << "MxlSdkFeed: failed to get config info" << " (status=" << status << ")" << std::endl; mxlReleaseFlowReader(instance, mReader); mReader = nullptr; return false; } mSourceStride = configInfo.discrete.sliceSizes[0]; mGrainRate = configInfo.common.grainRate; if (mVerbose) { std::cerr << "MxlSdkFeed: stride=" << mSourceStride << " grainRate=" << mGrainRate.numerator << "/" << mGrainRate.denominator << " grainsPerFrame=" << configInfo.discrete.grainCount << std::endl; } mSourceWidth = deriveV210Width(mSourceStride); if (mSourceWidth == 0) { std::cerr << "MxlSdkFeed: could not determine source width from stride=" << mSourceStride << std::endl; mxlReleaseFlowReader(instance, mReader); mReader = nullptr; return false; } size_t defBufSize = 0; mxlStatus defStatus = mxlGetFlowDef( instance, mFlowId.c_str(), nullptr, &defBufSize); if (mVerbose) { std::cerr << "MxlSdkFeed: mxlGetFlowDef status=" << defStatus << " bufSize=" << defBufSize << std::endl; } if (defBufSize > 0) { std::vector defBuf(defBufSize + 1); size_t readSize = defBufSize; defStatus = mxlGetFlowDef( instance, mFlowId.c_str(), defBuf.data(), &readSize); if (mVerbose) { std::cerr << "MxlSdkFeed: mxlGetFlowDef(read) status=" << defStatus << " readSize=" << readSize << std::endl; } if (readSize > 0) { defBuf[readSize] = '\0'; uint32_t jsonWidth = parseUintFromJson(defBuf.data(), "frame_width"); uint32_t jsonHeight = parseUintFromJson(defBuf.data(), "frame_height"); std::string flowName = firstNonEmptyFlowLabel(defBuf.data()); if (jsonWidth > 0) { mSourceWidth = jsonWidth; } if (jsonHeight > 0) { mSourceHeight = jsonHeight; } if (!flowName.empty()) { mFlowName = std::move(flowName); } if (mVerbose) { std::cerr << "MxlSdkFeed: flow definition: " << jsonWidth << "x" << jsonHeight << std::endl; if (!mFlowName.empty()) { std::cerr << "MxlSdkFeed: flow name: " << mFlowName << std::endl; } } } } if (mSourceHeight == 0) { mSourceHeight = 1080; std::cerr << "MxlSdkFeed: assuming height=1080" << std::endl; } if (mGrainRate.denominator == 0) { mGrainRate.numerator = 30000; mGrainRate.denominator = 1001; } mxlFlowRuntimeInfo runtimeInfo{}; if (mxlFlowReaderGetRuntimeInfo(mReader, &runtimeInfo) == MXL_STATUS_OK && runtimeInfo.headIndex > 0) { mLastDisplayedIndex = runtimeInfo.headIndex - 1; } else { mLastDisplayedIndex = mxlGetCurrentIndex(&mGrainRate); } if (mVerbose) { std::cerr << "MxlSdkFeed: opened flow " << mFlowId << " stride=" << mSourceStride << " grainRate=" << mGrainRate.numerator << "/" << mGrainRate.denominator << " src=" << mSourceWidth << "x" << mSourceHeight << std::endl; } mFlowOpened = true; mFlowInvalid = false; mOpenRetryCount = 0; return true; } void MxlSdkFeed::closeFlow() { if (mReader && mInstanceHandle && mInstanceHandle->instance) { mxlReleaseFlowReader(mInstanceHandle->instance, mReader); mReader = nullptr; } mFlowOpened = false; mFlowInvalid = true; mOpenRetryCount = 0; mV210Payload.clear(); } const VideoFrame& MxlSdkFeed::getFrame(uint32_t frameCounter) { (void)frameCounter; if (!mFlowOpened && !openFlow()) { return mFrame; } uint64_t index = mLastDisplayedIndex + 1; const uint64_t currentIndex = mxlGetCurrentIndex(&mGrainRate); if (index > currentIndex) { return mFrame; } mxlGrainInfo grain{}; grain.version = 2; grain.size = sizeof(mxlGrainInfo); uint8_t* payload = nullptr; while (true) { mxlStatus status = mxlFlowReaderGetGrainNonBlocking( mReader, index, &grain, &payload); if (status == MXL_STATUS_OK) { break; } if (status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { return mFrame; } if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE) { mxlFlowRuntimeInfo runtimeInfo{}; if (mxlFlowReaderGetRuntimeInfo(mReader, &runtimeInfo) != MXL_STATUS_OK) { return mFrame; } if (runtimeInfo.headIndex == 0) { return mFrame; } index = runtimeInfo.headIndex - 1; if (index <= mLastDisplayedIndex) { return mFrame; } continue; } if (status == MXL_ERR_FLOW_INVALID) { std::cerr << "MxlSdkFeed: flow invalidated, reconnecting" << std::endl; closeFlow(); return mFrame; } std::cerr << "MxlSdkFeed: grain read error status=" << static_cast(status) << std::endl; return mFrame; } mLastDisplayedIndex = index; const size_t payloadSize = static_cast(mSourceStride) * static_cast(mSourceHeight); if (payload == nullptr || payloadSize == 0) { return mFrame; } mV210Payload.resize(payloadSize); std::memcpy(mV210Payload.data(), payload, payloadSize); ++mFrameVersion; return mFrame; } bool MxlSdkFeed::readV210FrameInto( uint32_t frameCounter, void* destination, size_t destinationSize, V210ReadTiming* timing) { (void)frameCounter; if (timing != nullptr) { timing->sourceReadNs = 0; timing->payloadCopyNs = 0; } if (!mFlowOpened && !openFlow()) { return false; } uint64_t index = mLastDisplayedIndex + 1; const uint64_t currentIndex = mxlGetCurrentIndex(&mGrainRate); if (index > currentIndex) { return false; } mxlGrainInfo grain{}; grain.version = 2; grain.size = sizeof(mxlGrainInfo); uint8_t* payload = nullptr; const auto readStart = std::chrono::steady_clock::now(); while (true) { mxlStatus status = mxlFlowReaderGetGrainNonBlocking( mReader, index, &grain, &payload); if (status == MXL_STATUS_OK) { break; } if (status == MXL_ERR_OUT_OF_RANGE_TOO_EARLY) { return false; } if (status == MXL_ERR_OUT_OF_RANGE_TOO_LATE) { mxlFlowRuntimeInfo runtimeInfo{}; if (mxlFlowReaderGetRuntimeInfo(mReader, &runtimeInfo) != MXL_STATUS_OK) { return false; } if (runtimeInfo.headIndex == 0) { return false; } index = runtimeInfo.headIndex - 1; if (index <= mLastDisplayedIndex) { return false; } continue; } if (status == MXL_ERR_FLOW_INVALID) { std::cerr << "MxlSdkFeed: flow invalidated, reconnecting" << std::endl; closeFlow(); return false; } std::cerr << "MxlSdkFeed: grain read error status=" << static_cast(status) << std::endl; return false; } const auto readEnd = std::chrono::steady_clock::now(); const size_t payloadSize = static_cast(mSourceStride) * static_cast(mSourceHeight); if (payload == nullptr || destination == nullptr || payloadSize == 0 || destinationSize < payloadSize) { return false; } const auto copyStart = std::chrono::steady_clock::now(); std::memcpy(destination, payload, payloadSize); const auto copyEnd = std::chrono::steady_clock::now(); if (timing != nullptr) { timing->sourceReadNs = static_cast( std::chrono::duration_cast( readEnd - readStart).count()); timing->payloadCopyNs = static_cast( std::chrono::duration_cast( copyEnd - copyStart).count()); } mLastDisplayedIndex = index; ++mFrameVersion; return true; } FeedRuntimeStatus MxlSdkFeed::status() const { if (mFlowOpened && !mV210Payload.empty()) { return FeedRuntimeStatus::Live; } if (mFlowOpened) { return FeedRuntimeStatus::Connecting; } return mFlowInvalid ? FeedRuntimeStatus::Reconnecting : FeedRuntimeStatus::Connecting; } std::string MxlSdkFeed::displayName() const { return mFlowName.empty() ? "MXL LIVE" : mFlowName; } std::string MxlSdkFeed::sourceInfo() const { std::ostringstream out; if (!mFlowName.empty()) { out << mFlowName; } if (mSourceWidth == 0 || mSourceHeight == 0) { return out.str(); } if (!mFlowName.empty()) { out << " "; } out << mSourceWidth << "x" << mSourceHeight; if (mGrainRate.denominator > 0) { const double fps = static_cast(mGrainRate.numerator) / static_cast(mGrainRate.denominator); out << "@"; if (mGrainRate.denominator == 1) { out << mGrainRate.numerator; } else { out << std::fixed << std::setprecision(2) << fps; } out << "p"; } return out.str(); } uint64_t MxlSdkFeed::frameVersion() const { return mFrameVersion; } bool MxlSdkFeed::hasGrainIndex() const { return mFlowOpened; } uint64_t MxlSdkFeed::grainIndex() const { return mLastDisplayedIndex; } bool MxlSdkFeed::hasFrameRate() const { return mGrainRate.denominator > 0 && mGrainRate.numerator > 0; } double MxlSdkFeed::frameRate() const { if (!hasFrameRate()) { return 0.0; } return static_cast(mGrainRate.numerator) / static_cast(mGrainRate.denominator); } float MxlSdkFeed::srcAspectRatio() const { if (mSourceWidth > 0 && mSourceHeight > 0) { return static_cast(mSourceWidth) / static_cast(mSourceHeight); } return 16.0f / 9.0f; } bool MxlSdkFeed::hasV210() const { return mFlowOpened && !mV210Payload.empty(); } const uint8_t* MxlSdkFeed::v210Data() const { return mV210Payload.data(); } bool MxlSdkFeed::supportsDirectV210Read() const { return mFlowOpened; } uint32_t MxlSdkFeed::v210Width() const { return mSourceWidth; } uint32_t MxlSdkFeed::v210Height() const { return mSourceHeight; } uint32_t MxlSdkFeed::v210Stride() const { return mSourceStride; } #else bool MxlSdkFeed::openFlow() { return false; } void MxlSdkFeed::closeFlow() {} FeedRuntimeStatus MxlSdkFeed::status() const { return FeedRuntimeStatus::Error; } std::string MxlSdkFeed::displayName() const { return "MXL SDK DISABLED"; } std::string MxlSdkFeed::sourceInfo() const { return ""; } uint64_t MxlSdkFeed::frameVersion() const { return 0; } bool MxlSdkFeed::hasGrainIndex() const { return false; } uint64_t MxlSdkFeed::grainIndex() const { return 0; } bool MxlSdkFeed::hasFrameRate() const { return false; } double MxlSdkFeed::frameRate() const { return 0.0; } float MxlSdkFeed::srcAspectRatio() const { return 16.0f / 9.0f; } bool MxlSdkFeed::hasV210() const { return false; } const uint8_t* MxlSdkFeed::v210Data() const { return nullptr; } bool MxlSdkFeed::supportsDirectV210Read() const { return false; } bool MxlSdkFeed::readV210FrameInto( uint32_t frameCounter, void* destination, size_t destinationSize, V210ReadTiming* timing) { (void)frameCounter; (void)destination; (void)destinationSize; if (timing != nullptr) { timing->sourceReadNs = 0; timing->payloadCopyNs = 0; } return false; } uint32_t MxlSdkFeed::v210Width() const { return 0; } uint32_t MxlSdkFeed::v210Height() const { return 0; } uint32_t MxlSdkFeed::v210Stride() const { return 0; } const VideoFrame& MxlSdkFeed::getFrame(uint32_t frameCounter) { (void)frameCounter; for (uint32_t y = 0; y < mFrame.height; ++y) { for (uint32_t x = 0; x < mFrame.width; ++x) { uint8_t v = ((x + y) % 2 == 0) ? 80 : 40; mFrame.pixels[y * mFrame.width + x] = packRgba8({v, v, v}); } } return mFrame; } #endif