#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) { std::string searchKey = std::string("\"") + key + "\""; const char* pos = std::strstr(json, searchKey.c_str()); if (!pos) { return ""; } pos += searchKey.size(); while (*pos == ' ' || *pos == '\t' || *pos == '\n' || *pos == '\r' || *pos == ':') { pos++; } if (*pos != '"') { return ""; } ++pos; const char* valueBegin = pos; bool escaped = false; while (*pos != '\0') { if (!escaped && *pos == '"') { return unescapeJsonString(valueBegin, pos); } escaped = !escaped && *pos == '\\'; if (*pos != '\\') { escaped = false; } ++pos; } 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; } 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; mV210Payload = payload; ++mFrameVersion; return mFrame; } FeedRuntimeStatus MxlSdkFeed::status() const { if (mFlowOpened && mV210Payload != nullptr) { 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; } 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 != nullptr; } const uint8_t* MxlSdkFeed::v210Data() const { return mV210Payload; } 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; } float MxlSdkFeed::srcAspectRatio() const { return 16.0f / 9.0f; } bool MxlSdkFeed::hasV210() const { return false; } const uint8_t* MxlSdkFeed::v210Data() const { return nullptr; } 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