From 9e144c8745e8387f909523cc06266a8767f7a8a7 Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Wed, 9 Sep 2026 15:06:37 +0200 Subject: [PATCH 1/5] Add MXL demuxer/muxer patch on top of the FFmpeg patch stack Adds 0008-avformat-libmxl-demuxer-muxer.patch (squashed from 32 upstream commits on cbcrc/FFmpeg branch dmf-mxl/master, pinned at 5c5d593), plus a Docker-based generator that rebuilds the patch from the fork onto n7.1.5 for reproducibility. verify.sh is bumped to expect 8 patches and the new post-apply tree hash f4eebb6a255d1f4b76eb795885102d1cfea8cd08. The generator lives at deps/ffmpeg-patches/Dockerfile.mkpatch and picks MXL commits by (a) files under libavformat/mxl* and (b) commit subjects containing "mxl", then cherry-picks with -X theirs onto n7.1.5 + the existing 0001-0007 stack. mkpatch-finish.sh squashes the picks into a single provenance-preserving patch with Co-authored-by trailers. Co-Authored-By: Claude Opus 4.7 --- .../0008-avformat-libmxl-demuxer-muxer.patch | 14061 ++++++++++++++++ deps/ffmpeg-patches/Dockerfile.mkpatch | 32 + deps/ffmpeg-patches/README.md | 56 +- deps/ffmpeg-patches/mkpatch-0008-mxl.sh | 148 + deps/ffmpeg-patches/mkpatch-finish.sh | 63 + deps/ffmpeg-patches/verify.sh | 4 +- 6 files changed, 14360 insertions(+), 4 deletions(-) create mode 100644 deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch create mode 100644 deps/ffmpeg-patches/Dockerfile.mkpatch create mode 100755 deps/ffmpeg-patches/mkpatch-0008-mxl.sh create mode 100755 deps/ffmpeg-patches/mkpatch-finish.sh diff --git a/deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch b/deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch new file mode 100644 index 00000000..f1ff4e5b --- /dev/null +++ b/deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch @@ -0,0 +1,14061 @@ +From 71fa35cc23be263c222e18f4605c22fc3977d759 Mon Sep 17 00:00:00 2001 +From: avplumber mxl builder +Date: Tue, 8 Sep 2026 23:55:47 +0000 +Subject: [PATCH] avformat/mxl: add libmxl demuxer and muxer + +Adds the MXL demuxer, muxer, and --enable-libmxl configure glue. +Cherry-picked from https://github.com/cbcrc/FFmpeg (branch +dmf-mxl/master) and rebased onto n7.1.5 on top of the existing +avplumber patch stack (0001-0007). + +Original commits: + * 11bcd59ca5 update to match mxl v1.0.0 error code + * 83dec7e900 enable fate-mxl-uri test + * cfed343ac8 add missing mxl swresample dep + * b4ab137167 update mxl-encdec ref output to match lavf version + * ca9e02bc8f add raw audio dump option for demux debug + * d24ccff2f0 fix audio pts jump log message + * 22c95f4bb1 add monotonic pts sanity check and pts jump log + * 93e2f6156a add execute duration to diag messsage + * fc7aac61e6 diag audio tail_index fix + * 059acae971 fix head tail arg position + * 5c7f743a64 move client only message helpers into test + * 635687e441 MXL monitoring and diagnostic interface + * de1e1b7c5f lower log level of per good frame log message in muxer to debug + * cfcc2e8da3 adjust log level, too late to verbose, too early to debug + * 2bce944b50 normalize log messages + * b4de667288 minor json parser improvements + * 5b4182dc74 increased error log validation + * 0fe62d2895 fix validation access to unitialized stream context + * 10ece046f9 MXL demuxer: support multiple flows per instance + * 8b125e2872 refactor MXL demuxer filesystem path parsing + * 7755b8c903 MXL URI parser intergration + * 37fb2465a5 add MXL scheme prefix check + * 1a292ddd90 MXL URI parser with validation and tests + * 846ed688aa c99 compliance + * 7c1171f30d use unescaped_media_type value for stream metadata + * d6f2afdb02 unescape json media_type + * b4e2192ee5 update to flow api changes + * 6f79915b50 MXL audio support + * 29e27173fe update to MXL commit hash b876396 + * 7f76f34ae5 remove lingering dev comments + * e9cdf41eac change copyright notice to short form legal name + * 71ce604453 Add initial MXL muxer and demuxer support + +Co-authored-by: jpt +Co-authored-by: jpt +--- + configure | 13 +- + libavformat/Makefile | 5 + + libavformat/allformats.c | 2 + + libavformat/jsmn.c | 27 + + libavformat/jsmn.h | 468 ++++++++ + libavformat/mxl_common.h | 29 + + libavformat/mxl_diag.c | 379 ++++++ + libavformat/mxl_diag.h | 227 ++++ + libavformat/mxl_dump.c | 92 ++ + libavformat/mxl_dump.h | 36 + + libavformat/mxl_flow_def.h | 272 +++++ + libavformat/mxl_json.c | 449 +++++++ + libavformat/mxl_json.h | 225 ++++ + libavformat/mxl_loc.c | 217 ++++ + libavformat/mxl_loc.h | 42 + + libavformat/mxl_log.h | 67 ++ + libavformat/mxl_status.h | 59 + + libavformat/mxl_uri.c | 304 +++++ + libavformat/mxl_uri.h | 118 ++ + libavformat/mxldec.c | 1975 +++++++++++++++++++++++++++++++ + libavformat/mxlenc.c | 893 ++++++++++++++ + libavformat/tests/mxl_diag.c | 1471 +++++++++++++++++++++++ + libavformat/tests/mxl_json.c | 1076 +++++++++++++++++ + libavformat/tests/mxl_loc.c | 587 +++++++++ + libavformat/tests/mxl_uri.c | 1393 ++++++++++++++++++++++ + tests/Makefile | 4 + + tests/fate/mxl.mak | 240 ++++ + tests/ref/fate/mxl-audio-encdec | 15 + + tests/ref/fate/mxl-audio-probe | 9 + + tests/ref/fate/mxl-av-encdec | 25 + + tests/ref/fate/mxl-av-probe | 17 + + tests/ref/fate/mxl-bad-domain | 8 + + tests/ref/fate/mxl-bad-flow | 8 + + tests/ref/fate/mxl-bad-loc | 43 + + tests/ref/fate/mxl-diag | 82 ++ + tests/ref/fate/mxl-json | 1041 ++++++++++++++++ + tests/ref/fate/mxl-loc | 564 +++++++++ + tests/ref/fate/mxl-uri | 1138 ++++++++++++++++++ + tests/ref/fate/mxl-video-encdec | 15 + + tests/ref/fate/mxl-video-probe | 10 + + 40 files changed, 13644 insertions(+), 1 deletion(-) + create mode 100644 libavformat/jsmn.c + create mode 100644 libavformat/jsmn.h + create mode 100644 libavformat/mxl_common.h + create mode 100644 libavformat/mxl_diag.c + create mode 100644 libavformat/mxl_diag.h + create mode 100644 libavformat/mxl_dump.c + create mode 100644 libavformat/mxl_dump.h + create mode 100644 libavformat/mxl_flow_def.h + create mode 100644 libavformat/mxl_json.c + create mode 100644 libavformat/mxl_json.h + create mode 100644 libavformat/mxl_loc.c + create mode 100644 libavformat/mxl_loc.h + create mode 100644 libavformat/mxl_log.h + create mode 100644 libavformat/mxl_status.h + create mode 100644 libavformat/mxl_uri.c + create mode 100644 libavformat/mxl_uri.h + create mode 100644 libavformat/mxldec.c + create mode 100644 libavformat/mxlenc.c + create mode 100644 libavformat/tests/mxl_diag.c + create mode 100644 libavformat/tests/mxl_json.c + create mode 100644 libavformat/tests/mxl_loc.c + create mode 100644 libavformat/tests/mxl_uri.c + create mode 100644 tests/fate/mxl.mak + create mode 100644 tests/ref/fate/mxl-audio-encdec + create mode 100644 tests/ref/fate/mxl-audio-probe + create mode 100644 tests/ref/fate/mxl-av-encdec + create mode 100644 tests/ref/fate/mxl-av-probe + create mode 100644 tests/ref/fate/mxl-bad-domain + create mode 100644 tests/ref/fate/mxl-bad-flow + create mode 100644 tests/ref/fate/mxl-bad-loc + create mode 100644 tests/ref/fate/mxl-diag + create mode 100644 tests/ref/fate/mxl-json + create mode 100644 tests/ref/fate/mxl-loc + create mode 100644 tests/ref/fate/mxl-uri + create mode 100644 tests/ref/fate/mxl-video-encdec + create mode 100644 tests/ref/fate/mxl-video-probe + +diff --git a/configure b/configure +index 5ae4594586..e4382adc3b 100755 +--- a/configure ++++ b/configure +@@ -1938,6 +1938,7 @@ EXTERNAL_LIBRARY_LIST=" + liblcevc_dec + libmodplug + libmp3lame ++ libmxl + libmysofa + libopencv + libopenh264 +@@ -3600,6 +3601,8 @@ libxevd_decoder_deps="libxevd" + libxeve_encoder_deps="libxeve" + libxvid_encoder_deps="libxvid" + libzvbi_teletext_decoder_deps="libzvbi" ++mxl_muxer_deps="libmxl swresample" ++mxl_demuxer_deps="libmxl swresample" + vapoursynth_demuxer_deps="vapoursynth" + videotoolbox_suggest="coreservices" + videotoolbox_deps="corefoundation coremedia corevideo" +@@ -6986,6 +6989,8 @@ fi + + enabled libmodplug && require_pkg_config libmodplug libmodplug libmodplug/modplug.h ModPlug_Load + enabled libmp3lame && require "libmp3lame >= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs ++enabled libmpeghdec && require_pkg_config libmpeghdec "mpeghdec >= 3.0.0" mpeghdec/mpeghdecoder.h mpeghdecoder_init ++enabled libmxl && require_pkg_config libmxl libmxl mxl/mxl.h mxlCreateInstance + enabled libmysofa && { check_pkg_config libmysofa libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine || + require libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; } + enabled libnpp && { check_lib libnpp npp.h nppGetLibVersion -lnppig -lnppicc -lnppc -lnppidei -lnppif || +@@ -7105,7 +7110,13 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co + add_ldflags -L/opt/vc/lib/ && + check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } || + die "ERROR: mmal not found" && +- check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } ++ check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } ++enabled mxl_demuxer && prepend avformat_deps "swresample" ++enabled mxl_muxer && prepend avformat_deps "swresample" ++enabled ohcodec && { check_lib ohcodec "multimedia/player_framework/native_avcodec_videodecoder.h multimedia/player_framework/native_avcodec_videoencoder.h" \ ++ "OH_VideoDecoder_CreateByName OH_VideoEncoder_CreateByName" \ ++ -lnative_media_vdec -lnative_media_venc -lnative_media_codecbase -lnative_media_core -lnative_window || ++ die "ERROR: missing native_media libs"; } + enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" alGetError || + { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do + check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } || +diff --git a/libavformat/Makefile b/libavformat/Makefile +index 7ca68a7036..8b2568b782 100644 +--- a/libavformat/Makefile ++++ b/libavformat/Makefile +@@ -660,6 +660,10 @@ OBJS-$(CONFIG_LIBMODPLUG_DEMUXER) += libmodplug.o + OBJS-$(CONFIG_LIBOPENMPT_DEMUXER) += libopenmpt.o + OBJS-$(CONFIG_VAPOURSYNTH_DEMUXER) += vapoursynth.o + ++# MXL demuxer/muxer ++OBJS-$(CONFIG_MXL_DEMUXER) += mxldec.o mxl_dump.o mxl_diag.o mxl_uri.o mxl_loc.o mxl_json.o jsmn.o ++OBJS-$(CONFIG_MXL_MUXER) += mxlenc.o mxl_diag.o mxl_json.o jsmn.o ++ + # protocols I/O + OBJS-$(CONFIG_ANDROID_CONTENT_PROTOCOL) += file.o + OBJS-$(CONFIG_ASYNC_PROTOCOL) += async.o +@@ -765,6 +769,7 @@ TESTPROGS-$(CONFIG_MOV_MUXER) += movenc + TESTPROGS-$(CONFIG_NETWORK) += noproxy + TESTPROGS-$(CONFIG_SRTP) += srtp + TESTPROGS-$(CONFIG_IMF_DEMUXER) += imf ++TESTPROGS-$(CONFIG_MXL_DEMUXER) += mxl_diag mxl_uri mxl_loc mxl_json + + TOOLS = aviocat \ + ismindex \ +diff --git a/libavformat/allformats.c b/libavformat/allformats.c +index 305fa46532..952e9ba2a5 100644 +--- a/libavformat/allformats.c ++++ b/libavformat/allformats.c +@@ -568,6 +568,8 @@ extern const FFInputFormat ff_image_webp_pipe_demuxer; + extern const FFInputFormat ff_image_xbm_pipe_demuxer; + extern const FFInputFormat ff_image_xpm_pipe_demuxer; + extern const FFInputFormat ff_image_xwd_pipe_demuxer; ++extern const FFInputFormat ff_mxl_demuxer; ++extern const FFOutputFormat ff_mxl_muxer; + + /* external libraries */ + extern const FFOutputFormat ff_chromaprint_muxer; +diff --git a/libavformat/jsmn.c b/libavformat/jsmn.c +new file mode 100644 +index 0000000000..e36effacfa +--- /dev/null ++++ b/libavformat/jsmn.c +@@ -0,0 +1,27 @@ ++/* ++ * MXL JSON parser for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * jsmn implementation file ++ * see: https://github.com/zserge/jsmn/blob/v1.1.0/README.md#usage ++ */ ++ ++#include "jsmn.h" +diff --git a/libavformat/jsmn.h b/libavformat/jsmn.h +new file mode 100644 +index 0000000000..b95368a206 +--- /dev/null ++++ b/libavformat/jsmn.h +@@ -0,0 +1,468 @@ ++/* ++ * MIT License ++ * ++ * Copyright (c) 2010 Serge Zaitsev ++ * ++ * Permission is hereby granted, free of charge, to any person obtaining a copy ++ * of this software and associated documentation files (the "Software"), to deal ++ * in the Software without restriction, including without limitation the rights ++ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ++ * copies of the Software, and to permit persons to whom the Software is ++ * furnished to do so, subject to the following conditions: ++ * ++ * The above copyright notice and this permission notice shall be included in ++ * all copies or substantial portions of the Software. ++ * ++ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ++ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ++ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ++ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ++ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE ++ * SOFTWARE. ++ */ ++#ifndef JSMN_H ++#define JSMN_H ++ ++#include ++ ++#ifdef __cplusplus ++extern "C" { ++#endif ++ ++#ifdef JSMN_STATIC ++#define JSMN_API static ++#else ++#define JSMN_API extern ++#endif ++ ++/** ++ * JSON type identifier. Basic types are: ++ * o Object ++ * o Array ++ * o String ++ * o Other primitive: number, boolean (true/false) or null ++ */ ++typedef enum { ++ JSMN_UNDEFINED = 0, ++ JSMN_OBJECT = 1, ++ JSMN_ARRAY = 2, ++ JSMN_STRING = 3, ++ JSMN_PRIMITIVE = 4 ++} jsmntype_t; ++ ++enum jsmnerr { ++ /* Not enough tokens were provided */ ++ JSMN_ERROR_NOMEM = -1, ++ /* Invalid character inside JSON string */ ++ JSMN_ERROR_INVAL = -2, ++ /* The string is not a full JSON packet, more bytes expected */ ++ JSMN_ERROR_PART = -3 ++}; ++ ++/** ++ * JSON token description. ++ * type type (object, array, string etc.) ++ * start start position in JSON data string ++ * end end position in JSON data string ++ */ ++typedef struct { ++ jsmntype_t type; ++ int start; ++ int end; ++ int size; ++#ifdef JSMN_PARENT_LINKS ++ int parent; ++#endif ++} jsmntok_t; ++ ++/** ++ * JSON parser. Contains an array of token blocks available. Also stores ++ * the string being parsed now and current position in that string. ++ */ ++typedef struct { ++ unsigned int pos; /* offset in the JSON string */ ++ unsigned int toknext; /* next token to allocate */ ++ int toksuper; /* superior token node, e.g. parent object or array */ ++} jsmn_parser; ++ ++/** ++ * Create JSON parser over an array of tokens ++ */ ++JSMN_API void jsmn_init(jsmn_parser *parser); ++ ++/** ++ * Run JSON parser. It parses a JSON data string into and array of tokens, each ++ * describing ++ * a single JSON object. ++ */ ++JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, ++ jsmntok_t *tokens, const unsigned int num_tokens); ++ ++#ifndef JSMN_HEADER ++/** ++ * Allocates a fresh unused token from the token pool. ++ */ ++static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, ++ const size_t num_tokens) { ++ jsmntok_t *tok; ++ if (parser->toknext >= num_tokens) { ++ return NULL; ++ } ++ tok = &tokens[parser->toknext++]; ++ tok->start = tok->end = -1; ++ tok->size = 0; ++#ifdef JSMN_PARENT_LINKS ++ tok->parent = -1; ++#endif ++ return tok; ++} ++ ++/** ++ * Fills token type and boundaries. ++ */ ++static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, ++ const int start, const int end) { ++ token->type = type; ++ token->start = start; ++ token->end = end; ++ token->size = 0; ++} ++ ++/** ++ * Fills next available token with JSON primitive. ++ */ ++static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, ++ const size_t len, jsmntok_t *tokens, ++ const size_t num_tokens) { ++ jsmntok_t *token; ++ int start; ++ ++ start = parser->pos; ++ ++ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { ++ switch (js[parser->pos]) { ++#ifndef JSMN_STRICT ++ /* In strict mode primitive must be followed by "," or "}" or "]" */ ++ case ':': ++#endif ++ case '\t': ++ case '\r': ++ case '\n': ++ case ' ': ++ case ',': ++ case ']': ++ case '}': ++ goto found; ++ } ++ if (js[parser->pos] < 32 || js[parser->pos] >= 127) { ++ parser->pos = start; ++ return JSMN_ERROR_INVAL; ++ } ++ } ++#ifdef JSMN_STRICT ++ /* In strict mode primitive must be followed by a comma/object/array */ ++ parser->pos = start; ++ return JSMN_ERROR_PART; ++#endif ++ ++found: ++ if (tokens == NULL) { ++ parser->pos--; ++ return 0; ++ } ++ token = jsmn_alloc_token(parser, tokens, num_tokens); ++ if (token == NULL) { ++ parser->pos = start; ++ return JSMN_ERROR_NOMEM; ++ } ++ jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); ++#ifdef JSMN_PARENT_LINKS ++ token->parent = parser->toksuper; ++#endif ++ parser->pos--; ++ return 0; ++} ++ ++/** ++ * Fills next token with JSON string. ++ */ ++static int jsmn_parse_string(jsmn_parser *parser, const char *js, ++ const size_t len, jsmntok_t *tokens, ++ const size_t num_tokens) { ++ jsmntok_t *token; ++ ++ int start = parser->pos; ++ ++ parser->pos++; ++ ++ /* Skip starting quote */ ++ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { ++ char c = js[parser->pos]; ++ ++ /* Quote: end of string */ ++ if (c == '\"') { ++ if (tokens == NULL) { ++ return 0; ++ } ++ token = jsmn_alloc_token(parser, tokens, num_tokens); ++ if (token == NULL) { ++ parser->pos = start; ++ return JSMN_ERROR_NOMEM; ++ } ++ jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); ++#ifdef JSMN_PARENT_LINKS ++ token->parent = parser->toksuper; ++#endif ++ return 0; ++ } ++ ++ /* Backslash: Quoted symbol expected */ ++ if (c == '\\' && parser->pos + 1 < len) { ++ int i; ++ parser->pos++; ++ switch (js[parser->pos]) { ++ /* Allowed escaped symbols */ ++ case '\"': ++ case '/': ++ case '\\': ++ case 'b': ++ case 'f': ++ case 'r': ++ case 'n': ++ case 't': ++ break; ++ /* Allows escaped symbol \uXXXX */ ++ case 'u': ++ parser->pos++; ++ for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; ++ i++) { ++ /* If it isn't a hex character we have an error */ ++ if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ ++ (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ ++ (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ ++ parser->pos = start; ++ return JSMN_ERROR_INVAL; ++ } ++ parser->pos++; ++ } ++ parser->pos--; ++ break; ++ /* Unexpected symbol */ ++ default: ++ parser->pos = start; ++ return JSMN_ERROR_INVAL; ++ } ++ } ++ } ++ parser->pos = start; ++ return JSMN_ERROR_PART; ++} ++ ++/** ++ * Parse JSON string and fill tokens. ++ */ ++JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, ++ jsmntok_t *tokens, const unsigned int num_tokens) { ++ int r; ++ int i; ++ jsmntok_t *token; ++ int count = parser->toknext; ++ ++ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { ++ char c; ++ jsmntype_t type; ++ ++ c = js[parser->pos]; ++ switch (c) { ++ case '{': ++ case '[': ++ count++; ++ if (tokens == NULL) { ++ break; ++ } ++ token = jsmn_alloc_token(parser, tokens, num_tokens); ++ if (token == NULL) { ++ return JSMN_ERROR_NOMEM; ++ } ++ if (parser->toksuper != -1) { ++ jsmntok_t *t = &tokens[parser->toksuper]; ++#ifdef JSMN_STRICT ++ /* In strict mode an object or array can't become a key */ ++ if (t->type == JSMN_OBJECT) { ++ return JSMN_ERROR_INVAL; ++ } ++#endif ++ t->size++; ++#ifdef JSMN_PARENT_LINKS ++ token->parent = parser->toksuper; ++#endif ++ } ++ token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); ++ token->start = parser->pos; ++ parser->toksuper = parser->toknext - 1; ++ break; ++ case '}': ++ case ']': ++ if (tokens == NULL) { ++ break; ++ } ++ type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); ++#ifdef JSMN_PARENT_LINKS ++ if (parser->toknext < 1) { ++ return JSMN_ERROR_INVAL; ++ } ++ token = &tokens[parser->toknext - 1]; ++ for (;;) { ++ if (token->start != -1 && token->end == -1) { ++ if (token->type != type) { ++ return JSMN_ERROR_INVAL; ++ } ++ token->end = parser->pos + 1; ++ parser->toksuper = token->parent; ++ break; ++ } ++ if (token->parent == -1) { ++ if (token->type != type || parser->toksuper == -1) { ++ return JSMN_ERROR_INVAL; ++ } ++ break; ++ } ++ token = &tokens[token->parent]; ++ } ++#else ++ for (i = parser->toknext - 1; i >= 0; i--) { ++ token = &tokens[i]; ++ if (token->start != -1 && token->end == -1) { ++ if (token->type != type) { ++ return JSMN_ERROR_INVAL; ++ } ++ parser->toksuper = -1; ++ token->end = parser->pos + 1; ++ break; ++ } ++ } ++ /* Error if unmatched closing bracket */ ++ if (i == -1) { ++ return JSMN_ERROR_INVAL; ++ } ++ for (; i >= 0; i--) { ++ token = &tokens[i]; ++ if (token->start != -1 && token->end == -1) { ++ parser->toksuper = i; ++ break; ++ } ++ } ++#endif ++ break; ++ case '\"': ++ r = jsmn_parse_string(parser, js, len, tokens, num_tokens); ++ if (r < 0) { ++ return r; ++ } ++ count++; ++ if (parser->toksuper != -1 && tokens != NULL) { ++ tokens[parser->toksuper].size++; ++ } ++ break; ++ case '\t': ++ case '\r': ++ case '\n': ++ case ' ': ++ break; ++ case ':': ++ parser->toksuper = parser->toknext - 1; ++ break; ++ case ',': ++ if (tokens != NULL && parser->toksuper != -1 && ++ tokens[parser->toksuper].type != JSMN_ARRAY && ++ tokens[parser->toksuper].type != JSMN_OBJECT) { ++#ifdef JSMN_PARENT_LINKS ++ parser->toksuper = tokens[parser->toksuper].parent; ++#else ++ for (i = parser->toknext - 1; i >= 0; i--) { ++ if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { ++ if (tokens[i].start != -1 && tokens[i].end == -1) { ++ parser->toksuper = i; ++ break; ++ } ++ } ++ } ++#endif ++ } ++ break; ++#ifdef JSMN_STRICT ++ /* In strict mode primitives are: numbers and booleans */ ++ case '-': ++ case '0': ++ case '1': ++ case '2': ++ case '3': ++ case '4': ++ case '5': ++ case '6': ++ case '7': ++ case '8': ++ case '9': ++ case 't': ++ case 'f': ++ case 'n': ++ /* And they must not be keys of the object */ ++ if (tokens != NULL && parser->toksuper != -1) { ++ const jsmntok_t *t = &tokens[parser->toksuper]; ++ if (t->type == JSMN_OBJECT || ++ (t->type == JSMN_STRING && t->size != 0)) { ++ return JSMN_ERROR_INVAL; ++ } ++ } ++#else ++ /* In non-strict mode every unquoted value is a primitive */ ++ default: ++#endif ++ r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); ++ if (r < 0) { ++ return r; ++ } ++ count++; ++ if (parser->toksuper != -1 && tokens != NULL) { ++ tokens[parser->toksuper].size++; ++ } ++ break; ++ ++#ifdef JSMN_STRICT ++ /* Unexpected char in strict mode */ ++ default: ++ return JSMN_ERROR_INVAL; ++#endif ++ } ++ } ++ ++ if (tokens != NULL) { ++ for (i = parser->toknext - 1; i >= 0; i--) { ++ /* Unmatched opened object or array */ ++ if (tokens[i].start != -1 && tokens[i].end == -1) { ++ return JSMN_ERROR_PART; ++ } ++ } ++ } ++ ++ return count; ++} ++ ++/** ++ * Creates a new parser based over a given buffer with an array of tokens ++ * available. ++ */ ++JSMN_API void jsmn_init(jsmn_parser *parser) { ++ parser->pos = 0; ++ parser->toknext = 0; ++ parser->toksuper = -1; ++} ++ ++#endif /* JSMN_HEADER */ ++ ++#ifdef __cplusplus ++} ++#endif ++ ++#endif /* JSMN_H */ +diff --git a/libavformat/mxl_common.h b/libavformat/mxl_common.h +new file mode 100644 +index 0000000000..baf8877e39 +--- /dev/null ++++ b/libavformat/mxl_common.h +@@ -0,0 +1,29 @@ ++/* ++ * MXL commond definitions for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef AVFORMAT_MXL_COMMON_H ++#define AVFORMAT_MXL_COMMON_H ++ ++#define MXL_FLOW_EXT "mxl-flow" ++#define MXL_DOT_FLOW_EXT ".mxl-flow" ++#define MXL_LONG_NAME "Media eXchange Layer" ++ ++#endif /* AVFORMAT_MXL_COMMON_H */ +diff --git a/libavformat/mxl_diag.c b/libavformat/mxl_diag.c +new file mode 100644 +index 0000000000..e056d7887c +--- /dev/null ++++ b/libavformat/mxl_diag.c +@@ -0,0 +1,379 @@ ++/* ++ * MXL runtime diagnostics for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "mxl_diag.h" ++#include "mxl_log.h" ++ ++#include "libavutil/avassert.h" ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++static const char *msg_type_to_string(enum mxl_diag_msg_type type) ++{ ++ switch (type) { ++ case MXL_DIAG_MSG_UNDEFINED: ++ return "UNDEFINED"; ++ case MXL_DIAG_MSG_CONNECT: ++ return "CONNECT"; ++ case MXL_DIAG_MSG_RELEASE: ++ return "RELEASE"; ++ case MXL_DIAG_MSG_VIDEO_READ: ++ return "VIDEO_READ"; ++ case MXL_DIAG_MSG_AUDIO_READ: ++ return "AUDIO_READ"; ++ default: ++ return "UNKNOWN"; ++ } ++} ++ ++static int set_nonblocking(int fd) ++{ ++ int flags = fcntl(fd, F_GETFL, 0); ++ if (flags < 0) { ++ return AVERROR(errno); ++ } ++ if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { ++ return AVERROR(errno); ++ } ++ return 0; ++} ++ ++static int is_path_socket(const char *path) ++{ ++ struct stat st; ++ if (lstat(path, &st) < 0) { ++ return 0; ++ } ++ return S_ISSOCK(st.st_mode); ++} ++ ++static int is_same_addr(const struct sockaddr_un *a, socklen_t a_len, ++ const struct sockaddr_un *b, socklen_t b_len) ++{ ++ if (a_len != b_len) { ++ return 0; ++ } ++ return memcmp(a, b, (size_t)a_len) == 0; ++} ++ ++static void clear_client(mxl_diag_server *s) ++{ ++ memset(&s->client_addr, 0, sizeof(s->client_addr)); ++ s->client_addr_len = 0; ++ s->has_client = 0; ++} ++ ++static int validate_path(const char *path) ++{ ++ size_t len; ++ ++ if (!path || !*path) { ++ return AVERROR(EINVAL); ++ } ++ ++ len = strlen(path); ++ if (len >= sizeof(((struct sockaddr_un *)0)->sun_path)) { ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ return 0; ++} ++ ++int mxl_diag_init(void *logctx, mxl_diag_server *server, const char *server_path) ++{ ++ (void)logctx; ++ struct sockaddr_un addr; ++ int fd = -1; ++ int ret; ++ ++ if (!server) ++ return AVERROR(EINVAL); ++ ++ memset(server, 0, sizeof(*server)); ++ server->fd = -1; ++ ++ ret = validate_path(server_path); ++ if (ret < 0) { ++ return ret; ++ } ++ ++ // must not exist, or must be a socket ++ if (access(server_path, F_OK) == 0) { ++ if (!is_path_socket(server_path)) { ++ return AVERROR(EEXIST); ++ } ++ if (unlink(server_path) < 0) { ++ return AVERROR(errno); ++ } ++ } else if (errno != ENOENT) { ++ return AVERROR(errno); ++ } ++ ++ fd = socket(AF_UNIX, SOCK_DGRAM, 0); ++ if (fd < 0) { ++ return AVERROR(errno); ++ } ++ ++ ret = set_nonblocking(fd); ++ if (ret < 0) { ++ close(fd); ++ return ret; ++ } ++ ++ memset(&addr, 0, sizeof(addr)); ++ addr.sun_family = AF_UNIX; ++ strcpy(addr.sun_path, server_path); ++ ++ if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { ++ ret = AVERROR(errno); ++ close(fd); ++ return ret; ++ } ++ ++ server->fd = fd; ++ strcpy(server->server_path, server_path); ++ clear_client(server); ++ ++ return 0; ++} ++ ++void mxl_diag_close(void *logctx, mxl_diag_server *server) ++{ ++ (void)logctx; ++ ++ if (!server) ++ return; ++ ++ if (server->fd >= 0) { ++ close(server->fd); ++ server->fd = -1; ++ } ++ ++ if (server->server_path[0] != '\0') { ++ unlink(server->server_path); ++ server->server_path[0] = '\0'; ++ } ++ ++ clear_client(server); ++} ++ ++int mxl_diag_poll(void *logctx, mxl_diag_server *server) ++{ ++ mxl_diag_msg msg; ++ struct sockaddr_un from_addr; ++ socklen_t from_addr_len = (socklen_t)sizeof(from_addr); ++ int same_sender = 0; ++ ssize_t n; ++ ++ if (!server || server->fd < 0) ++ return AVERROR(EINVAL); ++ ++ memset(&msg, 0, sizeof(msg)); ++ memset(&from_addr, 0, sizeof(from_addr)); ++ ++ struct iovec iov = { ++ .iov_base = &msg, ++ .iov_len = sizeof(msg), ++ }; ++ struct msghdr mh = { ++ .msg_name = &from_addr, ++ .msg_namelen = from_addr_len, ++ .msg_iov = &iov, ++ .msg_iovlen = 1, ++ .msg_control = NULL, ++ .msg_controllen = 0, ++ .msg_flags = 0, ++ }; ++ ++ n = recvmsg(server->fd, &mh, 0); ++ from_addr_len = mh.msg_namelen; ++ ++ // system error ++ if (n < 0) { ++ // transient, retry later ++ if (EAGAIN == errno || EWOULDBLOCK == errno || EINTR == errno) ++ return 0; ++ ++ logd(logctx, "DIAG server recvmsg error: %s\n", av_err2str(AVERROR(errno))); ++ return AVERROR(errno); ++ } ++ ++ if (mh.msg_flags & MSG_TRUNC) { ++ logd(logctx, "DIAG server datagram was truncated\n"); ++ return AVERROR(MXL_DIAG_EDATATRUNCATED); ++ } ++ ++ // data size smaller than sizeof(msg.header) ++ if ((size_t)n < sizeof(msg.header)) { ++ logd(logctx, "DIAG server message is smaller than sizeof(msg.header)\n"); ++ return AVERROR(MXL_DIAG_EDATATOOSMALL); ++ } ++ ++ // header.size smaller than sizeof(msg.header) ++ if (msg.header.size < (uint16_t)sizeof(msg.header)) { ++ logd(logctx, "DIAG header.size is smaller than sizeof(msg.header)\n"); ++ return AVERROR(MXL_DIAG_ESIZETOOSMALL); ++ } ++ ++ // header.size bigger than largest message ++ if (msg.header.size > (uint16_t)sizeof(msg)) { ++ logd(logctx, "DIAG server message header.size is larger than " ++ "maximum possible message size\n"); ++ return AVERROR(MXL_DIAG_ESIZETOOBIG); ++ } ++ ++ // datagram size vs header.size mismatch ++ if ((uint16_t)n != msg.header.size) { ++ logd(logctx, "DIAG server message size mismatch\n"); ++ return AVERROR(MXL_DIAG_ESIZEMISMATCH); ++ } ++ ++ // header.version bad ++ if (msg.header.version != MXL_DIAG_MSG_VERSION) { ++ logd(logctx, "DIAG server message bad version: %d\n", msg.header.version); ++ return AVERROR(MXL_DIAG_EBADVERSION); ++ } ++ ++ if (server->has_client) { ++ same_sender = is_same_addr(&from_addr, from_addr_len, ++ &server->client_addr, server->client_addr_len); ++ } ++ ++ logd(logctx, "DIAG server received control message: %s\n", ++ msg_type_to_string(msg.header.type)); ++ ++ switch (msg.header.type) { ++ case MXL_DIAG_MSG_CONNECT: ++ ++ if ((size_t)n != sizeof(msg.header)) { ++ logd(logctx, "DIAG connect message size mismatch (%d != %d)\n", n, sizeof(msg.header)); ++ return AVERROR(MXL_DIAG_EMSGSIZEMISMATCH); ++ } ++ ++ if (same_sender) { ++ logd(logctx, "DIAG server duplicate connect from client '%s'\n", ++ from_addr.sun_path); ++ return 1; ++ } ++ ++ if (server->has_client) { ++ logd(logctx, "DIAG server ignored registration request from client '%s'\n", ++ from_addr.sun_path); ++ return 1; ++ } ++ ++ server->client_addr = from_addr; ++ server->client_addr_len = from_addr_len; ++ server->has_client = 1; ++ ++ logd(logctx, "DIAG server registered client '%s'\n", from_addr.sun_path); ++ ++ return 1; ++ ++ case MXL_DIAG_MSG_RELEASE: ++ if ((size_t)n != sizeof(msg.header)) { ++ logd(logctx, "DIAG release message size mismatch (%d != %d)\n", n, sizeof(msg.header)); ++ return AVERROR(MXL_DIAG_EMSGSIZEMISMATCH); ++ } ++ ++ if (same_sender) { ++ logd(logctx, "DIAG server released client '%s'\n", from_addr.sun_path); ++ clear_client(server); ++ } ++ else ++ logd(logctx, "DIAG server ignored release from an unregistered client '%s'\n", ++ from_addr.sun_path); ++ ++ return 1; ++ ++ default: ++ logd(logctx, "DIAG server unknown client message from '%s'\n", from_addr.sun_path); ++ return AVERROR(MXL_DIAG_EUNKNOWN); ++ } ++} ++ ++int mxl_diag_send(void *logctx, mxl_diag_server *server, const mxl_diag_msg *msg) ++{ ++ ssize_t n; ++ ++ if (!server || !msg) { ++ return AVERROR(EINVAL); ++ } ++ ++ if (server->fd < 0) { ++ return AVERROR(EBADF); ++ } ++ ++ // lower bound sanity ++ if ((size_t)msg->header.size < sizeof(msg->header)) { ++ logd(logctx, "DIAG server sendto sanity, header.size is smaller " ++ "than minimum message size\n"); ++ return AVERROR(EINVAL); ++ } ++ ++ // upper bound sanity ++ if (msg->header.size > sizeof(*msg)) { ++ logd(logctx, "DIAG server sendto sanity, header.size is larger" ++ " than maximum message size\n"); ++ return AVERROR(EINVAL); ++ } ++ ++ if (!server->has_client) { ++ return 0; ++ } ++ ++ n = sendto(server->fd, msg, (size_t)msg->header.size, 0, ++ (const struct sockaddr *)&server->client_addr, ++ server->client_addr_len); ++ if (n < 0) { ++ // client disappeared ++ if (ENOENT == errno || ECONNREFUSED == errno) { ++ clear_client(server); ++ return 0; ++ } ++ ++ // transient, retry later ++ if (EAGAIN == errno || EWOULDBLOCK == errno || EINTR == errno) ++ return 0; ++ ++ // an error worth logging ++ logd(logctx, "DIAG server sendto failed: %s\n", strerror(errno)); ++ return AVERROR(errno); ++ } ++ ++ if ((size_t)n != (size_t)msg->header.size) { ++ logd(logctx, "DIAG server send length sanity: expected=%zu actual=%zd\n", ++ (size_t)msg->header.size, n); ++ return AVERROR(EIO); ++ } ++ ++ return 1; ++} +diff --git a/libavformat/mxl_diag.h b/libavformat/mxl_diag.h +new file mode 100644 +index 0000000000..d2ea097792 +--- /dev/null ++++ b/libavformat/mxl_diag.h +@@ -0,0 +1,227 @@ ++/* ++ * MXL runtime diagnostics for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef MXL_DIAG_H ++#define MXL_DIAG_H ++ ++#include "libavutil/avassert.h" ++ ++#include ++ ++#include ++#include ++#include ++#include ++#include ++ ++typedef struct mxl_diag_server { ++ int fd; ++ char server_path[sizeof(((struct sockaddr_un *)0)->sun_path)]; ++ struct sockaddr_un client_addr; ++ socklen_t client_addr_len; ++ int has_client; ++} mxl_diag_server; ++ ++enum mxl_diag_error_type { ++ MXL_DIAG_EDATATOOSMALL = 2001, ++ MXL_DIAG_EDATATRUNCATED, ++ MXL_DIAG_ESIZETOOSMALL, ++ MXL_DIAG_ESIZETOOBIG, ++ MXL_DIAG_ESIZEMISMATCH, ++ MXL_DIAG_EMSGSIZEMISMATCH, ++ MXL_DIAG_EBADVERSION, ++ MXL_DIAG_EUNKNOWN ++}; ++ ++enum { MXL_DIAG_MSG_VERSION = 1 }; ++ ++enum mxl_diag_msg_type { ++ MXL_DIAG_MSG_UNDEFINED = 0, ++ MXL_DIAG_MSG_CONNECT, ++ MXL_DIAG_MSG_RELEASE, ++ MXL_DIAG_MSG_VIDEO_READ, ++ MXL_DIAG_MSG_AUDIO_READ, ++}; ++ ++typedef struct mxl_diag_msg { ++ struct { ++ uint16_t version; ++ uint16_t type; ++ uint16_t size; ++ uint8_t reserved[10]; ++ } header; ++ union { ++ struct { ++ uint64_t timestamp; ++ uint64_t exec_dur; ++ uint64_t tail_index; ++ uint64_t head_index; ++ uint64_t read_index; ++ uint32_t mxl_status; ++ uint8_t reserved[4]; ++ } video_read; ++ struct { ++ uint64_t timestamp; ++ uint64_t exec_dur; ++ uint64_t tail_index; ++ uint64_t head_index; ++ uint64_t read_index; ++ uint32_t read_size; ++ uint32_t mxl_status; ++ } audio_read; ++ } u; ++} mxl_diag_msg; ++ ++#define MXL_DIAG_LAYOUT_CHECK(cond, name) \ ++ typedef char mxl_diag_layout_check_##name[(cond) ? 1 : -1] ++MXL_DIAG_LAYOUT_CHECK(sizeof(((mxl_diag_msg *)0)->header) == 16, header_size_must_be_16); ++MXL_DIAG_LAYOUT_CHECK(sizeof(((mxl_diag_msg *)0)->u.video_read) == 48, video_read_size_must_be_48); ++MXL_DIAG_LAYOUT_CHECK(sizeof(((mxl_diag_msg *)0)->u.audio_read) == 48, audio_read_size_must_be_48); ++MXL_DIAG_LAYOUT_CHECK(sizeof(mxl_diag_msg) == 64, msg_size_must_be_64); ++ ++static inline void mxl_diag_init_msg_video_read( ++ mxl_diag_msg *msg, ++ uint64_t timestamp, ++ uint64_t exec_dur, ++ uint64_t tail_index, ++ uint64_t head_index, ++ uint64_t read_index, ++ mxlStatus mxl_status) ++{ ++ av_assert1(msg); ++ int size = sizeof(msg->header) + sizeof(msg->u.video_read); ++ memset(msg, 0, size); ++ msg->header.version = MXL_DIAG_MSG_VERSION; ++ msg->header.type = MXL_DIAG_MSG_VIDEO_READ; ++ msg->header.size = size; ++ msg->u.video_read.timestamp = timestamp; ++ msg->u.video_read.exec_dur = exec_dur; ++ msg->u.video_read.tail_index = tail_index; ++ msg->u.video_read.head_index = head_index; ++ msg->u.video_read.read_index = read_index; ++ msg->u.video_read.mxl_status = mxl_status; ++} ++ ++static inline void mxl_diag_init_msg_audio_read( ++ mxl_diag_msg *msg, ++ uint64_t timestamp, ++ uint64_t exec_dur, ++ uint64_t tail_index, ++ uint64_t head_index, ++ uint64_t read_index, ++ uint32_t read_size, ++ mxlStatus mxl_status) ++{ ++ av_assert1(msg); ++ int size = sizeof(msg->header) + sizeof(msg->u.audio_read); ++ memset(msg, 0, size); ++ msg->header.version = MXL_DIAG_MSG_VERSION; ++ msg->header.type = MXL_DIAG_MSG_AUDIO_READ; ++ msg->header.size = size; ++ msg->u.audio_read.timestamp = timestamp; ++ msg->u.audio_read.exec_dur = exec_dur; ++ msg->u.audio_read.tail_index = tail_index; ++ msg->u.audio_read.head_index = head_index; ++ msg->u.audio_read.read_index = read_index; ++ msg->u.audio_read.read_size = read_size; ++ msg->u.audio_read.mxl_status = mxl_status; ++} ++ ++/** ++ * Initialize a diagnostic server socket. ++ * ++ * Creates a Unix domain datagram socket at `server_path`, configures it ++ * non-blocking, and prepares it to accept registration from a single client. ++ * If a stale socket file already exists at `server_path`, it is unlinked ++ * before bind. If a non-socket filesystem entry exists there, initialization ++ * fails. ++ * ++ * @param logctx Log context for `av_log()`. May be NULL. ++ * @param server Server object to initialize. ++ * @param server_path Filesystem path for the server Unix datagram socket. ++ * ++ * @return 0 on success, or a negative AVERROR(...) code on failure. ++ */ ++int mxl_diag_init(void *logctx, ++ mxl_diag_server *server, ++ const char *server_path); ++ ++/** ++ * Close a diagnostic server socket and release its resources. ++ * ++ * Closes the server socket, forgets any registered client, and unlinks the ++ * server socket pathname if one is present. ++ * ++ * @param logctx Log context for `av_log()`. May be NULL. ++ * @param server Server object to close. ++ */ ++void mxl_diag_close(void *logctx, ++ mxl_diag_server *server); ++ ++/** ++ * Poll the inbound socket and process at most one datagram. ++ * ++ * Transient errors EAGAIN, EWOULDBLOCK, and EINTR are ignored and ++ * return zero. Other errors are returned as negative AVERROR(...) ++ * values wrapping either a mxl_diag_error_type code or a system ++ * error code. ++ * ++ * @param logctx Log context for `av_log()`. May be NULL. ++ * @param server Server object to poll. ++ * @return 0 If no datagram is available ++ * @return 1 A datagram was processed ++ * @return <0 A malformed-message or system error occurred ++ */ ++int mxl_diag_poll(void *logctx, mxl_diag_server *server); ++ ++/** ++ * Send one diagnostic message to the currently registered client. ++ * ++ * The message size is taken from msg->header.size and transmitted as-is. ++ * The caller must ensure that the message header is initialized correctly ++ * and that header.size reflects the number of bytes to send. ++ * ++ * If no client is currently registered, the message is silently ++ * dropped and 0 is returned. ++ * ++ * Call mxl_diag_poll() periodically to process client connect and ++ * release messages. ++ * ++ * If the client appears to have gone away, its registration is ++ * cleared. ++ * ++ * System error codes ENOENT and ECONNREFUSED are handled, clear the ++ * registered client, and return 0. Transient errors EAGAIN, ++ * EWOULDBLOCK, and EINTR are ignored and return zero. Any other ++ * system error is returned as a negative AVERROR(...) code. ++ * ++ * @param logctx Log context for `av_log()`. May be NULL. ++ * @param server Diagnostic server state. ++ * @param msg Message to send. Must not be NULL. ++ * ++ * @return 1 On successful send. ++ * @return 0 If no client is registered, or if a transient send error ++ * occurred that was handled ++ * @return <0 A send error occurred ++ */ ++int mxl_diag_send(void *logctx, mxl_diag_server *server, const mxl_diag_msg *msg); ++ ++#endif /* MXL_DIAG_H */ +diff --git a/libavformat/mxl_dump.c b/libavformat/mxl_dump.c +new file mode 100644 +index 0000000000..976ea7d4e3 +--- /dev/null ++++ b/libavformat/mxl_dump.c +@@ -0,0 +1,92 @@ ++/* ++ * MXL raw sample dump for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "mxl_dump.h" ++ ++#include ++#include ++#include ++ ++int mxl_dump_init_audio_writer(mxl_dump_audio_writer *writer, const char *path) ++{ ++ int fd; ++ ++ if (!writer || !path) ++ return EINVAL; ++ ++ writer->fd = -1; ++ ++ fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644); ++ if (fd < 0) ++ return errno; ++ ++ writer->fd = fd; ++ return 0; ++} ++ ++int mxl_dump_write_audio(mxl_dump_audio_writer *writer, const uint8_t *data, size_t size) ++{ ++ const uint8_t *p; ++ size_t remaining; ++ ++ if (!writer || writer->fd < 0 || (!data && size > 0)) ++ return EINVAL; ++ ++ p = data; ++ remaining = size; ++ ++ while (remaining > 0) { ++ ssize_t n = write(writer->fd, p, remaining); ++ ++ if (n < 0) { ++ if (errno == EINTR) ++ continue; ++ return errno; ++ } ++ ++ if (n == 0) ++ return EIO; ++ ++ p += n; ++ remaining -= (size_t)n; ++ } ++ ++ return 0; ++} ++ ++int mxl_dump_close_audio_writer(mxl_dump_audio_writer *writer) ++{ ++ int ret; ++ ++ if (!writer) ++ return EINVAL; ++ ++ if (writer->fd < 0) ++ return 0; ++ ++ ret = close(writer->fd); ++ writer->fd = -1; ++ ++ if (ret < 0) ++ return errno; ++ ++ return 0; ++} +diff --git a/libavformat/mxl_dump.h b/libavformat/mxl_dump.h +new file mode 100644 +index 0000000000..b556e27bb7 +--- /dev/null ++++ b/libavformat/mxl_dump.h +@@ -0,0 +1,36 @@ ++/* ++ * MXL raw sample dump for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef MXL_DUMP_H ++#define MXL_DUMP_H ++ ++#include ++#include ++ ++typedef struct mxl_dump_audio_writer { ++ int fd; ++} mxl_dump_audio_writer; ++ ++int mxl_dump_init_audio_writer(mxl_dump_audio_writer *writer, const char *path); ++int mxl_dump_write_audio(mxl_dump_audio_writer *writer, const uint8_t *data, size_t size); ++int mxl_dump_close_audio_writer(mxl_dump_audio_writer *writer); ++ ++#endif +diff --git a/libavformat/mxl_flow_def.h b/libavformat/mxl_flow_def.h +new file mode 100644 +index 0000000000..48f947342c +--- /dev/null ++++ b/libavformat/mxl_flow_def.h +@@ -0,0 +1,272 @@ ++/* ++ * MXL json flow definition for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * See: https://github.com/dmf-mxl/mxl ++ */ ++ ++#ifndef MXL_FLOW_DEF_H ++#define MXL_FLOW_DEF_H ++ ++#include "libavutil/mem.h" ++ ++#include ++#include ++ ++/* ++ * Sample flow definitions for reference ++{ ++ "$copyright": "SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project.", ++ "$license": "SPDX-License-Identifier: Apache-2.0", ++ "description": "MXL Test Flow, 1080p29", ++ "id": "5fbec3b1-1b0f-417d-9059-8b94a47197ed", ++ "tags": { ++ "urn:x-nmos:tag:grouphint/v1.0": [ ++ "Media Function XYZ:Video" ++ ] ++ }, ++ "format": "urn:x-nmos:format:video", ++ "label": "MXL Test Flow, 1080p29", ++ "parents": [], ++ "media_type": "video/v210", ++ "grain_rate": { ++ "numerator": 30000, ++ "denominator": 1001 ++ }, ++ "frame_width": 1920, ++ "frame_height": 1080, ++ "interlace_mode": "progressive", ++ "colorspace": "BT709", ++ "components": [ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ] ++} ++ ++{ ++ "$copyright": "SPDX-FileCopyrightText: 2025 Contributors to the Media eXchange Layer project.", ++ "$license": "SPDX-License-Identifier: Apache-2.0", ++ "description": "MXL Audio Flow", ++ "format": "urn:x-nmos:format:audio", ++ "tags": { ++ "urn:x-nmos:tag:grouphint/v1.0": [ ++ "Media Function XYZ:Audio" ++ ] ++ }, ++ "label": "MXL Audio Flow", ++ "version": "1441812152:154331951", ++ "id": "b3bb5be7-9fe9-4324-a5bb-4c70e1084449", ++ "media_type": "audio/float32", ++ "sample_rate": { ++ "numerator": 48000 ++ }, ++ "channel_count": 2, ++ "bit_depth": 32, ++ "parents": [], ++ "source_id": "2aa143ac-0ab7-4d75-bc32-5c00c13d186f", ++ "device_id": "169feb2c-3fae-42a5-ae2e-f6f8cbce29cf" ++} ++*/ ++ ++typedef struct VideoFlowDefParams { ++ const char *id; ++ int grain_rate_num; ++ int grain_rate_den; ++ int frame_width; ++ int frame_height; ++ int y_width, y_height, y_bit_depth; ++ int cb_width, cb_height, cb_bit_depth; ++ int cr_width, cr_height, cr_bit_depth; ++} VideoFlowDefParams; ++ ++static const char *video_flow_def_fmt = ++"{\n" ++" \"description\": \"FFmpeg video stream\",\n" ++" \"id\": \"%s\",\n" ++" \"tags\": {\n" ++" \"urn:x-nmos:tag:grouphint/v1.0\": [\n" ++" \"Media Function XYZ:Video\"\n" ++" ]\n" ++" },\n" ++" \"format\": \"urn:x-nmos:format:video\",\n" ++" \"label\": \"FFmpeg video stream\",\n" ++" \"parents\": [],\n" ++" \"media_type\": \"video/v210\",\n" ++" \"grain_rate\": {\n" ++" \"numerator\": %d,\n" ++" \"denominator\": %d\n" ++" },\n" ++" \"frame_width\": %d,\n" ++" \"frame_height\": %d,\n" ++" \"interlace_mode\": \"progressive\",\n" ++" \"colorspace\": \"BT709\",\n" ++" \"components\": [\n" ++" {\n" ++" \"name\": \"Y\",\n" ++" \"width\": %d,\n" ++" \"height\": %d,\n" ++" \"bit_depth\": %d\n" ++" },\n" ++" {\n" ++" \"name\": \"Cb\",\n" ++" \"width\": %d,\n" ++" \"height\": %d,\n" ++" \"bit_depth\": %d\n" ++" },\n" ++" {\n" ++" \"name\": \"Cr\",\n" ++" \"width\": %d,\n" ++" \"height\": %d,\n" ++" \"bit_depth\": %d\n" ++" }\n" ++" ]\n" ++"}\n"; ++ ++typedef struct AudioFlowDefParams { ++ const char *id; ++ int sample_rate; ++ int channel_count; ++} AudioFlowDefParams; ++ ++static const char *audio_flow_def_fmt = ++"{\n" ++" \"description\": \"FFmpeg audio stream\",\n" ++" \"id\": \"%s\",\n" ++" \"tags\": {\n" ++" \"urn:x-nmos:tag:grouphint/v1.0\": [\n" ++" \"Media Function XYZ:Audio\"\n" ++" ]\n" ++" },\n" ++" \"format\": \"urn:x-nmos:format:audio\",\n" ++" \"label\": \"FFmpeg audio stream\",\n" ++" \"parents\": [],\n" ++" \"media_type\": \"audio/float32\",\n" ++" \"sample_rate\": {\n" ++" \"numerator\": %d\n" ++" },\n" ++" \"channel_count\": %d,\n" ++" \"bit_depth\": 32\n" ++"}\n"; ++ ++/** ++ * Create an MXL video flow definition. ++ * ++ * Formats a JSON video flow definition using fields from ++ * VideoFlowDefParams p and returns a newly allocated string. The ++ * caller owns the returned string. ++ * ++ * @param p Pointer to a VideoFlowDefParams structure containing all values. ++ * @return Newly allocated null-terminated JSON string, or NULL on failure. ++ */ ++static char *make_video_flow_def(const VideoFlowDefParams *p) ++{ ++ if (!p || !p->id) ++ return NULL; ++ ++ int len = snprintf(NULL, 0, video_flow_def_fmt, ++ p->id, ++ p->grain_rate_num, p->grain_rate_den, ++ p->frame_width, p->frame_height, ++ p->y_width, p->y_height, p->y_bit_depth, ++ p->cb_width, p->cb_height, p->cb_bit_depth, ++ p->cr_width, p->cr_height, p->cr_bit_depth ++ ); ++ ++ if (len < 0) ++ return NULL; ++ ++ size_t buf_size = len+1; ++ char *buf = av_malloc(buf_size); ++ if (!buf) ++ return NULL; ++ ++ int rc = snprintf(buf, buf_size, video_flow_def_fmt, ++ p->id, ++ p->grain_rate_num, p->grain_rate_den, ++ p->frame_width, p->frame_height, ++ p->y_width, p->y_height, p->y_bit_depth, ++ p->cb_width, p->cb_height, p->cb_bit_depth, ++ p->cr_width, p->cr_height, p->cr_bit_depth ++ ); ++ if (rc < 0 || (size_t)rc >= buf_size) { ++ av_free(buf); ++ return NULL; ++ } ++ ++ return buf; ++} ++ ++/** ++ * Create an MXL audio flow definition. ++ * ++ * Formats a JSON audio flow definition using fields from ++ * AudioFlowDefParams p and returns a newly allocated string. The ++ * caller owns the returned string. ++ * ++ * @param p Pointer to an AudioFlowDefParams structure containing all values. ++ * @return Newly allocated null-terminated JSON string, or NULL on failure. ++ */ ++static char *make_audio_flow_def(const AudioFlowDefParams *p) ++{ ++ if (!p || !p->id) ++ return NULL; ++ ++ int len = snprintf(NULL, 0, audio_flow_def_fmt, ++ p->id, ++ p->sample_rate, ++ p->channel_count); ++ if (len < 0) ++ return NULL; ++ ++ size_t buf_size = len+1; ++ char *buf = av_malloc(buf_size); ++ if (!buf) ++ return NULL; ++ ++ int rc = snprintf(buf, buf_size, audio_flow_def_fmt, ++ p->id, ++ p->sample_rate, ++ p->channel_count); ++ if (rc < 0 || (size_t)rc >= buf_size) { ++ av_free(buf); ++ return NULL; ++ } ++ ++ return buf; ++} ++ ++#endif /* MXL_FLOW_DEF_H */ +diff --git a/libavformat/mxl_json.c b/libavformat/mxl_json.c +new file mode 100644 +index 0000000000..602a63dbce +--- /dev/null ++++ b/libavformat/mxl_json.c +@@ -0,0 +1,449 @@ ++/* ++ * MXL JSON parser for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * Wrapper around jsmn (https://github.com/zserge/jsmn). ++ * Depends on vendored jsmn v1.1.0 (see jsmn.h). ++ */ ++ ++#include "mxl_json.h" ++#include "mxl_common.h" ++#include "mxl_log.h" ++ ++#include "libavutil/error.h" ++#include "libavutil/avassert.h" ++#include "libavutil/log.h" ++#include "libavutil/mem.h" ++ ++#include ++#include ++#include ++#include ++ ++// maximum json string length to log on parse error ++enum { MAX_JSON_LOG = 256 }; ++ ++// compare jsmn string token to value ++static int is_token_name_equal(const char *js, const jsmntok_t *t, const char *s) ++{ ++ av_assert1(js); ++ av_assert1(s); ++ ++ if (!t || t->type != JSMN_STRING) ++ return 0; ++ ++ // jsmn parser invariant debug assert and release safety check ++ av_assert1(t->end >= t->start); ++ if (t->end < t->start) ++ return 0; ++ ++ size_t tok_len = (size_t)(t->end - t->start); ++ ++ return strlen(s) == tok_len && ++ strncmp(js + t->start, s, tok_len) == 0; ++} ++ ++static unsigned int last_token_of_subtree(const jsmntok_t *tokens, unsigned int ntok, ++ unsigned int node_idx, unsigned int depth); ++ ++// return the last jsmn token of an object ++static unsigned int last_token_of_object(const jsmntok_t *tokens, unsigned int ntok, ++ unsigned int node_idx, unsigned int depth) ++{ ++ if (node_idx >= ntok || depth > MXL_JSON_MAX_DEPTH) ++ return ntok; ++ ++ jsmntok_t t = tokens[node_idx]; ++ ++ // type sanity ++ av_assert0(t.type == JSMN_OBJECT); ++ ++ int num_children = t.size; ++ for (int child = 0; child < num_children; child++) { ++ // skip ahead to the key ++ node_idx++; ++ ++ // node_idx sanity ++ av_assert0(node_idx < ntok); ++ ++ // key type sanity ++ av_assert0(tokens[node_idx].type == JSMN_STRING); ++ ++ // jump to end of value ++ node_idx = last_token_of_subtree(tokens, ntok, ++ node_idx + 1, ++ depth+1); ++ // depth guard ++ if (node_idx >= ntok) ++ return ntok; ++ } ++ ++ return node_idx; ++} ++ ++// return the last jsmn token of an array ++static unsigned int last_token_of_array(const jsmntok_t *tokens, unsigned int ntok, ++ unsigned int node_idx, unsigned int depth) ++{ ++ if (node_idx >= ntok || depth > MXL_JSON_MAX_DEPTH) ++ return ntok; ++ ++ jsmntok_t t = tokens[node_idx]; ++ ++ // type sanity ++ av_assert0(t.type == JSMN_ARRAY); ++ ++ int num_children = t.size; ++ for (int child = 0; child < num_children; child++) { ++ // jump to end of value ++ node_idx = last_token_of_subtree(tokens, ntok, ++ node_idx + 1, ++ depth+1); ++ // depth guard ++ if (node_idx >= ntok) ++ return ntok; ++ } ++ ++ return node_idx; ++} ++ ++// return the index of the last jsmn token of a json subtree ++static unsigned int last_token_of_subtree(const jsmntok_t *tokens, unsigned int ntok, ++ unsigned int node_idx, unsigned int depth) ++{ ++ if (node_idx >= ntok || depth > MXL_JSON_MAX_DEPTH) ++ return ntok; ++ ++ jsmntok_t t = tokens[node_idx]; ++ ++ switch (t.type) { ++ case JSMN_OBJECT: ++ return last_token_of_object(tokens, ntok, node_idx, depth); ++ case JSMN_ARRAY: ++ return last_token_of_array(tokens, ntok, node_idx, depth); ++ case JSMN_STRING: ++ case JSMN_PRIMITIVE: ++ return node_idx; ++ default: ++ av_assert0(!"unknown jsmn token type"); ++ return ntok; ++ } ++} ++ ++// jsmn token to string value with special case json null detection ++static mxl_json_str jsmn_tok_to_string(const char *json, const jsmntok_t *t) ++{ ++ if (!t) { ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_EBADTYPE }; ++ } ++ ++ // jsmn parser invariant debug check and release safety check ++ av_assert1(t->end >= t->start); ++ if (t->end < t->start) ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_EINTERNAL }; ++ ++ if (t->type != JSMN_STRING) ++ if (t->type == JSMN_PRIMITIVE && ++ t->end - t->start == 4 && ++ !strncmp(json + t->start, "null", 4)) ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_NULL }; ++ else ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_EBADTYPE }; ++ ++ size_t len = t->end - t->start; ++ char *s = (char *)av_malloc(len + 1); ++ if (!s) ++ return (mxl_json_str){ .value = NULL, .err = ENOMEM }; ++ memcpy(s, json + t->start, len); ++ s[len] = '\0'; ++ ++ return (mxl_json_str){ .value = s, .err = 0 }; ++} ++ ++// jsmn token to double value with null, infinity, and strtod errno detection ++static mxl_json_num jsmn_tok_to_double(const char *json, const jsmntok_t *t) ++{ ++ if (!t || t->type != JSMN_PRIMITIVE) ++ return (mxl_json_num){.value = NAN, .err = MXL_JSON_EBADTYPE}; ++ ++ // jsmn parser invariant debug check and release safety check ++ av_assert1(t->end >= t->start); ++ if (t->end < t->start) ++ return (mxl_json_num){.value = NAN, .err = MXL_JSON_EINTERNAL}; ++ ++ char buf[MXL_JSON_MAX_NUMBER_CHARS+1]; ++ size_t len = t->end - t->start; ++ if (len >= sizeof(buf)) ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_UNSUPPORTED}; ++ memcpy(buf, json + t->start, len); ++ buf[len] = '\0'; ++ ++ errno = 0; ++ char *endptr = NULL; ++ double v = strtod(buf, &endptr); ++ ++ // strtod() endptr no conversion case: detect attempts to convert ++ // null, boolean, or invalid jsmn primitives. ++ if (buf == endptr) ++ if (len == 4 && !strncmp(buf, "null", 4)) ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_NULL }; ++ else ++ return (mxl_json_num){.value = NAN, .err = MXL_JSON_EBADTYPE}; ++ ++ // strtod() incomplete conversion, general case, indicating a ++ // malformed number ++ if ((size_t)(endptr - buf) != len) ++ return (mxl_json_num){.value = NAN, .err = MXL_JSON_MALFORMED}; ++ ++ // strtod errno cases ++ if (errno == ERANGE) ++ return (mxl_json_num){ .value = v, .err = ERANGE }; ++ // safety catch-all, shouldn't happen with post C99 strtod() ++ else if (errno) ++ return (mxl_json_num){ .value = NAN, .err = errno }; ++ ++ // nan/inf error flag ++ if (isnan(v) || isinf(fabs(v))) ++ return (mxl_json_num){.value = v, .err = MXL_JSON_EBADTYPE}; ++ ++ return (mxl_json_num){ .value = v, .err = 0 }; ++} ++ ++// jsmn token and its index together ++typedef struct token_idx_pair { ++ const jsmntok_t *tok; ++ const unsigned int node_idx; ++} token_idx_pair; ++ ++// json path follower, recursively descends object tree ++static token_idx_pair ++find_path_rec(const char *json, const jsmntok_t *tokens, unsigned int ntok, ++ unsigned int node_idx, const char **keys, unsigned int nkeys, ++ unsigned int depth) ++{ ++ // safety limits enforce ++ if (depth > MXL_JSON_MAX_DEPTH || node_idx >= ntok) ++ return (token_idx_pair){0,ntok}; ++ ++ // key exhaustion means we've found the token ++ if (nkeys == 0) ++ return (token_idx_pair){&tokens[node_idx], node_idx}; ++ ++ // cannot descend non object types ++ if (tokens[node_idx].type != JSMN_OBJECT) ++ return (token_idx_pair){0,ntok}; ++ ++ // iterate over object's children to find the named child ++ int num_children = tokens[node_idx].size; ++ unsigned int child_node_idx = node_idx+1; ++ for (int child = 0; child < num_children; child++) { ++ ++ // child_node_idx sanity ++ av_assert0(child_node_idx < ntok); ++ ++ const jsmntok_t *child_tok = &tokens[child_node_idx]; ++ ++ // keys[0] sanity ++ av_assert0(keys[0]); ++ ++ // is this the named child we are looking for? ++ if (is_token_name_equal(json, child_tok, keys[0])) { ++ // descend into the child to find the next key ++ return find_path_rec(json, tokens, ntok, ++ child_node_idx+1, ++ keys + 1, nkeys - 1, depth + 1); ++ } ++ ++ // else jump past the child's contained tokens ++ child_node_idx = last_token_of_subtree(tokens, ntok, child_node_idx+1, ++ depth + 1) + 1; ++ ++ // did we step past end? (because depth limit exceeded) ++ if (child_node_idx >= ntok) ++ break; ++ } ++ ++ // didn't find it ++ return (token_idx_pair){0, ntok}; ++} ++ ++// varargs adapter, build a key array and call the recursive finder ++static token_idx_pair find_path_va(const mxl_json_doc *doc, unsigned int nkeys, ++ va_list ap) ++{ ++ // doc and nkeys sanity ++ av_assert0(doc && doc->tokens && 0 < nkeys && nkeys <= MXL_JSON_MAX_DEPTH); ++ ++ const char *keys[MXL_JSON_MAX_DEPTH]; ++ for (unsigned int i = 0; i < nkeys; i++) ++ keys[i] = va_arg(ap, const char *); ++ ++ return find_path_rec(doc->json, doc->tokens, doc->ntok, 0, keys, nkeys, 0); ++} ++ ++// ++// public api ++// ++ ++int mxl_json_doc_build2(void* logctx, ++ const char *json, mxl_json_doc *out, ++ const unsigned int initial_token_capacity, ++ const unsigned int max_token_capacity) ++{ ++ if (!json || !out) ++ return AVERROR(EINVAL); ++ ++ if (0 == initial_token_capacity || initial_token_capacity > max_token_capacity) ++ return AVERROR(EINVAL); ++ ++ size_t json_len = strlen(json); ++ jsmn_parser p; ++ jsmntok_t *tokens = NULL; ++ unsigned int token_capacity = initial_token_capacity; ++ int rc; ++ ++ jsmn_init(&p); ++ ++ for (;;) { ++ ++ if (token_capacity > max_token_capacity) { ++ loge(logctx, "JSON token cap (%u) exceeded\n", ++ max_token_capacity); ++ av_free(tokens); ++ return AVERROR(ENOMEM); ++ } ++ ++ size_t token_buf_size = (size_t)token_capacity * sizeof(*tokens); ++ ++ // size_t overflow guard ++ if (token_buf_size/token_capacity != sizeof(*tokens)) { ++ av_free(tokens); ++ return AVERROR(ENOMEM); ++ } ++ ++ jsmntok_t *new_tokens = (jsmntok_t *)av_realloc(tokens, token_buf_size); ++ if (!new_tokens) { ++ av_free(tokens); ++ return AVERROR(ENOMEM); ++ } ++ tokens = new_tokens; ++ ++ rc = jsmn_parse(&p, json, json_len, tokens, token_capacity); ++ ++ if (JSMN_ERROR_NOMEM == rc) { ++ // paranoid double; force cap check if near limit ++ if (token_capacity > max_token_capacity / 2u) { ++ // will fail cap check on next loop ++ token_capacity = max_token_capacity + 1u; ++ } else { ++ token_capacity *= 2u; ++ } ++ continue; ++ } ++ ++ break; ++ } ++ ++ if (rc < 0) { ++ loge(logctx, "JSON failed to parse: >>%.*s%s<<\n", ++ MAX_JSON_LOG, json, json_len > MAX_JSON_LOG ? "..." : ""); ++ ++ av_free(tokens); ++ return AVERROR_INVALIDDATA; ++ } ++ ++ out->json = json; ++ out->tokens = tokens; ++ out->ntok = (unsigned int)rc; ++ ++ return 0; ++} ++ ++void mxl_json_doc_release(mxl_json_doc *doc) ++{ ++ if (!doc) ++ return; ++ ++ av_free(doc->tokens); ++ *doc = (mxl_json_doc){0}; ++} ++ ++mxl_json_str mxl_json_doc_get_stringv(const mxl_json_doc *doc, unsigned int nkeys, ++ va_list ap) ++{ ++ if (!doc || !doc->tokens) ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_ENOTFOUND }; ++ ++ if (nkeys > MXL_JSON_MAX_DEPTH) ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_UNSUPPORTED }; ++ ++ token_idx_pair t = find_path_va(doc, nkeys, ap); ++ if (!t.tok) ++ return (mxl_json_str){ .value = NULL, .err = MXL_JSON_ENOTFOUND }; ++ ++ return jsmn_tok_to_string(doc->json, t.tok); ++} ++ ++mxl_json_str mxl_json_doc_get_string(const mxl_json_doc *doc, unsigned int nkeys, ++ ...) ++{ ++ va_list ap; ++ va_start(ap, nkeys); ++ mxl_json_str str = mxl_json_doc_get_stringv(doc, nkeys, ap); ++ va_end(ap); ++ ++ return str; ++} ++ ++mxl_json_num mxl_json_doc_get_doublev(const mxl_json_doc *doc, unsigned int nkeys, ++ va_list ap) ++{ ++ if (!doc || !doc->tokens) ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_ENOTFOUND }; ++ ++ if (nkeys > MXL_JSON_MAX_DEPTH) ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_UNSUPPORTED }; ++ ++ token_idx_pair t = find_path_va(doc, nkeys, ap); ++ if (!t.tok) ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_ENOTFOUND }; ++ ++ // Token look ahead to validate json formation, only one primitive ++ // should follow an object key. ++ if (t.tok->type == JSMN_PRIMITIVE && ++ t.node_idx+1 < doc->ntok && ++ doc->tokens[t.node_idx+1].type == JSMN_PRIMITIVE) { ++ return (mxl_json_num){ .value = NAN, .err = MXL_JSON_MALFORMED}; ++ } ++ ++ return jsmn_tok_to_double(doc->json, t.tok); ++} ++ ++mxl_json_num mxl_json_doc_get_double(const mxl_json_doc *doc, unsigned int nkeys, ++ ...) ++{ ++ va_list ap; ++ va_start(ap, nkeys); ++ mxl_json_num num = mxl_json_doc_get_doublev(doc, nkeys, ap); ++ va_end(ap); ++ ++ return num; ++} +diff --git a/libavformat/mxl_json.h b/libavformat/mxl_json.h +new file mode 100644 +index 0000000000..4781ba02e7 +--- /dev/null ++++ b/libavformat/mxl_json.h +@@ -0,0 +1,225 @@ ++/* ++ * MXL JSON parser for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * Wrapper around jsmn (https://github.com/zserge/jsmn). ++ * Depends on vendored jsmn v1.1.0 (see jsmn.h). ++ */ ++ ++#ifndef AVFORMAT_MXL_JSON_H ++#define AVFORMAT_MXL_JSON_H ++ ++// jsmn header file ++// see: https://github.com/zserge/jsmn/blob/v1.1.0/README.md#usage ++#define JSMN_HEADER ++#include "jsmn.h" ++ ++#include ++ ++/** ++ * Parsed JSON document wrapper. ++ * ++ * Produced by mxl_json_doc_build(). ++ * Released by mxl_json_doc_release() to free the token array. ++ * ++ * Fields: ++ * - json : Pointer to the original NUL-terminated JSON buffer. ++ * Owned by the caller; must remain valid while the doc is used. ++ * - tokens : Heap-allocated array of jsmntok_t tokens. ++ * - ntok : Number of tokens in tokens[] array. ++ */ ++typedef struct { ++ const char *json; ++ jsmntok_t *tokens; ++ unsigned int ntok; ++} mxl_json_doc; ++ ++/** ++ * mxl_json specific error codes ++ * ++ * These are distinct from system values and are returned in ++ * the .err field of json_num/json_str results. ++ */ ++enum MXL_JSON_ERROR { ++ MXL_JSON_EBADTYPE = 2001, // wrong token type ++ MXL_JSON_ENOTFOUND, // lookup failed, key path not present ++ MXL_JSON_NULL, // null primitive found where value requested ++ MXL_JSON_MALFORMED, // malformed json structure ++ MXL_JSON_UNSUPPORTED, // unsupported json, ++ MXL_JSON_EINTERNAL // unspecified internal error ++}; ++ ++/** ++ * Implementation limits ++ */ ++enum MXL_JSON_LIMITS { ++ // limit imposed on object depth and key path length ++ MXL_JSON_MAX_DEPTH = 16, ++ // limit imposed on the character count in a json number ++ MXL_JSON_MAX_NUMBER_CHARS = 127, ++ // the default token capacity used by mxl_json_doc_build ++ MXL_JSON_INITIAL_TOKEN_CAPACITY_DEFAULT = 128, ++ // default maximum token capacity ++ MXL_JSON_TOKEN_CAPACITY_MAX_DEFAULT = 8192 ++}; ++ ++/** ++ * Result of parsing a JSON token as a double. ++ * ++ * @field value Parsed numeric value. ++ * @field err Status code: ++ * 0 - success ++ * EINVAL - token not numeric or conversion failed ++ * ERANGE - overflow or underflow ++ * MXL_JSON_* - an MXL_JSON_ERROR ++ */ ++typedef struct { ++ double value; ++ int err; ++} mxl_json_num; ++ ++/** ++ * Result of parsing a JSON token as a string. The value pointer is ++ * heap allocated and owned by the caller. ++ * ++ * @field value Parsed string value. ++ * @field err Status code: ++ * 0 - success ++ * EINVAL - token is not a string ++ * ENOMEM - allocation failed ++ * MXL_JSON_* - an MXL_JSON_ERROR ++ */ ++typedef struct { ++ char *value; ++ int err; ++} mxl_json_str; ++ ++/** ++ * See @mxl_json_doc_build(). ++ * ++ * @initial_token_capacity is the initial size of the json token array ++ * allocated for @out. The capacity of the token array is doubled as ++ * necessary until it is sufficiently large to parse @json. Value of ++ * zero returns AVERROR(EINVAL). ++ * ++ * @max_token_capacity is a safety limit on the size of tokens ++ * allocated to store the parsed @json. It must be >= ++ * @initial_token_capacity. ++ * ++ */ ++int mxl_json_doc_build2(void* logctx, ++ const char *json, mxl_json_doc *out, ++ unsigned int initial_token_capacity, ++ unsigned int max_token_capacity); ++ ++/** ++ * Parse a JSON string into a json_doc. ++ * ++ * The caller retains ownership of @json, and it must remain ++ * valid. The token array is owned by the mxl_json_doc instance. ++ * ++ * The maximum json document size is limited by ++ * MXL_JSON_TOKEN_CAPACITY_MAX_DEFAULT. Use @mxl_json_doc_build2 to ++ * increase the maximum token capacity. ++ * ++ * @param json Raw JSON buffer (NUL-terminated). ++ * @param out Output json_doc (filled on success). ++ * @return 0 on success, or error code ++ * AVERROR(EINVAL) if args are invalid, ++ * AVERROR(ENOMEM) on token capacity over limit, or allocation failure ++ * AVERROR(EIO) on parse error (other than NOMEM). ++ */ ++static inline int mxl_json_doc_build(void* logctx, const char *json, mxl_json_doc *out) ++{ ++ return mxl_json_doc_build2(logctx, json, out, ++ MXL_JSON_INITIAL_TOKEN_CAPACITY_DEFAULT, ++ MXL_JSON_TOKEN_CAPACITY_MAX_DEFAULT); ++} ++ ++/** ++ * Release resource allocated by @mxl_json_doc_build and clear @doc. ++ */ ++void mxl_json_doc_release(mxl_json_doc *doc); ++ ++/** ++ * Look up a JSON key path and return its string value. ++ * ++ * Traverse the tokens in @doc along the specified sequence key ++ * path. If the path resolves to a string, then a copy of its value is ++ * returned in .value with .err == 0. The caller owns .value and is ++ * responsible for freeing it. ++ * ++ * Errors: ++ * - MXL_JSON_ENOTFOUND : key path not found or @doc is invalid ++ * - MXL_JSON_EBADTYPE : token at path was not string ++ * - MXL_JSON_NULL : token at path was json null, .value is null ++ * - MXL_JSON_UNSUPPORTED : token at path exceeds depth limit ++ * - ENOMEM : allocation failed ++ * ++ * @param doc JSON document (from json_doc_build()). ++ * @param nkeys Number of keys in the path. ++ * @param ap Sequence of (const char*) keys to descend. ++ * @return mxl_json_str with string in .value, and .err status. ++ */ ++mxl_json_str mxl_json_doc_get_stringv(const mxl_json_doc *doc, unsigned int nkeys, va_list ap); ++mxl_json_str mxl_json_doc_get_string(const mxl_json_doc *doc, unsigned int nkeys, ...); ++ ++/** ++ * Look up a JSON key path and return its numeric value. ++ * ++ * Traverse the tokens in @doc along the specified sequence key ++ * path. If the path resolves to a number, then its value is returned ++ * in .value with .err == 0. ++ * ++ * Error: ++ * - MXL_JSON_ENOTFOUND : key path not found or @doc invalid ++ * - MXL_JSON_EBADTYPE : token at path was not numeric ++ * - MXL_JSON_NULL : token at path was json null, .value is NaN ++ * - MXL_JSON_MALFORMED : token at path was a malformed number ++ * - MXL_JSON_UNSUPPORTED : token path length exceeds depth limit ++ * - ERANGE : overflow or underflow during conversion ++ * ++ * @param doc Parsed JSON document (from mxl_json_doc_build()). ++ * @param nkeys Number of keys in the path. ++ * @param ap Sequence of (const char*) keys to descend. ++ * @return mxl_json_num with numeric value in .value, and .err status. ++ */ ++mxl_json_num mxl_json_doc_get_doublev(const mxl_json_doc *doc, unsigned int nkeys, va_list ap); ++mxl_json_num mxl_json_doc_get_double(const mxl_json_doc *doc, unsigned int nkeys, ...); ++ ++/* Fixed-arity convenience wrappers */ ++static inline mxl_json_num mxl_json_doc_get_double1(const mxl_json_doc *doc, ++ const char *k1) ++{ return mxl_json_doc_get_double(doc, 1, k1); } ++ ++static inline mxl_json_num mxl_json_doc_get_double2(const mxl_json_doc *doc, ++ const char *k1, const char *k2) ++{ return mxl_json_doc_get_double(doc, 2, k1, k2); } ++ ++static inline mxl_json_str mxl_json_doc_get_string1(const mxl_json_doc *doc, ++ const char *k1) ++{ return mxl_json_doc_get_string(doc, 1, k1); } ++ ++static inline mxl_json_str mxl_json_doc_get_string2(const mxl_json_doc *doc, ++ const char *k1, const char *k2) ++{ return mxl_json_doc_get_string(doc, 2, k1, k2); } ++ ++#endif /* AVFORMAT_MXL_JSON_H */ +diff --git a/libavformat/mxl_loc.c b/libavformat/mxl_loc.c +new file mode 100644 +index 0000000000..f7fd6e06b8 +--- /dev/null ++++ b/libavformat/mxl_loc.c +@@ -0,0 +1,217 @@ ++/* ++ * MXL domain and flow locator for Media eXchange Layer flows ++ * ++ * AVFMT_NOFILE demuxer; requires "/path/to/domain/.mxl-flow" ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "mxl_loc.h" ++#include "mxl_uri.h" ++#include "mxl_log.h" ++#include "mxl_common.h" ++ ++#include "libavutil/error.h" ++#include "libavutil/avassert.h" ++#include "libavutil/avstring.h" ++#include "libavutil/mem.h" ++#include "libavformat/avformat.h" ++ ++#include ++/** ++ * Extract the directory from a path of the form ++ * "/path/to/domain/.mxl-flow". ++ * ++ * Returns a newly allocated string or null if the path contains no ++ * usable domain component. Caller must free the returned string. ++ */ ++static char *extract_domain_from_path(const char *path) ++{ ++ if (NULL == path) ++ return NULL; ++ ++ char *tmp = av_strdup(path); ++ if (NULL == tmp) ++ return NULL; ++ ++ const char* dir = av_dirname(tmp); ++ av_assert0(dir); ++ ++ // av_dirname special case ++ if (*dir == '\0') ++ dir = "/"; ++ ++ // stable copy ++ char *out = av_strdup(dir); ++ av_free(tmp); ++ ++ return out; ++} ++ ++/** ++ * Extract the id from a path of the form "/path/to/domain/.mxl-flow". ++ * ++ * Returns a newly allocated string or NULL if the path has no usable ++ * id component. Caller must free the returned string. ++ */ ++static char *extract_flowid_from_path(const char *path) ++{ ++ if (NULL == path) ++ return NULL; ++ ++ const char *base = av_basename(path); ++ av_assert0(base); ++ ++ const char *ext = MXL_DOT_FLOW_EXT; ++ size_t ext_len = sizeof(MXL_DOT_FLOW_EXT) - 1; ++ size_t base_len = strlen(base); ++ ++ if (base_len <= ext_len) ++ return NULL; ++ ++ if (strcmp(base + base_len - ext_len, ext) != 0) ++ return NULL; ++ ++ size_t id_len = base_len - ext_len; ++ av_assert1(id_len > 0); ++ ++ char *id = av_malloc(id_len + 1); ++ if (NULL == id) ++ return NULL; ++ ++ memcpy(id, base, id_len); ++ id[id_len] = '\0'; ++ ++ return id; ++} ++ ++int mxl_loc_parse(void* logctx, const char *locator, mxl_loc *loc) ++{ ++ if (NULL == locator || NULL == loc) ++ return AVERROR(EINVAL); ++ ++ if (*locator == '\0') ++ return AVERROR(ENOENT); ++ ++ char *domain_path = NULL; ++ char **flow_ids = NULL; ++ int nb_flow_ids = 0; ++ mxl_uri uri = {0}; ++ int exit_status = AVERROR(EINVAL); ++ ++ // URI case ++ if (mxl_uri_is_mxl_scheme(locator)) { ++ int parse_rc = mxl_parse_uri(logctx, locator, &uri); ++ if (parse_rc) { ++ logv(logctx, "LOC URI scheme found but parse failed: \"%s\"\n", locator); ++ exit_status = parse_rc; ++ goto finally; ++ } ++ ++ av_assert0(uri.host); ++ if (uri.host[0] != '\0') { ++ logv(logctx, "LOC URI host not supported: \"%s\"\n", locator); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // take ownership of URI domain ++ av_assert0(uri.domain); ++ domain_path = uri.domain; ++ uri.domain = NULL; ++ ++ // take ownership of URI flow_ids array ++ nb_flow_ids = uri.nb_flow_ids; ++ flow_ids = uri.flow_ids; ++ uri.flow_ids = NULL; ++ uri.nb_flow_ids = 0; ++ } ++ // filesystem path case ++ else if (av_match_ext(locator, MXL_FLOW_EXT)) { ++ domain_path = extract_domain_from_path(locator); ++ if (NULL == domain_path) { ++ logv(logctx, "LOC URI failed to extract domain\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ char *flow_id = extract_flowid_from_path(locator); ++ if (NULL == flow_id) { ++ logv(logctx, "LOC URI failed to extract MXL flow id\n"); ++ exit_status = AVERROR(EINVAL);; ++ goto finally; ++ } ++ if (!mxl_uri_is_valid_uuid(flow_id)) { ++ logv(logctx, "LOC URI flow ID is not a valid uuid: %s\n", flow_id); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // transfer ownership of flow_id to flow_ids array ++ nb_flow_ids = 1; ++ flow_ids = av_malloc_array(nb_flow_ids, sizeof(*flow_ids)); ++ if (NULL == flow_ids) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ flow_ids[0] = flow_id; ++ flow_id = NULL; ++ } ++ else { ++ domain_path = av_strdup(locator); ++ nb_flow_ids = 0; ++ flow_ids = NULL; ++ } ++ ++ // loc takes ownerhsip of domain_path and flow_ids ++ loc->domain_path = domain_path; ++ domain_path = NULL; ++ loc->nb_flow_ids = nb_flow_ids; ++ loc->flow_ids = flow_ids; ++ flow_ids = NULL; ++ ++ exit_status = 0; ++ ++finally: ++ ++ // no execution path will leave flow_ids set ++ av_assert0(NULL == flow_ids); ++ ++ av_free(domain_path); ++ mxl_uri_free(&uri); ++ ++ if (exit_status) { ++ memset(loc, 0, sizeof(*loc)); ++ } ++ ++ return exit_status; ++} ++ ++void mxl_loc_free(mxl_loc *loc) ++{ ++ if (NULL == loc) ++ return; ++ ++ av_free(loc->domain_path); ++ ++ for (int i = 0; i < loc->nb_flow_ids; i++) ++ av_free(loc->flow_ids[i]); ++ av_free(loc->flow_ids); ++ ++ memset(loc, 0, sizeof(*loc)); ++} +diff --git a/libavformat/mxl_loc.h b/libavformat/mxl_loc.h +new file mode 100644 +index 0000000000..19e963f8d8 +--- /dev/null ++++ b/libavformat/mxl_loc.h +@@ -0,0 +1,42 @@ ++/* ++ * MXL domain and flow locator for Media eXchange Layer flows ++ * ++ * AVFMT_NOFILE demuxer; requires "/path/to/domain/.mxl-flow" ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef AVFORMAT_MXL_LOC_H ++#define AVFORMAT_MXL_LOC_H ++ ++typedef struct mxl_loc { ++ char *domain_path; ++ char **flow_ids; ++ int nb_flow_ids; ++} mxl_loc; ++ ++/** ++ * domain must exist ++ * host must not exist ++ * one or more flow IDs must exist ++ */ ++int mxl_loc_parse(void *logctx, const char *input, mxl_loc *loc); ++ ++void mxl_loc_free(mxl_loc *loc); ++ ++#endif /* AVFORMAT_MXL_LOC_H */ +diff --git a/libavformat/mxl_log.h b/libavformat/mxl_log.h +new file mode 100644 +index 0000000000..2823861d6c +--- /dev/null ++++ b/libavformat/mxl_log.h +@@ -0,0 +1,67 @@ ++/* ++ * MXL logging for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef AVFORMAT_MXL_LOG_H ++#define AVFORMAT_MXL_LOG_H ++ ++#include "libavutil/log.h" ++ ++static inline void logd(const void *avcl, const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ av_vlog((void *)avcl, AV_LOG_DEBUG, fmt, ap); ++ va_end(ap); ++} ++ ++static inline void logv(const void *avcl, const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ av_vlog((void *)avcl, AV_LOG_VERBOSE, fmt, ap); ++ va_end(ap); ++} ++ ++static inline void logi(const void *avcl, const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ av_vlog((void *)avcl, AV_LOG_INFO, fmt, ap); ++ va_end(ap); ++} ++ ++static inline void logw(const void *avcl, const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ av_vlog((void *)avcl, AV_LOG_WARNING, fmt, ap); ++ va_end(ap); ++} ++ ++static inline void loge(const void *avcl, const char *fmt, ...) ++{ ++ va_list ap; ++ va_start(ap, fmt); ++ av_vlog((void *)avcl, AV_LOG_ERROR, fmt, ap); ++ va_end(ap); ++} ++ ++#endif /* AVFORMAT_MXL_LOG_H */ +diff --git a/libavformat/mxl_status.h b/libavformat/mxl_status.h +new file mode 100644 +index 0000000000..8186328f48 +--- /dev/null ++++ b/libavformat/mxl_status.h +@@ -0,0 +1,59 @@ ++/* ++ * MXL status functions for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#ifndef AVFORMAT_MXL_STATUS_H ++#define AVFORMAT_MXL_STATUS_H ++ ++#include ++ ++static const char* mxl_status_to_str(mxlStatus status) { ++ ++ switch(status) { ++ case MXL_STATUS_OK: ++ return "MXL_STATUS_OK"; ++ case MXL_ERR_UNKNOWN: ++ return "MXL_ERR_UNKNOWN"; ++ case MXL_ERR_FLOW_NOT_FOUND: ++ return "MXL_ERR_FLOW_NOT_FOUND"; ++ case MXL_ERR_OUT_OF_RANGE_TOO_LATE: ++ return "MXL_ERR_OUT_OF_RANGE_TOO_LATE"; ++ case MXL_ERR_OUT_OF_RANGE_TOO_EARLY: ++ return "MXL_ERR_OUT_OF_RANGE_TOO_EARLY"; ++ case MXL_ERR_INVALID_FLOW_READER: ++ return "MXL_ERR_INVALID_FLOW_READER"; ++ case MXL_ERR_INVALID_FLOW_WRITER: ++ return "MXL_ERR_INVALID_FLOW_WRITER"; ++ case MXL_ERR_TIMEOUT: ++ return "MXL_ERR_TIMEOUT"; ++ case MXL_ERR_INVALID_ARG: ++ return "MXL_ERR_INVALID_ARG"; ++ case MXL_ERR_CONFLICT: ++ return "MXL_ERR_CONFLICT"; ++ case MXL_ERR_PERMISSION_DENIED: ++ return "MXL_ERR_PERMISSION_DENIED"; ++ case MXL_ERR_FLOW_INVALID: ++ return "MXL_ERR_FLOW_INVALID"; ++ default: ++ return "UNKNOWN"; ++ } ++} ++ ++#endif /* AVFORMAT_MXL_STATUS_H */ +diff --git a/libavformat/mxl_uri.c b/libavformat/mxl_uri.c +new file mode 100644 +index 0000000000..335473e9cf +--- /dev/null ++++ b/libavformat/mxl_uri.c +@@ -0,0 +1,304 @@ ++/* ++ * MXL URI parser for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "mxl_uri.h" ++#include "mxl_log.h" ++ ++#include "libavformat/avformat.h" ++#include "libavutil/mem.h" ++#include "libavutil/error.h" ++#include "libavutil/avstring.h" ++ ++#include ++#include ++ ++/* returns 0 if flow_ids are unique */ ++static int validate_unique_ids(void* logctx, int nb_flow_ids, char** flow_ids) ++{ ++ // demuxer requires unique flow IDs ++ for(int i = 0; i < nb_flow_ids; i++) ++ for(int j = i+1; j < nb_flow_ids; j++) ++ if (0 == av_strcasecmp(flow_ids[i], flow_ids[j])) { ++ logv(logctx, "URI locator flow ID not unique: %s\n", flow_ids[j]); ++ return -1; ++ } ++ ++ return 0; ++} ++ ++/* returns boolean if uri is valid (0 = not valid) */ ++int mxl_uri_is_valid_uuid(const char *s) ++{ ++ if (!s) ++ return 0; ++ ++ if (strlen(s) != 36) ++ return 0; ++ ++ for (int i = 0; i < 36; i++) { ++ switch (i) { ++ case 8: ++ case 13: ++ case 18: ++ case 23: ++ if (s[i] != '-') ++ return 0; ++ break; ++ ++ default: ++ if (!isxdigit((unsigned char)s[i])) ++ return 0; ++ } ++ } ++ ++ return 1; ++} ++ ++void mxl_uri_free(mxl_uri *u) ++{ ++ if (!u) ++ return; ++ ++ av_free(u->host); ++ av_free(u->domain); ++ ++ for (int i = 0; i < u->nb_flow_ids; i++) ++ av_free(u->flow_ids[i]); ++ av_free(u->flow_ids); ++ ++ memset(u, 0, sizeof(*u)); ++} ++ ++int mxl_uri_is_mxl_scheme(const char *uri) ++{ ++ return uri && av_stristart(uri, "mxl:", NULL); ++} ++ ++int mxl_parse_uri(void *logctx, const char *uri, mxl_uri *out) ++{ ++ if (out) { ++ memset(out, 0, sizeof(*out)); ++ out->port = -1; ++ } ++ else ++ return AVERROR(EINVAL); ++ ++ if (!uri) ++ return AVERROR(EINVAL); ++ ++ /* protect against runaway reads */ ++ size_t len = av_strnlen(uri, MXL_URI_MAX + 1); ++ if (len == MXL_URI_MAX + 1) ++ return AVERROR(ENAMETOOLONG); ++ ++ const char *p = strchr(uri, ':'); ++ if (!p) { ++ logv(logctx, "URI scheme not found\n"); ++ return AVERROR(EINVAL); ++ } ++ ++ if ((size_t)(p - uri) >= MXL_SCHEME_MAX) { ++ logv(logctx, "URI scheme too long\n"); ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ if (strchr(uri, '#')) { ++ logv(logctx, "URI fragments not supported\n"); ++ return AVERROR(EINVAL); ++ } ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ char *out_host = NULL; ++ int out_port = -1; ++ char *out_domain = NULL; ++ char **out_flow_ids = NULL; ++ int out_nb_flow_ids = 0; ++ ++ int port = -1; ++ char scheme[MXL_SCHEME_MAX] = { 0 }; ++ char auth[2]; ++ char *host = av_mallocz(MXL_HOST_MAX); ++ char *path = av_mallocz(MXL_PATH_MAX); ++ ++ if (!host || !path) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ av_url_split(scheme, sizeof(scheme), ++ auth, sizeof(auth), ++ host, MXL_HOST_MAX, ++ &port, ++ path, MXL_PATH_MAX, ++ uri); ++ ++ if (av_strcasecmp(scheme, "mxl") != 0) { ++ logv(logctx, "URI unknown scheme: %s\n", scheme); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ /* require mxl:// (authority marker present) */ ++ if (len < 6 || av_strncasecmp(uri, "mxl://", 6) != 0) { ++ logv(logctx, "URI authority marker required in (\"mxl://\")\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ if (auth[0] != '\0') { ++ logv(logctx, "URI user authorization info not supported\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ /* reject empty IPv6 literal: mxl://[]/... */ ++ const char *auth_start = uri + 6; ++ if (auth_start[0] == '[' && host[0] == '\0') { ++ logv(logctx, "URI invalid IPv6 host literal: []\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ /* reject host without domain path */ ++ if (!path || path[0] != '/' || path[1] == '\0' || path[1] == '?') { ++ logv(logctx, "URI must contain a non-empty domain path\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // port=-1 is returned by av_url_split to indicate that the port ++ // is absent, therefore, anything less is an invalid negative ++ // port. Port equal to 0 is returned in abberrant cases such as ++ // absent port or non integer port number. ++ if (port < -1 || port == 0 || port > 65535) { ++ logv(logctx, "URI invalid port: %d\n", port); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ else ++ out_port = port; ++ ++ /* copy host */ ++ out_host = av_strdup(host); ++ if (!out_host) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ /* split path and query */ ++ char *query = strchr(path, '?'); ++ if (query) { ++ *query++ = '\0'; ++ } ++ ++ /* domain must exist */ ++ if (!path[0]) { ++ logv(logctx, "URI invalid missing domain path\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ out_domain = av_strdup(path); ++ if (!out_domain) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ if (query) { ++ char *strtok_context = NULL; ++ char *token = strtok_r(query, "&", &strtok_context); ++ while (token) { ++ ++ if (!strncmp(token, "id=", 3)) { ++ char *flow_id = av_strdup(token + 3); ++ char **tmp; ++ ++ if (!flow_id) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ if (flow_id[0] == '\0') { ++ logv(logctx, "URI invalid query parameter: %s\n", token); ++ av_free(flow_id); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ if (!mxl_uri_is_valid_uuid(flow_id)) { ++ logv(logctx, "URI invalid flow ID parameter: %s\n", flow_id); ++ av_free(flow_id); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ tmp = av_realloc_array(out_flow_ids, ++ out_nb_flow_ids + 1, ++ sizeof(char *)); ++ if (!tmp) { ++ av_free(flow_id); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ out_flow_ids = tmp; ++ out_flow_ids[out_nb_flow_ids++] = flow_id; ++ } ++ ++ token = strtok_r(NULL, "&", &strtok_context); ++ } ++ } ++ ++ int rc = validate_unique_ids(logctx, out_nb_flow_ids, out_flow_ids); ++ if (rc) { ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ out->host = out_host; ++ out->port = out_port; ++ out->domain = out_domain; ++ out->flow_ids = out_flow_ids; ++ out->nb_flow_ids = out_nb_flow_ids; ++ ++ out_host = NULL; ++ out_flow_ids = NULL; ++ out_domain = NULL; ++ ++ exit_status = 0; ++ ++finally: ++ ++ av_free(host); ++ av_free(path); ++ ++ av_free(out_host); ++ av_free(out_domain); ++ ++ if (out_flow_ids) { ++ for (int i = 0; i < out_nb_flow_ids; i++) ++ av_free(out_flow_ids[i]); ++ av_free(out_flow_ids); ++ } ++ ++ return exit_status; ++} +diff --git a/libavformat/mxl_uri.h b/libavformat/mxl_uri.h +new file mode 100644 +index 0000000000..ce0e67131a +--- /dev/null ++++ b/libavformat/mxl_uri.h +@@ -0,0 +1,118 @@ ++/* ++ * MXL URI parser for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * Parse MXL v1.0.0 URIs. ++ * ++ * See: https://github.com/dmf-mxl/mxl/blob/release/v1.0/docs/Addressability.md ++ */ ++ ++#ifndef AVFORMAT_MXL_URI_H ++#define AVFORMAT_MXL_URI_H ++ ++/* Parsing limits: av_url_split() silently truncates into ++ * caller-provided buffers. We therefore cap the total URI length ++ * (MXL_URI_MAX) and require MXL_URI_MAX <= the host/path buffer ++ * sizes, so overlong inputs can be rejected deterministically ++ * (ENAMETOOLONG) and no truncation occurs. */ ++enum MXL_URI_LIMITS { ++ MXL_SCHEME_MAX = sizeof("mxl"), ++ MXL_URI_MAX = 1024, ++ MXL_HOST_MAX = MXL_URI_MAX, ++ MXL_PATH_MAX = MXL_URI_MAX ++}; ++ ++#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L ++_Static_assert(MXL_URI_MAX > 0, ++ "MXL_URI_MAX must be > 0"); ++_Static_assert(MXL_URI_MAX <= MXL_HOST_MAX, ++ "MXL_URI_MAX must be <= MXL_HOST_MAX"); ++_Static_assert(MXL_URI_MAX <= MXL_PATH_MAX, ++ "MXL_URI_MAX must be <= MXL_PATH_MAX"); ++#endif ++ ++typedef struct mxl_uri { ++ char *host; ++ int port; ++ char *domain; ++ char **flow_ids; ++ int nb_flow_ids; ++} mxl_uri; ++ ++/** ++ * Returns 1 if @s is a value UUID, or else returns 0. ++ */ ++int mxl_uri_is_valid_uuid(const char *s); ++ ++/** ++ * Test whether @uri uses the "mxl" scheme (i.e. begins with "mxl:"). ++ * ++ * Performs a simple prefix check for "mxl:" and does not parse or ++ * validate the remainder of the URI. ++ * ++ * @param uri URI string to test (may be NULL) ++ * @return 1 if the URI begins with "mxl:", 0 otherwise. ++ */ ++int mxl_uri_is_mxl_scheme(const char *uri); ++ ++/** ++ * Parse an MXL URI and return the parsed result in @out. ++ * ++ * The URI grammar is: ++ * ++ * mxl://[authority]/domain/path?id=[&id=...] ++ * ++ * The authority component is optional. ++ * ++ * Example of a valid MXL URI that specifies a local domain path: ++ * ++ * "mxl:///dev/shm/mxl?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ * ++ * "/dev/shm/mxl" is the local filesystem path to the MXL domain. The ++ * "id" query parameters carry the flow IDs. This is the format that ++ * is supported by the MXL demuxer. ++ * ++ * Note that the parser will accept URIs that include an authority ++ * component, e.g. "mxl://host:port/domain/path?id=...", but the ++ * demuxer implementation currently supports only local filesystem ++ * domains. ++ * ++ * @param logctx the logger context ++ * @param uri the URI to parse ++ * @param out the parsed URI ++ * @return 0 on success, or error code ++ * AVERROR(EINVAL) URI is invalid and cannot be parsed ++ * AVERROR(ENAMETOOLONG) URI exceeds parser implementation limits ++ * AVERROR(ENOMEM) memory allocation failed ++ * AVERROR_UNKNOWN an unspecified error occurred ++ */ ++int mxl_parse_uri(void *logctx, const char *uri, mxl_uri *out); ++ ++/* ++ * Free a parse result produced by mxl_parse_uri(). The fields of ++ * @parsed_uri are freed and cleared. The @parsed_uri pointer itself ++ * is not freed. ++ * ++ * @param parsed_uri The parsed URI to free (may be NULL). ++ */ ++void mxl_uri_free(mxl_uri *parsed_uri); ++ ++#endif /* AVFORMAT_MXL_URI_H */ +diff --git a/libavformat/mxldec.c b/libavformat/mxldec.c +new file mode 100644 +index 0000000000..dcb2c72e38 +--- /dev/null ++++ b/libavformat/mxldec.c +@@ -0,0 +1,1975 @@ ++/* ++ * MXL demuxer for Media eXchange Layer flows ++ * ++ * AVFMT_NOFILE demuxer; requires either ++ * "/path/to/domain/.mxl-flow" ++ * or "mxl:///path/to/domain?id=[&id=]" ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * See: https://github.com/dmf-mxl/mxl ++ */ ++ ++#include "mxl_json.h" ++#include "mxl_uri.h" ++#include "mxl_loc.h" ++#include "mxl_diag.h" ++#include "mxl_dump.h" ++#include "mxl_log.h" ++#include "mxl_common.h" ++#include "mxl_status.h" ++#include "demux.h" ++#include "avformat.h" ++ ++#include "libswresample/swresample.h" ++#include "libavutil/channel_layout.h" ++#include "libavcodec/avcodec.h" ++#include "libavutil/avassert.h" ++#include "libavutil/avstring.h" ++#include "libavutil/rational.h" ++#include "libavutil/error.h" ++#include "libavutil/time.h" ++#include "libavutil/opt.h" ++#include "libavutil/log.h" ++#include "libavutil/mem.h" ++ ++#include ++#include ++#include ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++typedef enum GrainIndexInit { ++ GRAIN_INDEX_INIT_CURRENT = 0, ++ GRAIN_INDEX_INIT_HEAD, ++ GRAIN_INDEX_INIT_TAIL, ++} GrainIndexInit; ++ ++typedef enum OnTooLate { ++ ON_TOO_LATE_INCREMENT = 0, ++ ON_TOO_LATE_RESET ++} OnTooLate; ++ ++typedef struct VideoState { ++ mxlRational mxl_grain_rate; ++ uint64_t mxl_start_grain_index; ++ uint64_t mxl_grain_index; ++ uint64_t last_pts; ++ int frame_count; ++} VideoState; ++ ++typedef struct AudioState { ++ mxlRational mxl_sample_rate; ++ uint64_t mxl_start_sample_index; ++ uint64_t mxl_sample_index; ++ struct SwrContext *swr_context; ++ int sample_point_count; ++ uint64_t last_pts; ++} AudioState; ++ ++typedef enum FlowType { ++ UNDEFINED_FLOW = 0, ++ VIDEO_FLOW = 1, ++ AUDIO_FLOW = 2 ++} FlowType; ++ ++typedef union FlowState { ++ VideoState video; ++ AudioState audio; ++} FlowState; ++ ++typedef struct StreamContext { ++ mxlFlowReader mxl_flow_reader; ++ FlowType flow_type; ++ FlowState flow; ++ int stream_index; ++} StreamContext; ++ ++typedef struct MXLTuningParams { ++ int audio_samples_per_read; ++ bool video_blocking_read; ++} MXLTuningParams; ++ ++typedef struct MXLContext { ++ ++ const AVClass *class; ++ ++ // domain and flow_ids parsed out of input url ++ mxl_loc loc; ++ ++ // options ++ int zero_copy; ++ int blocking; ++ int reset_on_drop; ++ int max_video_frames; ++ int max_audio_samples; ++ int max_audio_samples_per_read; ++ GrainIndexInit grain_index_init; ++ OnTooLate on_too_late; ++ char *diag_socket; ++ char *audio_dump_file; ++ ++ // interface objects ++ mxlInstance mxl_instance; ++ ++ // stream context array (size is loc.nb_flow_ids) ++ StreamContext* stream_contexts; ++ ++ // stream configuration dependant tuning parameters ++ MXLTuningParams tuning; ++ ++ // flag end of streams ++ bool at_eof; ++ ++ // the index of the next stream context to read ++ int next_stream_context_idx; ++ ++ // diagnostic server ++ mxl_diag_server* diag_server; ++ ++ // audio sample writer ++ mxl_dump_audio_writer* audio_dump_writer; ++} MXLContext; ++ ++#define OFFSET(x) offsetof(MXLContext, x) ++#define FLAGS AV_OPT_FLAG_DECODING_PARAM ++ ++static const AVOption mxl_options[] = { ++ { ++ .name = "zero_copy", ++ .help = "Use zero-copy packet delivery (experimental).", ++ .offset = OFFSET(zero_copy), ++ .type = AV_OPT_TYPE_BOOL, ++ .default_val = { .i64 = 0 }, ++ .min = 0, ++ .max = 1, ++ .flags = FLAGS ++ }, ++ { ++ .name = "blocking", ++ .help = "Use blocking video read: auto (default), 0=non-blocking, 1=blocking" , ++ .offset = OFFSET(blocking), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = -1 }, ++ .min = -1, ++ .max = 1, ++ .flags = FLAGS ++ }, ++ { ++ .name = "reset_on_drop", ++ .help = "Reset presentation timestamp to zero on frame drop.", ++ .offset = OFFSET(reset_on_drop), ++ .type = AV_OPT_TYPE_BOOL, ++ .default_val = { .i64 = 0 }, ++ .min = 0, ++ .max = 1, ++ .flags = FLAGS ++ }, ++ { ++ .name = "max_video_frames", ++ .help = "stop at exactly N video frames", ++ .offset = OFFSET(max_video_frames), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = 0 }, ++ .min = 0, ++ .max = INT_MAX, ++ .flags = FLAGS ++ }, ++ { ++ .name = "max_audio_samples", ++ .help = "stop at exactly N audio sample points", ++ .offset = OFFSET(max_audio_samples), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = 0 }, ++ .min = 0, ++ .max = INT_MAX, ++ .flags = FLAGS ++ }, ++ { ++ .name = "max_audio_samples_per_read", ++ .help = "max audio sample points per MXL read", ++ .offset = OFFSET(max_audio_samples_per_read), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = 0 }, ++ .min = 0, ++ .max = INT_MAX, ++ .flags = FLAGS ++ }, ++ ++ // grain_index_init option group ++ { ++ .name = "grain_index_init", ++ .help = "initial MXL grain index", ++ .offset = OFFSET(grain_index_init), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = GRAIN_INDEX_INIT_CURRENT }, ++ .min = GRAIN_INDEX_INIT_CURRENT, ++ .max = GRAIN_INDEX_INIT_TAIL, ++ .flags = FLAGS, ++ .unit = "grain_index_init", ++ }, ++ { ++ .name = "current", ++ .help = "current time", ++ .offset = 0, ++ .type = AV_OPT_TYPE_CONST, ++ .default_val = { .i64 = GRAIN_INDEX_INIT_CURRENT }, ++ .flags = FLAGS, ++ .unit = "grain_index_init", ++ }, ++ { ++ .name = "head", ++ .help = "ring buffer head", ++ .offset = 0, ++ .type = AV_OPT_TYPE_CONST, ++ .default_val = { .i64 = GRAIN_INDEX_INIT_HEAD }, ++ .flags = FLAGS, ++ .unit = "grain_index_init", ++ }, ++ { ++ .name = "tail", ++ .help = "ring buffer tail", ++ .offset = 0, ++ .type = AV_OPT_TYPE_CONST, ++ .default_val = { .i64 = GRAIN_INDEX_INIT_TAIL }, ++ .flags = FLAGS, ++ .unit = "grain_index_init", ++ }, ++ ++ // on_too_late option group ++ { ++ .name = "on_too_late", ++ .help = "action when MXL reports grain index too late", ++ .offset = OFFSET(on_too_late), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = ON_TOO_LATE_INCREMENT }, ++ .min = ON_TOO_LATE_INCREMENT, ++ .max = ON_TOO_LATE_RESET, ++ .flags = FLAGS, ++ .unit = "on_too_late", ++ }, ++ { ++ .name = "increment", ++ .help = "increment the grain index", ++ .offset = 0, ++ .type = AV_OPT_TYPE_CONST, ++ .default_val = { .i64 = ON_TOO_LATE_INCREMENT }, ++ .flags = FLAGS, ++ .unit = "on_too_late", ++ }, ++ { ++ .name = "reset", ++ .help = "reset to position defined by grain_index_init", ++ .offset = 0, ++ .type = AV_OPT_TYPE_CONST, ++ .default_val = { .i64 = ON_TOO_LATE_RESET }, ++ .flags = FLAGS, ++ .unit = "on_too_late", ++ }, ++ ++ { ++ .name = "diag_socket", ++ .help = "Unix domain socket path for diagnostic monitoring", ++ .offset = OFFSET(diag_socket), ++ .type = AV_OPT_TYPE_STRING, ++ .default_val = { .str = NULL }, ++ .flags = FLAGS, ++ }, ++ ++ { ++ .name = "audio_dump_file", ++ .help = "Write demuxed audio PCM samples to a raw output file", ++ .offset = OFFSET(audio_dump_file), ++ .type = AV_OPT_TYPE_STRING, ++ .default_val = { .str = NULL }, ++ .flags = FLAGS, ++ }, ++ ++ { NULL } ++}; ++ ++#undef OFFSET ++#undef FLAGS ++ ++static const AVClass mxl_demuxer_class = { ++ .class_name = "mxl demuxer", ++ .item_name = av_default_item_name, ++ .option = mxl_options, ++ .version = LIBAVUTIL_VERSION_INT ++}; ++ ++static void log_probe_data(const AVProbeData *p) ++{ ++ logv(NULL, ++ "mxl AVProbeData:\n" ++ " filename: %s\n" ++ " buf_size: %d\n" ++ " mime_type: %s\n", ++ p->filename ? p->filename : "(null)", ++ p->buf_size, ++ p->mime_type ? p->mime_type : "(null)"); ++} ++ ++static void log_format_context(const AVFormatContext *s) ++{ ++ av_assert1(s); ++ ++ logv(s, "mxl AVFormatContext:\n"); ++ logv(s, " url: %s\n", s->url ? s->url : "(null)"); ++} ++ ++static bool probe_is_flow_active(mxlInstance mxl_instance, const char* flow_id) ++{ ++ bool active = false; ++ mxlFlowReader flow_reader = NULL; ++ ++ mxlStatus mxl_status = mxlCreateFlowReader(mxl_instance, flow_id, "", ++ &flow_reader); ++ ++ if (MXL_STATUS_OK != mxl_status) { ++ logv(NULL, "mxlCreateFlowReader error %s for flow: \"%s\"\n", ++ mxl_status_to_str(mxl_status), flow_id); ++ goto finally; ++ } ++ ++ mxlFlowInfo info = {0}; ++ mxl_status = mxlFlowReaderGetInfo(flow_reader, &info); ++ if (MXL_STATUS_OK != mxl_status) { ++ logv(NULL, "mxlFlowReaderGetInfo error %s\n", ++ mxl_status_to_str(mxl_status)); ++ goto finally; ++ } ++ ++ mxl_status = mxlIsFlowActive(mxl_instance, flow_id, &active); ++ if (MXL_STATUS_OK != mxl_status) { ++ logv(NULL, "mxlIsFlowActive error %s\n", ++ mxl_status_to_str(mxl_status)); ++ goto finally; ++ } ++ ++finally: ++ ++ if (flow_reader) { ++ av_assert0(mxl_instance); ++ mxl_status = mxlReleaseFlowReader(mxl_instance, flow_reader); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(NULL, "mxlReleaseFlowReader error %s for flow_id %s\n", ++ mxl_status_to_str(mxl_status), flow_id); ++ } ++ ++ return active; ++} ++ ++ ++static int validate_locator(void *logctx, const mxl_loc* loc) { ++ av_assert0(loc); ++ ++ if (loc->nb_flow_ids < 1) { ++ logv(logctx, "locator requires at least one flow ID\n"); ++ return -1; ++ } ++ ++ // mxl_loc invariants (for nb_flow_ids > 0) ++ av_assert0(loc->domain_path); ++ av_assert0(loc->flow_ids); ++ ++ return 0; ++} ++ ++ ++/** ++ * Identify MXL flow inputs by filename pattern and state. ++ * ++ * Expected identifier: ++ * /path/to/domain/.mxl-flow ++ * mxl:///path/to/domain?id= ++ * ++ * Returns a probe score: ++ * 0 : not recognized ++ * AVPROBE_SCORE_EXTENSION : recognized by URI scheme or file extension ++ * AVPROBE_SCORE_EXTENSION+: flow exists but inactive ++ * AVPROBE_SCORE_MAX : active MXL flow detected ++ */ ++static int mxl_probe(const AVProbeData *p) { ++ ++ av_assert0(p); ++ ++ log_probe_data(p); ++ ++ mxlInstance mxl_instance = NULL; ++ int score = 0; ++ mxl_loc loc = {0}; ++ ++ if (!p->filename) ++ goto finally; ++ ++ int rc = mxl_loc_parse(NULL, p->filename, &loc); ++ if (rc) { ++ logv(NULL, "failed to parse locator: \"%s\"\n", p->filename); ++ goto finally; ++ } ++ ++ rc = validate_locator(NULL, &loc); ++ if (rc) { ++ logv(NULL, "invalid locator: %s\n", p->filename); ++ goto finally; ++ } ++ ++ score = AVPROBE_SCORE_EXTENSION; ++ ++ mxl_instance = mxlCreateInstance(loc.domain_path, NULL); ++ if (NULL == mxl_instance) { ++ logv(NULL, "mxlCreateInstance error\n"); ++ goto finally; ++ } ++ ++ logv(NULL, "probe MXL domain: \"%s\"\n", loc.domain_path); ++ logv(NULL, "probe %d MXL flows:\n", loc.nb_flow_ids); ++ int i = 0; ++ for (i = 0; i < loc.nb_flow_ids; i++) ++ logv(NULL, " flow id: \"%s\"\n", loc.flow_ids[i]); ++ ++ bool all_flows_active = true; ++ for (i = 0; i < loc.nb_flow_ids; i++) { ++ if (!probe_is_flow_active(mxl_instance, loc.flow_ids[i])) { ++ all_flows_active = false; ++ goto finally; ++ } ++ } ++ ++ if (all_flows_active) ++ score = AVPROBE_SCORE_MAX; ++ else ++ score = (score + AVPROBE_SCORE_MAX)/2; ++ ++finally: ++ ++ mxl_loc_free(&loc); ++ ++ if (mxl_instance) { ++ mxlStatus mxl_status = mxlDestroyInstance(mxl_instance); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(NULL, "mxlDestroyInstance error %s\n", mxl_status_to_str(mxl_status)); ++ } ++ ++ if (score) { ++ const char* reason = "unknown"; ++ ++ if (score <= AVPROBE_SCORE_EXTENSION) { ++ reason = MXL_DOT_FLOW_EXT " extension or URI scheme match"; ++ } ++ else if (score < AVPROBE_SCORE_MAX) { ++ reason = "one or more flows is inactive"; ++ } ++ else { ++ av_assert0(score == AVPROBE_SCORE_MAX); ++ reason = "all flows are active"; ++ } ++ ++ logv(NULL, "probe successful (%s)\n", reason); ++ } ++ ++ logv(NULL, "probe score = %d\n", score); ++ ++ return score; ++} ++ ++#define GET1(X,Y,D,K1) \ ++ X = mxl_json_doc_get_##Y(D, K1); \ ++ if (X.err) { \ ++ loge(s, "error reading flow def parameter \"%s\"\n", #K1); \ ++ exit_status = AVERROR_INVALIDDATA; \ ++ goto finally; \ ++ } ++#define GET2(X,Y,D,K1,K2) \ ++ X = mxl_json_doc_get_##Y(D, K1, K2); \ ++ if (X.err) { \ ++ loge(s, "error reading flow def parameter \"%s.%s\"\n", #K1, #K2); \ ++ exit_status = AVERROR_INVALIDDATA; \ ++ goto finally; \ ++ } ++ ++#define SET_META(ST, X, Y) \ ++ if (av_dict_set(&ST->metadata, X, Y, 0) < 0) { \ ++ logw(s, "failed to set stream metadata %s=%s\n", X, Y); \ ++ } ++ ++static inline int pos_int_range_sanity(AVFormatContext *s, double value, const char* detail) ++{ ++ if (value <= 0 || value > (double)INT_MAX) { ++ loge(s, "%s sanity, range error, %.0f\n", detail, value); ++ return AVERROR_INVALIDDATA; ++ } ++ ++ if (floor(value) != value) { ++ loge(s, "%s sanity, non-integer value %.6f\n", detail, value); ++ return AVERROR_INVALIDDATA; ++ } ++ ++ return 0; ++} ++ ++#define POS_INT_SANITY(S, VALUE, DETAIL) \ ++ { int rc = pos_int_range_sanity(S, VALUE, DETAIL); \ ++ if (rc) { exit_status = rc; goto finally; } } ++ ++static int read_video_header(AVFormatContext *s, mxlFlowInfo *flow_info, ++ mxl_json_doc *flow_def_doc, AVStream *st, ++ FlowState* flow_state, const char* media_type) ++{ ++ av_assert0(s && flow_info && flow_def_doc && st && flow_state && media_type); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ mxl_json_str colorspace = {0}; ++ mxl_json_num frame_width = {0}; ++ mxl_json_num frame_height = {0}; ++ mxl_json_num grain_rate_num = {0}; ++ mxl_json_num grain_rate_den = {0}; ++ ++ if (strcmp(media_type, "video/v210")) { ++ loge(s, "unexpected video type \"%s\"\n", media_type); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ GET1(colorspace, string1, flow_def_doc, "colorspace"); ++ GET1(frame_width, double1, flow_def_doc, "frame_width"); ++ GET1(frame_height, double1, flow_def_doc, "frame_height"); ++ GET2(grain_rate_num, double2, flow_def_doc, "grain_rate", "numerator"); ++ GET2(grain_rate_den, double2, flow_def_doc, "grain_rate", "denominator"); ++ ++ POS_INT_SANITY(s, frame_width.value, "frame_width"); ++ POS_INT_SANITY(s, frame_height.value, "frame_height"); ++ POS_INT_SANITY(s, grain_rate_num.value, "grain_rate_num"); ++ POS_INT_SANITY(s, grain_rate_den.value, "grain_rate_den"); ++ ++ if ((int64_t)grain_rate_num.value != flow_info->config.common.grainRate.numerator || ++ (int64_t)grain_rate_den.value != flow_info->config.common.grainRate.denominator) { ++ loge(s, "grain rate sanity, flow def rate != flow info rate" ++ ", {%"PRId64",%"PRId64"} != {%"PRId64",%"PRId64"}\n", ++ (int64_t)grain_rate_num.value, (int64_t)grain_rate_den.value, ++ flow_info->config.common.grainRate.numerator, ++ flow_info->config.common.grainRate.denominator); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ logv(s, "good video flow def: %s %dx%d at %d/%d fps\n", ++ media_type, ++ (int)frame_width.value, (int)frame_height.value, ++ (int)grain_rate_num.value, (int)grain_rate_den.value); ++ ++ st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; ++ st->codecpar->codec_id = AV_CODEC_ID_V210; ++ st->codecpar->codec_tag = MKTAG('v','2','1','0'); ++ st->codecpar->bits_per_coded_sample = 10; ++ st->codecpar->bits_per_raw_sample = 10; ++ st->codecpar->width = (int)frame_width.value; ++ st->codecpar->height = (int)frame_height.value; ++ st->codecpar->sample_aspect_ratio = (AVRational){1,1}; ++ st->codecpar->framerate = (AVRational){(int)grain_rate_num.value, (int)grain_rate_den.value}; ++ st->codecpar->field_order = AV_FIELD_PROGRESSIVE; ++ ++ st->avg_frame_rate = st->codecpar->framerate; ++ st->time_base = av_inv_q(st->codecpar->framerate); ++ ++ SET_META(st, "mxl_colorspace", colorspace.value); ++ ++ flow_state->video.mxl_grain_rate = flow_info->config.common.grainRate; ++ flow_state->video.mxl_start_grain_index = 0; ++ flow_state->video.mxl_grain_index = 0; ++ ++ exit_status = 0; ++ ++finally: ++ ++ av_free(colorspace.value); ++ ++ return exit_status; ++} ++ ++// create SwrContext for planar to interleaved audio conversion ++static int init_swr_context(struct SwrContext **swr_context, ++ int channels, int sample_rate) ++{ ++ AVChannelLayout in_layout, out_layout; ++ av_channel_layout_default(&in_layout, channels); ++ av_channel_layout_default(&out_layout, channels); ++ ++ int rc = swr_alloc_set_opts2(swr_context, ++ &out_layout, AV_SAMPLE_FMT_FLT, sample_rate, ++ &in_layout, AV_SAMPLE_FMT_FLTP, sample_rate, ++ 0, NULL); ++ if (rc < 0) ++ return rc; ++ ++ rc = swr_init(*swr_context); ++ if (rc < 0) { ++ swr_free(swr_context); ++ return rc; ++ } ++ ++ return 0; ++} ++ ++static int read_audio_header(AVFormatContext *s, mxlFlowInfo *flow_info, ++ mxl_json_doc *flow_def_doc, AVStream *st, ++ FlowState* flow_state, const char* media_type) ++{ ++ av_assert0(s && flow_info && flow_def_doc && st && flow_state && media_type); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ mxl_json_num sample_rate_num = {0}; ++ mxl_json_num sample_rate_den = {0}; ++ mxl_json_num channel_count; ++ mxl_json_num bit_depth; ++ ++ if (strcmp(media_type, "audio/float32")) { ++ loge(s, "unexpected audio type \"%s\"\n", media_type); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ GET2(sample_rate_num, double2, flow_def_doc, "sample_rate", "numerator"); ++ GET1(channel_count, double1, flow_def_doc, "channel_count"); ++ GET1(bit_depth, double1, flow_def_doc, "bit_depth"); ++ sample_rate_den = mxl_json_doc_get_double2(flow_def_doc, "sample_rate", "denominator"); ++ ++ // if sample_rate.denominator is present then it must have value 1.0 ++ if (0 == sample_rate_den.err && 1.0 != sample_rate_den.value) { ++ loge(s, "sample_rate_den sanity, must be absent or value 1.0, actual %f\n", ++ sample_rate_den.value); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ if (1LL != flow_info->config.common.grainRate.denominator) { ++ loge(s, "grainRate.denominator sanity, must be value 1, actual %"PRId64"\n", ++ flow_info->config.common.grainRate.denominator); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ if ((int64_t)sample_rate_num.value != flow_info->config.common.grainRate.numerator) { ++ loge(s, "sample rate sanity, flow def rate != flow info rate" ++ ", {%"PRId64",%"PRId64"} != {%"PRId64",%"PRId64"}\n", ++ (int64_t)sample_rate_num.value, 1LL, ++ flow_info->config.common.grainRate.numerator, ++ flow_info->config.common.grainRate.denominator); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ POS_INT_SANITY(s, sample_rate_num.value, "sample_rate_num"); ++ POS_INT_SANITY(s, channel_count.value, "channel_count"); ++ POS_INT_SANITY(s, bit_depth.value, "bit_depth"); ++ ++ if ((uint32_t)channel_count.value != flow_info->config.continuous.channelCount) { ++ loge(s, "channel count sanity, flow def != flow info (%u != %u)\n", ++ (uint32_t)channel_count.value, flow_info->config.continuous.channelCount); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ if ((int)bit_depth.value != 32) { ++ loge(s, "bit_depth sanity, bit_depth != 32, actual %d\n", ++ (int)bit_depth.value); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ logv(s, "good audio flow def: %s %d channels at %d hz\n", ++ media_type, ++ (int)channel_count.value, (int)sample_rate_num.value); ++ ++ st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO; ++ st->codecpar->codec_id = AV_CODEC_ID_PCM_F32LE; ++ st->codecpar->codec_tag = 0; ++ ++ st->codecpar->format = AV_SAMPLE_FMT_FLT; ++ st->codecpar->bits_per_coded_sample = 32; ++ st->codecpar->bits_per_raw_sample = 32; ++ ++ st->codecpar->sample_rate = (int)sample_rate_num.value; ++ st->codecpar->ch_layout.nb_channels = (int)channel_count.value; ++ st->codecpar->ch_layout.order = AV_CHANNEL_ORDER_UNSPEC; ++ ++ st->time_base = (AVRational){ 1, st->codecpar->sample_rate }; ++ st->avg_frame_rate = (AVRational){ 0, 1 }; ++ ++ flow_state->audio.mxl_sample_rate = flow_info->config.common.grainRate; ++ int rc = init_swr_context(&flow_state->audio.swr_context, ++ st->codecpar->ch_layout.nb_channels, ++ st->codecpar->sample_rate); ++ if (rc < 0) { ++ loge(s, "init_swr_context failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ exit_status = 0; ++ ++finally: ++ ++ return exit_status; ++} ++ ++static int read_flow(AVFormatContext *s, mxlInstance mxl_instance, ++ const char* flow_id, StreamContext* stream_ctx) ++{ ++ av_assert0(s && s->priv_data && ++ mxl_instance && flow_id && ++ stream_ctx); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ mxlFlowReader flow_reader = NULL; ++ FlowType flow_type = UNDEFINED_FLOW; ++ FlowState flow_state = {0}; ++ ++ char *flow_def_json = NULL; ++ mxl_json_doc flow_def_doc = {0}; ++ mxl_json_str id = {0}; ++ mxl_json_str desc = {0}; ++ mxl_json_str label = {0}; ++ mxl_json_str format = {0}; ++ mxl_json_str media_type = {0}; ++ char *unescaped_media_type = NULL; ++ ++ mxlStatus mxl_status = mxlCreateFlowReader(mxl_instance, flow_id, "", ++ &flow_reader); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlCreateFlowReader error %s for flow ID %s\n", ++ mxl_status_to_str(mxl_status), flow_id); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ mxlFlowInfo flow_info = {0}; ++ mxl_status = mxlFlowReaderGetInfo(flow_reader, &flow_info); ++ if (mxl_status != MXL_STATUS_OK) { ++ loge(s, "mxlFlowReaderGetInfo failed\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ bool active = false; ++ mxl_status = mxlIsFlowActive(mxl_instance, flow_id, &active); ++ if (mxl_status != MXL_STATUS_OK) { ++ loge(s, "mxlIsFlowActive failed\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ if (!active) { ++ loge(s, "flow exists but is not active\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ logv(s, "active flow with ID %s\n", flow_id); ++ ++ size_t flow_def_json_size = 0; ++ // initial call with NULL buffer gets size ++ mxl_status = mxlGetFlowDef(mxl_instance, flow_id, NULL, &flow_def_json_size); ++ if (MXL_ERR_INVALID_ARG == mxl_status) { ++ if (flow_def_json_size <= 0) { ++ loge(s, "flow def buffer size sanity (%zu)\n", flow_def_json_size); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ flow_def_json = av_malloc(flow_def_json_size); ++ if (!flow_def_json) { ++ loge(s, "failed to allocate flow_def_json of size %zu\n", flow_def_json_size); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ } ++ else { ++ loge(s, "unexpected mxlGetFlowDef status (%s)\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ mxl_status = mxlGetFlowDef(mxl_instance, flow_id, flow_def_json, &flow_def_json_size); ++ if (mxl_status != MXL_STATUS_OK) { ++ loge(s, "mxlGetFlowDef failed\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ logv(s, "good flow definition read for flow ID %s:\n%s\n", flow_id, flow_def_json); ++ ++ int json_status = mxl_json_doc_build(s, flow_def_json, &flow_def_doc); ++ if (json_status) { ++ loge(s, "json flow def parse error %d\n", json_status); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ GET1(id, string1, &flow_def_doc, "id"); ++ GET1(desc, string1, &flow_def_doc, "description"); ++ GET1(label, string1, &flow_def_doc, "label"); ++ GET1(format, string1, &flow_def_doc, "format"); ++ GET1(media_type, string1, &flow_def_doc, "media_type"); ++ ++ if (!id.value || av_strcasecmp(id.value, flow_id) != 0) { ++ loge(s, "flow id sanity, flow def ID != flow locator ID, %s != %s\n", ++ id.value, flow_id); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ AVStream *st = avformat_new_stream(s, NULL); ++ if (!st) { ++ loge(s, "failed to allocate new stream\n"); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ const char *buf = media_type.value; ++ unescaped_media_type = av_get_token(&buf, ""); ++ if (!unescaped_media_type) { ++ loge(s, "failed to allocate unescaped_media_type\n"); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ SET_META(st, "mxl_id", id.value); ++ SET_META(st, "mxl_description", desc.value); ++ SET_META(st, "mxl_label", label.value); ++ SET_META(st, "mxl_format", format.value); ++ SET_META(st, "mxl_media_type", unescaped_media_type); ++ ++ if ( strcmp(unescaped_media_type, "audio/float32") == 0 ) { ++ flow_type = AUDIO_FLOW; ++ int rc = read_audio_header(s, &flow_info, &flow_def_doc, st, &flow_state, ++ unescaped_media_type); ++ if (rc) { ++ loge(s, "read audio header error\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ } ++ else if ( strcmp(unescaped_media_type, "video/v210") == 0 ) { ++ flow_type = VIDEO_FLOW; ++ int rc = read_video_header(s, &flow_info, &flow_def_doc, st, &flow_state, ++ unescaped_media_type); ++ if (rc) { ++ loge(s, "read video header error\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ } ++ else { ++ loge(s, "unsupported media_type \"%s\"\n", unescaped_media_type); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ // success, transfer state to stream_ctx ++ ++ stream_ctx->mxl_flow_reader = flow_reader; ++ flow_reader = NULL; ++ ++ stream_ctx->flow_type = flow_type; ++ flow_type = UNDEFINED_FLOW; ++ ++ stream_ctx->flow= flow_state; ++ memset(&flow_state, 0, sizeof(flow_state)); ++ ++ stream_ctx->stream_index = st->index; ++ ++ exit_status = 0; ++ ++finally: ++ ++ mxl_json_doc_release(&flow_def_doc); ++ ++ av_free(id.value); ++ av_free(desc.value); ++ av_free(label.value); ++ av_free(format.value); ++ av_free(media_type.value); ++ av_free(unescaped_media_type); ++ av_free(flow_def_json); ++ ++ if (flow_reader) { ++ av_assert0(mxl_instance); ++ mxl_status = mxlReleaseFlowReader(mxl_instance, flow_reader); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlReleaseFlowReader error %s\n", mxl_status_to_str(mxl_status)); ++ } ++ ++ ++ return exit_status; ++} ++ ++/** ++ * Examine the configured flows and compute tuning parameters to ++ * optimize FFmpeg's streaming performance. ++ * 1. audio samples per read set to one video frame, or 1/60s ++ * 2. video blocking read set to false if both audio and video flows exit ++ */ ++static void header_init_tuning_params(AVFormatContext *s) ++{ ++ av_assert0(s); ++ ++ MXLContext *p = s->priv_data; ++ ++ AVStream *vst = NULL; ++ AVStream *ast = NULL; ++ for (int i = 0; i < s->nb_streams; i++) { ++ AVStream *st = s->streams[i]; ++ if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && !vst) ++ vst = st; ++ else if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && !ast) ++ ast = st; ++ } ++ ++ StreamContext *vctx = NULL; ++ StreamContext *actx = NULL; ++ for (int i = 0; i < p->loc.nb_flow_ids; i++) { ++ if (p->stream_contexts[i].flow_type == VIDEO_FLOW && !vctx) ++ vctx = &p->stream_contexts[i]; ++ else if (p->stream_contexts[i].flow_type == AUDIO_FLOW && !actx) ++ actx = &p->stream_contexts[i]; ++ } ++ ++ int audio_samples_per_frame = 0; ++ if (ast && vst) { ++ av_assert0(actx && vctx); ++ ++ mxlRational frame_rate = vctx->flow.video.mxl_grain_rate; ++ av_assert0(frame_rate.numerator && frame_rate.denominator); ++ ++ mxlRational sample_rate = actx->flow.audio.mxl_sample_rate; ++ av_assert0(sample_rate.numerator && sample_rate.denominator == 1); ++ ++ audio_samples_per_frame = av_rescale_q( ++ sample_rate.numerator, ++ (AVRational){frame_rate.denominator, frame_rate.numerator}, ++ (AVRational){1, 1}); ++ } ++ else if (ast) { ++ av_assert0(actx); ++ ++ mxlRational sample_rate = actx->flow.audio.mxl_sample_rate; ++ av_assert0(sample_rate.numerator && sample_rate.denominator == 1); ++ ++ // 16.66 ms = 1/60 second ++ audio_samples_per_frame = sample_rate.numerator / 60; ++ } ++ ++ p->tuning.audio_samples_per_read = audio_samples_per_frame; ++ p->tuning.video_blocking_read = !(ast && vst); ++ ++ logv(s, "tuned audio_samples_per_read = %d\n", p->tuning.audio_samples_per_read); ++ logv(s, "tuned video_blocking_read = %d\n", p->tuning.video_blocking_read); ++} ++ ++static void release_stream_context(AVFormatContext *s, ++ mxlInstance mxl_instance, StreamContext* stream_ctx) ++{ ++ if (stream_ctx->mxl_flow_reader) { ++ av_assert0(mxl_instance); ++ if (MXL_STATUS_OK != mxlReleaseFlowReader(mxl_instance, stream_ctx->mxl_flow_reader)) ++ logw(s, "mxlReleaseFlowReader error\n"); ++ stream_ctx->mxl_flow_reader = NULL; ++ } ++ ++ switch(stream_ctx->flow_type) { ++ case VIDEO_FLOW: ++ stream_ctx->flow.video = (VideoState){0}; ++ break; ++ case AUDIO_FLOW: ++ swr_free(&stream_ctx->flow.audio.swr_context); ++ stream_ctx->flow.audio = (AudioState){0}; ++ break; ++ default: ++ logw(s, "unknown flow_type\n"); ++ } ++} ++ ++/** ++ * Reject configurations that have more than one video or more than ++ * one audio flow. ++ */ ++static int header_validate_flows(AVFormatContext *s, ++ int nb_stream_contexts, ++ const StreamContext* stream_contexts) ++{ ++ av_assert0(stream_contexts); ++ ++ int audio_count = 0; ++ int video_count = 0; ++ for (int i = 0; i < nb_stream_contexts; i++) { ++ switch(stream_contexts[i].flow_type) { ++ case AUDIO_FLOW: ++ audio_count++; ++ break; ++ case VIDEO_FLOW: ++ video_count++; ++ break; ++ default: ++ loge(s, "unknown stream context flow type\n"); ++ return -1; ++ } ++ } ++ ++ if (audio_count > 1) { ++ loge(s, "demuxer does not support multiple audio flows\n"); ++ return -1; ++ } ++ else if (video_count > 1) { ++ loge(s, "demuxer does not support multiple video flows\n"); ++ return -1; ++ } ++ ++ return 0; ++} ++ ++/** ++ * Open the MXL flow, parse the flow definition, and set up the stream. ++ * ++ * Return values: ++ * 0 - success ++ * AVERROR(EINVAL) - unrecognized URL ++ * AVERROR_INVALIDDATA - flow definition error ++ * AVERROR(EIO) - MXL API error, or flow inactive ++ * AVERROR(ENOMEM) - allocation failure ++ */ ++static int mxl_read_header(AVFormatContext *s) ++{ ++ av_assert0(s && s->priv_data); ++ ++ log_format_context(s); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ mxl_loc loc = {0}; ++ mxlInstance mxl_instance = NULL; ++ StreamContext *stream_contexts = NULL; ++ int initialized_stream_contexts_count = 0; ++ mxl_diag_server *diag_server = NULL; ++ mxl_dump_audio_writer *audio_dump_writer = NULL; ++ ++ MXLContext *p = s->priv_data; ++ ++ if (!s->url) { ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ int rc = mxl_loc_parse(s, s->url, &loc); ++ if (rc) { ++ logv(s, "failed to parse resource locator: %s\n", s->url); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ rc = validate_locator(s, &loc); ++ if (rc) { ++ logv(s, "invalid locator: %s\n", s->url); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ mxl_instance = mxlCreateInstance(loc.domain_path, NULL); ++ if (NULL == mxl_instance) { ++ loge(s, "mxlCreateInstance error\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ stream_contexts = av_malloc_array(loc.nb_flow_ids, sizeof(StreamContext)); ++ if (!stream_contexts) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ memset(stream_contexts, 0, loc.nb_flow_ids * sizeof(StreamContext)); ++ ++ initialized_stream_contexts_count = 0; ++ for (int i = 0; i < loc.nb_flow_ids; i++) { ++ int flow_rc = read_flow(s, mxl_instance, loc.flow_ids[i], &stream_contexts[i]); ++ if (flow_rc) { ++ exit_status = flow_rc; ++ goto finally; ++ } ++ ++ initialized_stream_contexts_count++; ++ } ++ ++ rc = header_validate_flows(s, loc.nb_flow_ids, stream_contexts); ++ if (rc) { ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ if (p->diag_socket) { ++ logv(s, "diagnostic socket: %s\n", p->diag_socket); ++ ++ diag_server = av_mallocz(sizeof(*diag_server)); ++ if (!diag_server) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ rc = mxl_diag_init(s, diag_server, p->diag_socket); ++ if (rc) { ++ loge(s, "mxl_diag_init failed (%d)\n", rc); ++ exit_status = rc; ++ goto finally; ++ } ++ } ++ ++ if (p->audio_dump_file) { ++ logv(s, "audio dump path: %s\n", p->audio_dump_file); ++ ++ audio_dump_writer = av_mallocz(sizeof(*audio_dump_writer)); ++ if (!audio_dump_writer) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ int rc = mxl_dump_init_audio_writer(audio_dump_writer, p->audio_dump_file); ++ if (rc) { ++ loge(s, "failed to init audio dump writer (%d)\n", rc); ++ return AVERROR(rc); ++ } ++ } ++ ++ // success, transfer state ++ p->loc = loc; ++ memset(&loc, 0, sizeof(loc)); ++ ++ p->mxl_instance = mxl_instance; ++ mxl_instance = NULL; ++ ++ p->stream_contexts = stream_contexts; ++ stream_contexts = NULL; ++ ++ p->at_eof = false; ++ ++ p->diag_server = diag_server; ++ diag_server = NULL; ++ ++ p->audio_dump_writer = audio_dump_writer; ++ audio_dump_writer = NULL; ++ ++ header_init_tuning_params(s); ++ ++ exit_status = 0; ++ ++finally: ++ ++ mxl_loc_free(&loc); ++ ++ if (stream_contexts) { ++ av_assert0(mxl_instance); ++ for(int i = 0; i < initialized_stream_contexts_count; i++) ++ release_stream_context(s, mxl_instance, &stream_contexts[i]); ++ av_free(stream_contexts); ++ } ++ ++ if (mxl_instance) { ++ mxlStatus mxl_status = mxlDestroyInstance(mxl_instance); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlDestroyInstance error %s\n", mxl_status_to_str(mxl_status)); ++ } ++ ++ if (diag_server) ++ mxl_diag_close(s, diag_server); ++ ++ if (audio_dump_writer) ++ mxl_dump_close_audio_writer(audio_dump_writer); ++ ++ return exit_status; ++} ++ ++#undef GET1 ++#undef GET2 ++#undef SET_META ++#undef POS_INT_SANITY ++ ++// zero copy packet release callback ++static void mxl_zero_copy_release_cb(void *ctx, uint8_t *data) ++{ ++ MXLContext *p = (MXLContext*)ctx; ++ (void)p; ++ logv(NULL, "mxl_zero_copy_release_cb %p\n", data); ++} ++ ++static int read_video_packet(AVFormatContext *s, AVStream *st, AVPacket *pkt, ++ StreamContext *stream_ctx, ++ const mxlFlowInfo *flow_info) ++{ ++ MXLContext *p = s->priv_data; ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ uint64_t timestamp = mxlGetTime(); ++ uint64_t diag_read_index = 0; ++ ++ logd(s, "read video packet\n"); ++ ++ if (p->max_video_frames > 0 && stream_ctx->flow.video.frame_count >= p->max_video_frames) { ++ logv(s, "at max_video_frames (%d)\n", p->max_video_frames); ++ return AVERROR_EOF; ++ } ++ ++ // init grain index ++ if (0 == stream_ctx->flow.video.mxl_grain_index) { ++ switch (p->grain_index_init) { ++ case GRAIN_INDEX_INIT_CURRENT: { ++ uint64_t cur_grain_index = ++ mxlTimestampToIndex(&stream_ctx->flow.video.mxl_grain_rate, timestamp); ++ if (MXL_UNDEFINED_INDEX == cur_grain_index) { ++ loge(s, "mxlGetCurrentIndex error MXL_UNDEFINED_INDEX\n"); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ stream_ctx->flow.video.mxl_grain_index = cur_grain_index; ++ logv(s, "init video grain_index = %"PRIu64", policy: current time\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ break; ++ } ++ case GRAIN_INDEX_INIT_HEAD: ++ stream_ctx->flow.video.mxl_grain_index = flow_info->runtime.headIndex; ++ logv(s, "init video grain index = %"PRIu64", policy: headIndex\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ break; ++ case GRAIN_INDEX_INIT_TAIL: ++ stream_ctx->flow.video.mxl_grain_index = flow_info->runtime.headIndex - ++ flow_info->config.discrete.grainCount + 1; ++ logv(s, "init video grain index = %"PRIu64", policy: tail index\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ break; ++ default: ++ loge(s, "unrecognized grain_index_init = \"%d\"\n", p->grain_index_init); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ if (p->reset_on_drop) { ++ // presentation timestamp is mxl_start_grain_index relative ++ stream_ctx->flow.video.mxl_start_grain_index = stream_ctx->flow.video.mxl_grain_index; ++ } ++ } ++ ++ // init the start grain index ++ if (0 == stream_ctx->flow.video.mxl_start_grain_index) ++ stream_ctx->flow.video.mxl_start_grain_index = stream_ctx->flow.video.mxl_grain_index; ++ ++ mxlGrainInfo grain_info = {0}; ++ uint8_t *mxl_payload = NULL; ++ mxlStatus mxl_status = MXL_ERR_UNKNOWN; ++ diag_read_index = stream_ctx->flow.video.mxl_grain_index; ++ if ((p->blocking == -1 && p->tuning.video_blocking_read) || p->blocking == 1) { ++ logd(s, "blocking video frame read, grain index %"PRIu64"\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ const AVRational ns_time_base = {1, 1000000000}; ++ uint64_t timeout_ns = (uint64_t)av_rescale_q(1, st->time_base, ns_time_base); ++ mxl_status = mxlFlowReaderGetGrain(stream_ctx->mxl_flow_reader, ++ stream_ctx->flow.video.mxl_grain_index, ++ timeout_ns, &grain_info, ++ &mxl_payload); ++ } ++ else { ++ logd(s, "non-blocking video read, 1 frame at index %"PRIu64"\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ mxl_status = mxlFlowReaderGetGrainNonBlocking(stream_ctx->mxl_flow_reader, ++ stream_ctx->flow.video.mxl_grain_index, ++ &grain_info, &mxl_payload); ++ } ++ ++ if (MXL_ERR_OUT_OF_RANGE_TOO_LATE == mxl_status) { ++ switch(p->on_too_late) { ++ case ON_TOO_LATE_INCREMENT: ++ logv(s, "no video grain, too late with grain index %"PRIu64 ++ ", increment index and try again\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ stream_ctx->flow.video.mxl_grain_index++; ++ exit_status = AVERROR(EAGAIN); ++ break; ++ case ON_TOO_LATE_RESET: ++ logv(s, "no grain, too late with grain index %"PRIu64 ++ ", reset index and try again\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ exit_status = AVERROR(EAGAIN); ++ stream_ctx->flow.video.mxl_grain_index = 0; ++ break; ++ default: ++ loge(s, "unrecognized on_too_late = \"%d\"\n", p->on_too_late); ++ exit_status = AVERROR_BUG; ++ } ++ goto finally; ++ } ++ else if (MXL_ERR_OUT_OF_RANGE_TOO_EARLY == mxl_status) { ++ logd(s, "no grain, too early with grain index %"PRIu64", try again\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ else if (MXL_ERR_TIMEOUT == mxl_status) { ++ logv(s, "no grain, timed out with grain index %"PRIu64", try again\n", ++ stream_ctx->flow.video.mxl_grain_index); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ else if (MXL_STATUS_OK != mxl_status) { ++ logv(s, "mxlFlowReaderGetGrain error %s (%d)\n", ++ mxl_status_to_str(mxl_status), mxl_status); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ if (grain_info.totalSlices != grain_info.validSlices) { ++ loge(s, "grain size sanity error, totalSlices != validSlices, %" ++ PRIu16" != %"PRIu16"\n", ++ grain_info.totalSlices, grain_info.validSlices); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ size_t grain_size = grain_info.totalSlices * flow_info->config.discrete.sliceSizes[0]; ++ if (p->zero_copy) { ++ AVBufferRef *zcbuf = av_buffer_create(mxl_payload, grain_size, ++ mxl_zero_copy_release_cb, p, 0); ++ if (!zcbuf) { ++ loge(s, "av_buffer_create error\n"); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ ++ pkt->buf = zcbuf; ++ pkt->data = mxl_payload; ++ pkt->size = grain_size; ++ } ++ else ++ { ++ av_assert1(!pkt->buf); ++ av_assert1(!pkt->data); ++ int rc = av_new_packet(pkt, grain_size); ++ if (rc < 0) { ++ loge(s, "av_new_packet error %d\n", rc); ++ exit_status = rc; ++ goto finally; ++ } ++ av_assert1(pkt->data); ++ memcpy(pkt->data, mxl_payload, grain_size); ++ } ++ ++ int64_t next_pts = ++ stream_ctx->flow.video.mxl_grain_index - stream_ctx->flow.video.mxl_start_grain_index; ++ ++ // monotonic pts sanity check ++ av_assert0(next_pts >= 0 && ++ (stream_ctx->flow.video.last_pts == 0 || ++ next_pts > stream_ctx->flow.video.last_pts)); ++ ++ if (next_pts - stream_ctx->flow.video.last_pts > 1) ++ logi(s, "video pts jumped %d frames\n", next_pts - stream_ctx->flow.video.last_pts); ++ ++ stream_ctx->flow.video.last_pts = next_pts; ++ ++ pkt->pos = -1; ++ pkt->pts = next_pts; ++ pkt->dts = next_pts; ++ pkt->duration = 1; ++ pkt->stream_index = stream_ctx->stream_index; ++ pkt->flags |= AV_PKT_FLAG_KEY; ++ ++ logd(s, "good frame, grain_index=%"PRIu64", next_pts=%"PRId64 ++ ", pkt->data=%p, mxl_payload=%p\n", ++ stream_ctx->flow.video.mxl_grain_index, next_pts, pkt->data, mxl_payload); ++ ++ // all good, advance the grain index ++ stream_ctx->flow.video.mxl_grain_index++; ++ stream_ctx->flow.video.frame_count++; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (exit_status < 0) ++ av_packet_unref(pkt); ++ ++ if (p->diag_server) { ++ uint64_t exec_dur = mxlGetTime() - timestamp; ++ uint64_t head_index = flow_info->runtime.headIndex; ++ uint64_t tail_index = head_index - flow_info->config.discrete.grainCount + 1; ++ mxl_diag_msg msg = {0}; ++ mxl_diag_init_msg_video_read(&msg, timestamp, exec_dur, ++ tail_index, head_index, ++ diag_read_index, mxl_status); ++ int diag_rc = mxl_diag_send(s, p->diag_server, &msg); ++ if (diag_rc < 0) ++ logd(s, "mxl_diag_send failed (%d)\n", diag_rc); ++ } ++ ++ return exit_status; ++} ++ ++#define USE_SWR_CONVERT ++#ifdef USE_SWR_CONVERT ++static int interleave_from_payload_fragments(AVFormatContext *s, ++ struct SwrContext *swr_context, ++ const mxlWrappedMultiBufferSlice *payload, ++ int fragment_idx, float *dst) { ++ ++ av_assert1(s && swr_context && payload && dst); ++ ++ int exit_status = 0; ++ ++ size_t fragment_size = payload->base.fragments[fragment_idx].size; ++ av_assert1(fragment_size % sizeof(float) == 0); ++ av_assert1(fragment_size / sizeof(float) <= INT_MAX); ++ ++ int nb_samples = (int)(fragment_size / sizeof(float)); ++ av_assert1(nb_samples > 0); ++ av_assert1(nb_samples*sizeof(float) == payload->base.fragments[fragment_idx].size); ++ ++ float **src_array = (float**)av_malloc_array(payload->count, sizeof(*src_array)); ++ if (!src_array) { ++ logw(s, "interleave_from_payload_fragments malloc failed\n"); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ for (int channel = 0; channel < payload->count; ++channel) { ++ src_array[channel] = (float*)((uint8_t*)payload->base.fragments[fragment_idx].pointer ++ + channel*payload->stride); ++ } ++ // planar to interleaved convert ++ uint8_t *dst_array[1] = { (uint8_t *)dst }; ++ int rc = swr_convert(swr_context, ++ dst_array, nb_samples, ++ (const uint8_t**)src_array, nb_samples); ++ if (rc < 0) { ++ loge(s, "swr_convert error %d\n", rc); ++ exit_status = rc; ++ goto finally; ++ } ++ else if (rc != nb_samples) { ++ loge(s, "incomplete fragment %d planar to interleaved conversion " ++ "(expected %d samples, actual %d samples)\n", fragment_idx, nb_samples, rc); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ exit_status = nb_samples; ++ ++finally: ++ ++ av_free(src_array); ++ ++ return exit_status; ++} ++ ++static int mxl_payload_to_ffmpeg_packet(AVFormatContext *s, ++ struct SwrContext *swr_context, ++ const mxlWrappedMultiBufferSlice *payload, ++ AVPacket *pkt) ++{ ++ av_assert1(s && swr_context && payload && pkt); ++ ++ int exit_status = 0; ++ ++ float* dst0 = (float*)(pkt->data); ++ int rc = interleave_from_payload_fragments(s, swr_context, payload, 0, dst0); ++ if (rc < 0) { ++ exit_status = rc; ++ goto finally; ++ } ++ ++ int nb_samples0 = rc; ++ ++ if (payload->base.fragments[1].size > 0) { ++ float* dst1 = dst0 + nb_samples0*payload->count; ++ int rc = interleave_from_payload_fragments(s, swr_context, payload, 1, dst1); ++ if (rc < 0) { ++ exit_status = rc; ++ goto finally; ++ } ++ ++ int nb_samples1 = rc; ++ (void)nb_samples1; ++ av_assert1((nb_samples0 + nb_samples1)*sizeof(float) == ++ (payload->base.fragments[0].size + payload->base.fragments[1].size)); ++ } ++ else { ++ av_assert1((nb_samples0)*sizeof(float) == (payload->base.fragments[0].size)); ++ } ++ ++finally: ++ ++ return exit_status; ++} ++#else ++// unoptimized interlacer remains for reference ++static int mxl_payload_to_ffmpeg_packet(AVFormatContext *, struct SwrContext *, ++ const mxlWrappedMultiBufferSlice *payload, ++ AVPacket *pkt) ++{ ++ av_assert0(payload && pkt); ++ ++ for (int channel = 0; channel < payload->count; ++channel) { ++ float *dst = (float*)(pkt->data + channel*sizeof(float)); ++ ++ float *src0 = (float*)((uint8_t*)payload->base.fragments[0].pointer + channel*payload->stride); ++ size_t nb_samples0 = payload->base.fragments[0].size/sizeof(float); ++ av_assert0(nb_samples0*sizeof(float) == payload->base.fragments[0].size); ++ for(int i = 0; i < nb_samples0; i++) { ++ *dst = src0[i]; ++ dst += payload->count; ++ } ++ ++ if (payload->base.fragments[1].size > 0) { ++ float *src1 = (float*)((uint8_t*)payload->base.fragments[1].pointer + channel*payload->stride); ++ size_t nb_samples1 = payload->base.fragments[1].size/sizeof(float); ++ av_assert0(nb_samples1*sizeof(float) == payload->base.fragments[1].size); ++ for(int i = 0; i < nb_samples1; i++) { ++ *dst = src1[i]; ++ dst += payload->count; ++ } ++ } ++ } ++ ++ return 0; ++} ++#endif ++ ++static int read_audio_packet(AVFormatContext *s, AVStream *st, AVPacket *pkt, ++ StreamContext *stream_ctx, ++ const mxlFlowInfo *flow_info) ++{ ++ MXLContext *p = s->priv_data; ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ uint64_t timestamp = mxlGetTime(); ++ uint64_t diag_read_index = 0; ++ ++ logd(s, "read audio packet\n"); ++ ++ // default max_samples_per_read is the MXL batch size hint ++ av_assert0(0 < flow_info->config.common.maxCommitBatchSizeHint && ++ flow_info->config.common.maxCommitBatchSizeHint <= INT_MAX); ++ int max_samples_per_read = (int)flow_info->config.common.maxCommitBatchSizeHint; ++ ++ if (p->tuning.audio_samples_per_read > 0) { ++ max_samples_per_read = p->tuning.audio_samples_per_read; ++ } ++ ++ // override default max_samples_per_read with command line option if set ++ if (p->max_audio_samples_per_read > 0) { ++ max_samples_per_read = p->max_audio_samples_per_read; ++ } ++ ++ if (p->max_audio_samples > 0 && stream_ctx->flow.audio.sample_point_count >= p->max_audio_samples) { ++ logv(s, "at max_audio_samples (%d)\n", p->max_audio_samples); ++ return AVERROR_EOF; ++ } ++ ++ // init sample index ++ if (0 == stream_ctx->flow.audio.mxl_sample_index) { ++ switch (p->grain_index_init) { ++ case GRAIN_INDEX_INIT_CURRENT: { ++ uint64_t cur_sample_index = ++ mxlTimestampToIndex(&stream_ctx->flow.audio.mxl_sample_rate, timestamp); ++ if (MXL_UNDEFINED_INDEX == cur_sample_index) { ++ loge(s, "mxlGetCurrentIndex error MXL_UNDEFINED_INDEX\n"); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ stream_ctx->flow.audio.mxl_sample_index = cur_sample_index; ++ logv(s, "init audio sample_index = %"PRIu64", policy: current time\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ break; ++ } ++ case GRAIN_INDEX_INIT_HEAD: ++ stream_ctx->flow.audio.mxl_sample_index = flow_info->runtime.headIndex; ++ logv(s, "init audio sample index = %"PRIu64", policy: headIndex\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ break; ++ case GRAIN_INDEX_INIT_TAIL: { ++ // MXL limits readable audio ring buffer length to half ++ // the buffer size. The tail calc must handle ++ // max_audio_samples smaller than readableBufLen and, in ++ // that case, align to the final (possibly partial) read ++ // block. ++ int readableBufLen = (int)(flow_info->config.continuous.bufferLength / 2); ++ if (0 < p->max_audio_samples && p->max_audio_samples < readableBufLen) { ++ int rem = p->max_audio_samples % max_samples_per_read; ++ uint64_t start_index = flow_info->runtime.headIndex - ++ (uint64_t)(p->max_audio_samples - (rem ? rem : max_samples_per_read)); ++ av_assert0(flow_info->runtime.headIndex >= start_index); ++ stream_ctx->flow.audio.mxl_sample_index = start_index; ++ } ++ else { ++ uint64_t start_index = ++ flow_info->runtime.headIndex - readableBufLen; ++ av_assert0(flow_info->runtime.headIndex >= start_index); ++ stream_ctx->flow.audio.mxl_sample_index = start_index; ++ } ++ logv(s, "init audio sample index = %"PRIu64", policy: tail index\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ break; ++ } ++ default: ++ loge(s, "unrecognized grain_index_init = \"%d\"\n", p->grain_index_init); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ if (p->reset_on_drop) { ++ // presentation timestamp is mxl_start_grain_index relative ++ stream_ctx->flow.audio.mxl_start_sample_index = stream_ctx->flow.audio.mxl_sample_index; ++ } ++ } ++ ++ // init the start grain index ++ if (0 == stream_ctx->flow.audio.mxl_start_sample_index) ++ stream_ctx->flow.audio.mxl_start_sample_index = stream_ctx->flow.audio.mxl_sample_index; ++ ++ int samples_this_read = max_samples_per_read; ++ if (p->max_audio_samples > 0) { ++ samples_this_read = p->max_audio_samples - stream_ctx->flow.audio.sample_point_count; ++ if (samples_this_read > max_samples_per_read) ++ samples_this_read = max_samples_per_read; ++ } ++ ++ const AVRational ns_time_base = {1, 1000000000}; ++ uint64_t timeout_ns = (uint64_t)av_rescale_q(max_samples_per_read, st->time_base, ns_time_base); ++ mxlWrappedMultiBufferSlice payload = {0}; ++ diag_read_index = stream_ctx->flow.audio.mxl_sample_index; ++ logd(s, "non-blocking audio read, %d samples at index %"PRIu64"\n", ++ samples_this_read, stream_ctx->flow.audio.mxl_sample_index); ++ mxlStatus mxl_status = mxlFlowReaderGetSamples( ++ stream_ctx->mxl_flow_reader, stream_ctx->flow.audio.mxl_sample_index, ++ (size_t)samples_this_read, timeout_ns, &payload); ++ ++ if (MXL_ERR_OUT_OF_RANGE_TOO_LATE == mxl_status) { ++ ++ static int64_t last_log_time; ++ int64_t now = av_gettime_relative(); ++ if (!last_log_time || now - last_log_time >= 1000000) { ++ last_log_time = now; ++ logv(s, "audio too late\n"); ++ } ++ ++ switch(p->on_too_late) { ++ case ON_TOO_LATE_INCREMENT: ++ logv(s, "headIndex = %"PRIu64" vs sample_indx = %"PRIu64" diff = %d\n", ++ flow_info->runtime.headIndex, stream_ctx->flow.audio.mxl_sample_index, ++ flow_info->runtime.headIndex - stream_ctx->flow.audio.mxl_sample_index); ++ logv(s, "no audio samples, too late with sample index %"PRIu64 ++ ", increment index and try again\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ stream_ctx->flow.audio.mxl_sample_index += max_samples_per_read; ++ exit_status = AVERROR(EAGAIN); ++ break; ++ case ON_TOO_LATE_RESET: ++ logv(s, "no audio samples, too late with sample index %"PRIu64 ++ ", reset index and try again\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ exit_status = AVERROR(EAGAIN); ++ stream_ctx->flow.audio.mxl_sample_index = 0; ++ break; ++ default: ++ loge(s, "unrecognized on_too_late = \"%d\"\n", p->on_too_late); ++ exit_status = AVERROR_BUG; ++ } ++ goto finally; ++ } ++ else if (MXL_ERR_OUT_OF_RANGE_TOO_EARLY == mxl_status) { ++ logd(s, "no audio samples, too early with sample index %"PRIu64", try again\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ else if (MXL_ERR_TIMEOUT == mxl_status) { ++ logv(s, "no audio samples, timed out with sample index %"PRIu64", try again\n", ++ stream_ctx->flow.audio.mxl_sample_index); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ else if (MXL_STATUS_OK != mxl_status) { ++ logv(s, "mxlFlowReaderGetSamples error %s (%d)\n", ++ mxl_status_to_str(mxl_status), mxl_status); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ int payload_len_bytes = samples_this_read * payload.count * sizeof(float); ++ ++ // payload total length invariant ++ av_assert0((size_t)payload_len_bytes == ++ payload.count*(payload.base.fragments[0].size + payload.base.fragments[1].size)); ++ av_assert1(payload_len_bytes <= INT_MAX); ++ av_assert1(!pkt->buf); ++ int rc = av_new_packet(pkt, payload_len_bytes); ++ if (rc < 0) { ++ loge(s, "av_new_packet error %d\n", rc); ++ exit_status = rc; ++ goto finally; ++ } ++ av_assert1(pkt->buf); ++ av_assert1(pkt->size == payload_len_bytes); ++ ++ // stride and bufferLength relationship invariant ++ av_assert0(payload.stride == flow_info->config.continuous.bufferLength * sizeof(float)); ++ ++ // interleave planar source buffers into pkt->data ++ rc = mxl_payload_to_ffmpeg_packet(s, stream_ctx->flow.audio.swr_context, &payload, pkt); ++ if (rc < 0) { ++ loge(s, "audio payload to packet conversion failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ int64_t next_pts = ++ stream_ctx->flow.audio.mxl_sample_index - stream_ctx->flow.audio.mxl_start_sample_index; ++ ++ // monotonic pts sanity check ++ av_assert0(next_pts >= 0 && ++ (stream_ctx->flow.audio.last_pts == 0 || ++ next_pts > stream_ctx->flow.audio.last_pts)); ++ ++ if (next_pts - stream_ctx->flow.audio.last_pts > samples_this_read) ++ logi(s, "audio pts jumped %d samples\n", next_pts - stream_ctx->flow.audio.last_pts); ++ ++ stream_ctx->flow.audio.last_pts = next_pts; ++ ++ pkt->pts = next_pts; ++ pkt->dts = next_pts; ++ pkt->duration = samples_this_read; ++ pkt->stream_index = stream_ctx->stream_index; ++ ++ logd(s, "good audio block, sample_index=%"PRIu64", next_pts=%"PRId64 ++ ", samples_this_read = %d, pkt->data=%p mxl_payload_0=%p, mxl_payload_1=%p\n", ++ stream_ctx->flow.audio.mxl_sample_index, next_pts, samples_this_read, pkt->data, ++ payload.base.fragments[0].pointer, ++ payload.base.fragments[1].size > 0 ? payload.base.fragments[1].pointer : NULL); ++ ++ // all good, advance the sample index ++ stream_ctx->flow.audio.mxl_sample_index += samples_this_read; ++ stream_ctx->flow.audio.sample_point_count += samples_this_read; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (exit_status == 0 && p->audio_dump_writer) { ++ mxl_dump_write_audio(p->audio_dump_writer, pkt->data, pkt->size); ++ } ++ ++ if (exit_status < 0) ++ av_packet_unref(pkt); ++ ++ if (p->diag_server) { ++ uint64_t duration = mxlGetTime() - timestamp; ++ uint64_t head_index = flow_info->runtime.headIndex; ++ uint64_t tail_index = head_index - flow_info->config.continuous.bufferLength + 1; ++ mxl_diag_msg msg = {0}; ++ mxl_diag_init_msg_audio_read(&msg, timestamp, duration, ++ tail_index, head_index, ++ diag_read_index, ++ (uint32_t)samples_this_read, mxl_status); ++ int diag_rc = mxl_diag_send(s, p->diag_server, &msg); ++ if (diag_rc < 0) ++ logd(s, "mxl_diag_send failed (%d)\n", diag_rc); ++ } ++ ++ return exit_status; ++} ++ ++static int mxl_read_packet(AVFormatContext *s, AVPacket *pkt) ++{ ++ av_assert0(s && s->priv_data); ++ ++ MXLContext *p = s->priv_data; ++ ++ int stream_ctx_idx = p->next_stream_context_idx; ++ p->next_stream_context_idx = ++ (p->next_stream_context_idx + 1) % p->loc.nb_flow_ids; ++ ++ av_assert0(stream_ctx_idx < p->loc.nb_flow_ids); ++ ++ // early bail-out to avoid flooding the log ++ if (p->at_eof) { ++ logv(s, "at eof\n"); ++ return AVERROR_EOF; ++ } ++ ++ StreamContext* stream_ctx = &p->stream_contexts[stream_ctx_idx]; ++ ++ av_assert0(stream_ctx->stream_index < s->nb_streams); ++ AVStream *st = s->streams[stream_ctx->stream_index]; ++ ++ int exit_status = AVERROR_UNKNOWN; ++ bool active = false; ++ ++ if (p->diag_server) { ++ int diag_rc = mxl_diag_poll(s, p->diag_server); ++ if (diag_rc < 0) ++ logd(s, "mxl_diag_poll failed (%d)\n", diag_rc); ++ } ++ ++ mxlStatus mxl_status = mxlIsFlowActive( ++ p->mxl_instance, p->loc.flow_ids[stream_ctx_idx], &active); ++ if (MXL_STATUS_OK == mxl_status && !active) { ++ logv(s, "flow not active, stopping\n"); ++ p->at_eof = true; ++ exit_status = AVERROR_EOF; ++ goto finally; ++ } ++ else if (MXL_ERR_FLOW_NOT_FOUND == mxl_status) { ++ logv(s, "flow not found, stopping\n"); ++ p->at_eof = true; ++ exit_status = AVERROR_EOF; ++ goto finally; ++ } ++ else if (mxl_status != MXL_STATUS_OK) { ++ loge(s, "mxlIsFlowActive failed %s, retrying\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ ++ mxlFlowInfo flow_info = {0}; ++ mxl_status = mxlFlowReaderGetInfo(stream_ctx->mxl_flow_reader, &flow_info); ++ if (mxl_status != MXL_STATUS_OK) { ++ loge(s, "mxlFlowReaderGetInfo failed\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ switch (stream_ctx->flow_type) { ++ case VIDEO_FLOW: { ++ int rc = read_video_packet(s, st, pkt, stream_ctx, &flow_info); ++ if (rc) { ++ exit_status = rc; ++ goto finally; ++ } ++ break; ++ } ++ case AUDIO_FLOW: { ++ int rc = read_audio_packet(s, st, pkt, stream_ctx, &flow_info); ++ if (rc) { ++ exit_status = rc; ++ goto finally; ++ } ++ break; ++ } ++ default: ++ loge(s, "unknown flow type\n"); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ exit_status = 0; ++ ++finally: ++ ++ return exit_status; ++} ++ ++/** ++ * Free or release all allocated resources. ++ * ++ * - 0 - success ++ */ ++static int mxl_read_close(AVFormatContext *s) ++{ ++ av_assert0(s && s->priv_data); ++ ++ MXLContext *p = s->priv_data; ++ ++ for (int i = 0; i < p->loc.nb_flow_ids; i++) ++ release_stream_context(s, p->mxl_instance, &p->stream_contexts[i]); ++ av_free(p->stream_contexts); ++ p->stream_contexts = NULL; ++ ++ if (p->mxl_instance) { ++ if (MXL_STATUS_OK != mxlDestroyInstance(p->mxl_instance)) ++ logw(s, "mxlDestroyInstance error\n"); ++ p->mxl_instance = NULL; ++ } ++ ++ mxl_loc_free(&p->loc); ++ ++ p->at_eof = false; ++ ++ if (p->diag_server) { ++ mxl_diag_close(s, p->diag_server); ++ av_free(p->diag_server); ++ p->diag_server = NULL; ++ } ++ ++ if (p->audio_dump_writer) { ++ mxl_dump_close_audio_writer(p->audio_dump_writer); ++ av_free(p->audio_dump_writer); ++ p->audio_dump_writer = NULL; ++ } ++ ++ return 0; ++} ++ ++// MXL demuxer definition ++const FFInputFormat ff_mxl_demuxer = { ++ // AVInputFormat (public-facing fields) ++ .p.name = "mxl", ++ .p.long_name = NULL_IF_CONFIG_SMALL(MXL_LONG_NAME), ++ .p.extensions = MXL_FLOW_EXT, ++ .p.priv_class = &mxl_demuxer_class, ++ .p.flags = AVFMT_NOFILE // not a file ++ | AVFMT_NOBINSEARCH // no binary search seek ++ | AVFMT_NOGENSEARCH // no generic timestamp search ++ | AVFMT_NO_BYTE_SEEK, // no byte position seek ++ ++ // private state ++ .priv_data_size = sizeof(MXLContext), ++ ++ // demuxer callbacks ++ .read_probe = mxl_probe, ++ .read_header = mxl_read_header, ++ .read_packet = mxl_read_packet, ++ .read_close = mxl_read_close, ++ ++ // unimplemented ++ .read_seek = NULL, // not seekable ++ .read_seek2 = NULL, // no timestamp seeking ++ .read_play = NULL, // no pause/resume ++ .read_pause = NULL, // no pause/resume ++ .read_timestamp = NULL, // no timestamp probing ++ .get_device_list = NULL, // not a device input ++ ++ // internal behavior flags (init/cleanup semantic) ++ .flags_internal = FF_INFMT_FLAG_INIT_CLEANUP, ++}; +diff --git a/libavformat/mxlenc.c b/libavformat/mxlenc.c +new file mode 100644 +index 0000000000..a0599f1e37 +--- /dev/null ++++ b/libavformat/mxlenc.c +@@ -0,0 +1,893 @@ ++/* ++ * MXL muxer for Media eXchange Layer flows ++ * ++ * AVFMT_NOFILE muxer; requires "-{audio|video}_flow_id id /path/to/domain" ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++/* ++ * See: https://github.com/dmf-mxl/mxl ++ */ ++ ++#include "mxl_log.h" ++#include "mxl_common.h" ++#include "mxl_status.h" ++#include "mxl_flow_def.h" ++#include "mux.h" ++#include "avformat.h" ++#include "avio.h" ++#include "internal.h" ++ ++#include ++#include ++#include "libavutil/opt.h" ++#include "libavutil/avassert.h" ++#include "libavutil/file.h" ++#include "libavutil/mem.h" ++#include "libavutil/time.h" ++ ++#include ++#include ++#include ++ ++#include ++#include ++ ++typedef struct FlowState { ++ int stream_index; ++ const char *flow_id; ++ const char *detail; ++ mxlFlowConfigInfo *mxl_flow_config_info; ++ mxlFlowWriter mxl_flow_writer; ++ mxlRational mxl_grain_rate; ++ uint64_t mxl_start_index; ++ uint64_t mxl_cur_index; ++} FlowState; ++ ++typedef struct VideoStreamState { ++ VideoFlowDefParams flow_def_params; ++ FlowState flow; ++} VideoStreamState; ++ ++typedef struct AudioStreamState { ++ AudioFlowDefParams flow_def_params; ++ FlowState flow; ++ struct SwrContext *swr_context; ++} AudioStreamState; ++ ++typedef struct State { ++ mxlInstance mxl_instance; ++ ++ bool has_video_stream; ++ VideoStreamState video; ++ ++ bool has_audio_stream; ++ AudioStreamState audio; ++} State; ++ ++typedef struct MXLContext { ++ ++ const AVClass *class; ++ ++ // domain_path from input url ++ const char *domain_path; ++ ++ // options ++ const char *audio_flow_id; ++ const char *video_flow_id; ++ const char *teardown_sync_file; ++ int teardown_sync_timeout; ++ ++ // state ++ State state; ++} MXLContext; ++ ++ ++#define OFFSET(x) offsetof(MXLContext, x) ++#define FLAGS AV_OPT_FLAG_ENCODING_PARAM | AV_OPT_FLAG_VIDEO_PARAM ++static const AVOption mxl_options[] = { ++ { ++ .name = "audio_flow_id", ++ .help = "MXL audio flow ID", ++ .offset = OFFSET(audio_flow_id), ++ .type = AV_OPT_TYPE_STRING, ++ .default_val = { .str = NULL }, ++ .flags = FLAGS, ++ }, ++ { ++ .name = "video_flow_id", ++ .help = "MXL video flow ID", ++ .offset = OFFSET(video_flow_id), ++ .type = AV_OPT_TYPE_STRING, ++ .default_val = { .str = NULL }, ++ .flags = FLAGS, ++ }, ++ { ++ .name = "teardown_sync_file", ++ .help = "wait for sentinel file to appear before destroying flow", ++ .offset = OFFSET(teardown_sync_file), ++ .type = AV_OPT_TYPE_STRING, ++ .flags = FLAGS ++ }, ++ { ++ .name = "teardown_sync_timeout", ++ .help = "maximum wait time in ms for teardown sync file", ++ .offset = OFFSET(teardown_sync_timeout), ++ .type = AV_OPT_TYPE_INT, ++ .default_val = { .i64 = 2000 }, ++ .min = 1, ++ .max = 10000, ++ .flags = FLAGS ++ }, ++ { NULL }, ++}; ++#undef OFFSET ++#undef FLAGS ++ ++static const AVClass mxl_muxer_class = { ++ .class_name = "mxl muxer", ++ .item_name = av_default_item_name, ++ .option = mxl_options, ++ .version = LIBAVUTIL_VERSION_INT, ++}; ++ ++static int validate_streams(AVFormatContext *s) ++{ ++ MXLContext *p = s->priv_data; ++ int exit_status = AVERROR_UNKNOWN; ++ ++ p->state.has_video_stream = false; ++ p->state.video.flow.detail = "video"; ++ p->state.video.flow.stream_index = -1; ++ ++ p->state.has_audio_stream = false; ++ p->state.audio.flow.detail = "audio"; ++ p->state.audio.flow.stream_index = -1; ++ ++ for(int i = 0; i < s->nb_streams; i++) { ++ AVStream *st = s->streams[i]; ++ ++ switch(st->codecpar->codec_type) { ++ case AVMEDIA_TYPE_VIDEO: ++ if (p->video_flow_id) { ++ if (st->codecpar->codec_id != AV_CODEC_ID_V210) { ++ loge(s, "unsupported video stream, codec must be v210\n"); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ // v210 requires even frame width ++ if (st->codecpar->width & 1) { ++ loge(s, "v210 width must be even, got %d\n", st->codecpar->width); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ p->state.has_video_stream = true; ++ p->state.video.flow.flow_id = p->video_flow_id; ++ p->state.video.flow.stream_index = i; ++ } ++ break; ++ case AVMEDIA_TYPE_AUDIO: ++ if (p->audio_flow_id) { ++ if (st->codecpar->codec_id != AV_CODEC_ID_PCM_F32LE) { ++ loge(s, "unsupported audio stream, codec must be f32le\n"); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ p->state.has_audio_stream = true; ++ p->state.audio.flow.flow_id = p->audio_flow_id; ++ p->state.audio.flow.stream_index = i; ++ } ++ break; ++ default: ++ logv(s, "ignoring unsupported stream %d (%s/%s)\n", ++ i, ++ av_get_media_type_string(st->codecpar->codec_type), ++ avcodec_get_name(st->codecpar->codec_id)); ++ break; ++ } ++ } ++ ++ if (!(p->state.has_video_stream || p->state.has_audio_stream)) { ++ loge(s, "no audio or video stream detected\n"); ++ exit_status = AVERROR_INVALIDDATA; ++ goto finally; ++ } ++ ++ exit_status = 0; ++ ++finally: ++ ++ return exit_status; ++} ++ ++typedef struct FlowDefFactoryResult { ++ // borrowed, immutable, must not be freed ++ const char * const flow_id; ++ ++ // owned by factory caller, mutable, must be freed by caller ++ char *flow_def_json; ++ ++ // borrowed, mutable, must not be freed or reassigned ++ FlowState * const flow_state; ++} FlowDefFactoryResult; ++ ++typedef FlowDefFactoryResult (*FlowDefFactory)(AVStream *st, MXLContext *p); ++ ++static int create_mxl_flow(AVFormatContext *s, AVStream *st, ++ mxlInstance mxl_instance, ++ FlowDefFactory flow_def_factory) ++{ ++ av_assert0(s && st && mxl_instance && flow_def_factory); ++ ++ MXLContext *p = s->priv_data; ++ ++ mxlFlowWriter flow_writer = NULL; ++ mxlFlowConfigInfo *flow_config_info = NULL; ++ ++ FlowDefFactoryResult factory_res = flow_def_factory(st, p); ++ av_assert0(factory_res.flow_id && factory_res.flow_def_json && factory_res.flow_state); ++ logv(s, "%s flow definition:\n%s\n", factory_res.flow_state->detail, factory_res.flow_def_json); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ flow_config_info = av_malloc(sizeof(*flow_config_info)); ++ if (!flow_config_info) { ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ *flow_config_info = (mxlFlowConfigInfo){0}; ++ ++ bool new_flow_created = false; ++ mxlStatus mxl_status = mxlCreateFlowWriter( ++ mxl_instance, factory_res.flow_def_json, ++ NULL, &flow_writer, flow_config_info, &new_flow_created); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlCreateFlowWriter error %s\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ logv(s, "mxlCreateFlowWriter opened %s flow\n", ++ new_flow_created ? "a new" : "an existing"); ++ ++ // all good, transfer state to context ++ factory_res.flow_state->mxl_flow_config_info = flow_config_info; ++ flow_config_info = NULL; ++ ++ factory_res.flow_state->mxl_flow_writer = flow_writer; ++ flow_writer = NULL; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (flow_writer) { ++ mxl_status = mxlReleaseFlowWriter(mxl_instance, flow_writer); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlReleaseFlowWriter error %s\n", mxl_status_to_str(mxl_status)); ++ flow_writer = NULL; ++ } ++ ++ av_free(flow_config_info); ++ av_free(factory_res.flow_def_json); ++ ++ return exit_status; ++} ++ ++static FlowDefFactoryResult video_flow_def_factory(AVStream *st, MXLContext* p) ++{ ++ av_assert0(st && p); ++ ++ AVRational fr = st->avg_frame_rate.num ? st->avg_frame_rate : st->r_frame_rate; ++ p->state.video.flow.mxl_grain_rate = (mxlRational){fr.num, fr.den}; ++ AVCodecParameters *par = st->codecpar; ++ p->state.video.flow_def_params = (VideoFlowDefParams) { ++ .id = p->state.video.flow.flow_id, ++ .grain_rate_num = fr.num, ++ .grain_rate_den = fr.den, ++ .frame_width = par->width, ++ .frame_height = par->height, ++ .y_width = par->width, ++ .y_height = par->height, ++ .y_bit_depth = 10, ++ .cb_width = par->width/2, ++ .cb_height = par->height, ++ .cb_bit_depth = 10, ++ .cr_width = par->width/2, ++ .cr_height = par->height, ++ .cr_bit_depth = 10 ++ }; ++ ++ return (FlowDefFactoryResult){ ++ .flow_id = p->state.video.flow.flow_id, ++ .flow_def_json = make_video_flow_def(&p->state.video.flow_def_params), ++ .flow_state = &p->state.video.flow}; ++} ++ ++static int create_video_flow(AVFormatContext *s, AVStream *st, ++ mxlInstance mxl_instance) ++{ ++ return create_mxl_flow(s, st, mxl_instance, video_flow_def_factory); ++} ++ ++static FlowDefFactoryResult audio_flow_def_factory(AVStream *st, MXLContext* p) ++{ ++ av_assert0(st && p); ++ ++ AVCodecParameters *par = st->codecpar; ++ p->state.audio.flow.mxl_grain_rate = (mxlRational){par->sample_rate, 1}; ++ p->state.audio.flow_def_params = (AudioFlowDefParams) { ++ .id = p->state.audio.flow.flow_id, ++ .sample_rate = par->sample_rate, ++ .channel_count = par->ch_layout.nb_channels ++ }; ++ ++ return (FlowDefFactoryResult){ ++ .flow_id = p->state.audio.flow.flow_id, ++ .flow_def_json = make_audio_flow_def(&p->state.audio.flow_def_params), ++ .flow_state = &p->state.audio.flow}; ++} ++ ++static int create_audio_flow(AVFormatContext *s, AVStream *st, ++ mxlInstance mxl_instance) ++{ ++ return create_mxl_flow(s, st, mxl_instance, audio_flow_def_factory); ++} ++ ++// create SwrContext for interleaved to planar audio conversion ++static int init_swr_context(struct SwrContext **swr_context, ++ int channels, int sample_rate) ++{ ++ AVChannelLayout in_layout, out_layout; ++ av_channel_layout_default(&in_layout, channels); ++ av_channel_layout_default(&out_layout, channels); ++ ++ int rc = swr_alloc_set_opts2(swr_context, ++ &out_layout, AV_SAMPLE_FMT_FLTP, sample_rate, // planar out ++ &in_layout, AV_SAMPLE_FMT_FLT, sample_rate, // interleaved in ++ 0, NULL); ++ if (rc < 0) ++ return rc; ++ ++ rc = swr_init(*swr_context); ++ if (rc < 0) { ++ swr_free(swr_context); ++ return rc; ++ } ++ ++ return 0; ++} ++ ++static int mxl_write_header(AVFormatContext *s) ++{ ++ av_assert0(s && s->priv_data); ++ ++ MXLContext *p = s->priv_data; ++ int exit_status = AVERROR_UNKNOWN; ++ ++ // inputs ++ const char *domain_path = s->url; ++ ++ // state ++ mxlInstance mxl_instance = NULL; ++ ++ if (!domain_path) { ++ loge(s, "domain not set\n"); ++ exit_status = AVERROR(EINVAL); ++ goto finally; ++ } ++ logv(s, "domain: \"%s\"\n", domain_path); ++ ++ int rc = validate_streams(s); ++ if (rc < 0) { ++ loge(s, "stream validation error\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ // post validation sanity ++ av_assert0(p->state.has_video_stream || p->state.has_audio_stream); ++ av_assert0(p->state.has_video_stream == !!p->state.video.flow.flow_id); ++ av_assert0(p->state.has_audio_stream == !!p->state.audio.flow.flow_id); ++ ++ mxl_instance = mxlCreateInstance(domain_path, NULL); ++ if (NULL == mxl_instance) { ++ logv(s, "mxlCreateInstance error\n"); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ if (p->state.has_video_stream) { ++ av_assert0(p->state.video.flow.stream_index >= 0 && ++ p->state.video.flow.flow_id && ++ p->state.video.flow.detail); ++ rc = create_video_flow(s, s->streams[p->state.video.flow.stream_index], mxl_instance); ++ if (rc < 0) { ++ loge(s, "error configuring mxl video flow\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ } ++ ++ if (p->state.has_audio_stream) { ++ av_assert0(p->state.audio.flow.stream_index >= 0 && ++ p->state.audio.flow.flow_id && ++ p->state.audio.flow.detail); ++ rc = create_audio_flow(s, s->streams[p->state.audio.flow.stream_index], mxl_instance); ++ if (rc < 0) { ++ loge(s, "error configuring mxl audio flow\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ rc = init_swr_context(&p->state.audio.swr_context, ++ p->state.audio.flow_def_params.channel_count, ++ p->state.audio.flow_def_params.sample_rate); ++ if (rc < 0) { ++ loge(s, "init_swr_context failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ } ++ ++ // all good, transfer state to context ++ p->domain_path = domain_path; ++ domain_path = NULL; ++ ++ p->state.mxl_instance = mxl_instance; ++ mxl_instance = NULL; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (mxl_instance) { ++ mxlStatus mxl_status = mxlDestroyInstance(mxl_instance); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlDestroyInstance error %s\n", mxl_status_to_str(mxl_status)); ++ } ++ ++ return exit_status; ++ ++} ++ ++static int video_write_packet(AVFormatContext *s, AVPacket *pkt) ++{ ++ int exit_status = AVERROR_UNKNOWN; ++ MXLContext *p = s->priv_data; ++ ++ if (0 == p->state.video.flow.mxl_start_index) { ++ uint64_t cur_index = mxlGetCurrentIndex(&p->state.video.flow.mxl_grain_rate); ++ av_assert0(MXL_UNDEFINED_INDEX != cur_index); ++ p->state.video.flow.mxl_start_index = cur_index; ++ p->state.video.flow.mxl_cur_index = cur_index; ++ logv(s, "video start index = %" PRIu64 "\n", cur_index); ++ } ++ ++ mxlStatus mxl_status = MXL_ERR_UNKNOWN; ++ mxlGrainInfo grain_info = {0}; ++ bool grain_open = false; ++ uint8_t *payload = NULL; ++ ++ logd(s, "write video grain for index = %" PRIu64 "\n", ++ p->state.video.flow.mxl_cur_index); ++ ++ mxl_status = mxlFlowWriterOpenGrain(p->state.video.flow.mxl_flow_writer, ++ p->state.video.flow.mxl_cur_index, ++ &grain_info, &payload); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlFlowWriterOpenGrain error %s\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ grain_open = true; ++ ++ size_t grain_size = grain_info.totalSlices * ++ p->state.video.flow.mxl_flow_config_info->discrete.sliceSizes[0]; ++ ++ if (pkt->size > grain_size) { ++ loge(s, "packet size exceeds grain size (%d > %zu)\n", ++ pkt->size, grain_size); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++ memcpy(payload, pkt->data, pkt->size); ++ ++ // mark all slices valid ++ grain_info.validSlices = grain_info.totalSlices; ++ ++ if (pkt->size < grain_info.grainSize) ++ logw(s, "grain under committed (pkt->size < grain_info.grainSize, %d < %" PRIu32 ")\n", ++ pkt->size, grain_info.grainSize); ++ ++ mxl_status = mxlFlowWriterCommitGrain(p->state.video.flow.mxl_flow_writer, &grain_info); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlFlowWriterCommitGrain error %s\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ grain_open = false; ++ p->state.video.flow.mxl_cur_index++; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (grain_open) { ++ mxl_status = mxlFlowWriterCancelGrain(p->state.video.flow.mxl_flow_writer); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlFlowWriterCancelGrain error %s\n", mxl_status_to_str(mxl_status)); ++ grain_open = false; ++ } ++ ++ return exit_status; ++} ++ ++#define USE_SWR_CONVERT ++#ifdef USE_SWR_CONVERT ++static int deinterleave_to_payload_fragments(AVFormatContext *s, ++ struct SwrContext *swr_context, ++ mxlMutableWrappedMultiBufferSlice *payload, ++ int fragment_idx, const float *src) { ++ ++ av_assert1(s && swr_context && payload && src); ++ ++ int exit_status = 0; ++ ++ size_t nb_samples = payload->base.fragments[fragment_idx].size/sizeof(float); ++ av_assert1(nb_samples*sizeof(float) == payload->base.fragments[fragment_idx].size); ++ ++ float **dst_array = (float**)av_malloc_array(payload->count, sizeof(*dst_array)); ++ if (!dst_array) { ++ logw(s, "deinterleave_to_payload_fragments malloc failed\n"); ++ exit_status = AVERROR(ENOMEM); ++ goto finally; ++ } ++ for (int channel = 0; channel < payload->count; ++channel) { ++ dst_array[channel] = (float*)((uint8_t*)payload->base.fragments[fragment_idx].pointer ++ + channel*payload->stride); ++ } ++ ++ // interleaved to planar convert ++ const uint8_t *src_array[1] = { (const uint8_t *)src }; ++ int rc = swr_convert(swr_context, ++ (uint8_t**)dst_array, nb_samples, ++ src_array, nb_samples); ++ if (rc < 0) { ++ loge(s, "swr_convert error %d\n", rc); ++ exit_status = rc; ++ goto finally; ++ } ++ else if (rc != nb_samples) { ++ loge(s, "incomplete fragment %d interleaved to planar conversion " ++ "(expected %d samples, actual %d samples)\n", fragment_idx, nb_samples, rc); ++ exit_status = AVERROR_BUG; ++ } ++ ++ exit_status = nb_samples; ++ ++finally: ++ ++ av_free(dst_array); ++ ++ return exit_status; ++} ++ ++ ++static int ffmpeg_packet_to_mxl_payload(AVFormatContext *s, ++ struct SwrContext *swr_context, ++ mxlMutableWrappedMultiBufferSlice *payload, ++ const AVPacket *pkt) ++{ ++ av_assert0(s && swr_context && payload && pkt); ++ ++ int exit_status = 0; ++ ++ float *src0 = (float*)(pkt->data); ++ int rc = deinterleave_to_payload_fragments(s, swr_context, payload, 0, src0); ++ if (rc < 0) { ++ exit_status = rc; ++ goto finally; ++ } ++ ++ int nb_samples0 = rc; ++ ++ if (payload->base.fragments[1].size > 0) { ++ float* src1 = src0 + nb_samples0*payload->count; ++ int rc = deinterleave_to_payload_fragments(s, swr_context, payload, 1, src1); ++ if (rc < 0) { ++ exit_status = rc; ++ goto finally; ++ } ++ ++ int nb_samples1 = rc; ++ (void)nb_samples1; ++ av_assert1((nb_samples0 + nb_samples1)*sizeof(float) == ++ (payload->base.fragments[0].size + payload->base.fragments[1].size)); ++ } ++ else { ++ av_assert1(nb_samples0*sizeof(float) == payload->base.fragments[0].size); ++ } ++ ++finally: ++ ++ return exit_status; ++} ++#else ++// unoptimized deinterlacer, retained for reference ++static int ffmpeg_packet_to_mxl_payload(AVFormatContext *s, ++ struct SwrContext *swr_context, ++ mxlMutableWrappedMultiBufferSlice *payload, ++ const AVPacket *pkt) ++{ ++ av_assert0(payload && pkt); ++ ++ for (int channel = 0; channel < payload->count; ++channel) { ++ float *src = (float*)(pkt->data + channel*sizeof(float)); ++ ++ float *dst0 = (float*)((uint8_t*)payload->base.fragments[0].pointer + channel*payload->stride); ++ size_t nb_samples0 = payload->base.fragments[0].size/sizeof(float); ++ av_assert0(nb_samples0*sizeof(float) == payload->base.fragments[0].size); ++ for(int i = 0; i < nb_samples0; i++) { ++ dst0[i] = *src; ++ src += payload->count; ++ } ++ ++ if (payload->base.fragments[1].size > 0) { ++ float *dst1 = (float*)((uint8_t*)payload->base.fragments[1].pointer + channel*payload->stride); ++ size_t nb_samples1 = payload->base.fragments[1].size/sizeof(float); ++ av_assert0(nb_samples1*sizeof(float) == payload->base.fragments[1].size); ++ for(int i = 0; i < nb_samples1; i++) { ++ dst1[i] = *src; ++ src += payload->count; ++ } ++ } ++ } ++ ++ return 0; ++} ++#endif ++ ++static int audio_write_packet(AVFormatContext *s, AVPacket *pkt) ++{ ++ int exit_status = AVERROR_UNKNOWN; ++ MXLContext *p = s->priv_data; ++ ++ av_assert0(s->streams[pkt->stream_index]->codecpar->ch_layout.nb_channels == ++ p->state.audio.flow_def_params.channel_count); ++ av_assert0(pkt->duration <= ++ pkt->size / p->state.audio.flow_def_params.channel_count / sizeof(float)); ++ ++ if (0 == p->state.audio.flow.mxl_start_index) { ++ uint64_t cur_index = mxlGetCurrentIndex(&p->state.audio.flow.mxl_grain_rate); ++ av_assert0(MXL_UNDEFINED_INDEX != cur_index); ++ p->state.audio.flow.mxl_start_index = cur_index; ++ p->state.audio.flow.mxl_cur_index = cur_index; ++ logv(s, "audio start index = %" PRIu64 "\n", cur_index); ++ } ++ ++ mxlStatus mxl_status = MXL_ERR_UNKNOWN; ++ bool samples_open = false; ++ size_t sample_count = pkt->duration; ++ ++ logd(s, "write audio samples for index = %" PRIu64 ", sample_count = %zu\n", ++ p->state.audio.flow.mxl_cur_index, sample_count); ++ ++ mxlMutableWrappedMultiBufferSlice payload = (mxlMutableWrappedMultiBufferSlice){0}; ++ mxl_status = mxlFlowWriterOpenSamples(p->state.audio.flow.mxl_flow_writer, ++ p->state.audio.flow.mxl_cur_index, ++ sample_count, &payload); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlFlowWriterOpenSamples error %s\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EAGAIN); ++ goto finally; ++ } ++ ++ samples_open = true; ++ ++ int rc = ffmpeg_packet_to_mxl_payload(s, p->state.audio.swr_context, &payload, pkt); ++ if (rc < 0) { ++ loge(s, "audio packet to MXL payload conversion failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ mxl_status = mxlFlowWriterCommitSamples(p->state.audio.flow.mxl_flow_writer); ++ if (MXL_STATUS_OK != mxl_status) { ++ loge(s, "mxlFlowWriterCommitSamples error %s\n", mxl_status_to_str(mxl_status)); ++ exit_status = AVERROR(EIO); ++ goto finally; ++ } ++ ++ samples_open = false; ++ p->state.audio.flow.mxl_cur_index += sample_count; ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (samples_open) { ++ mxl_status = mxlFlowWriterCancelSamples(p->state.audio.flow.mxl_flow_writer); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlFlowWriterCancelSamples error %s\n", mxl_status_to_str(mxl_status)); ++ samples_open = false; ++ } ++ ++ return exit_status; ++} ++ ++static int mxl_write_packet(AVFormatContext *s, AVPacket *pkt) ++{ ++ av_assert0(s && pkt); ++ ++ MXLContext *p = s->priv_data; ++ AVStream *st = s->streams[pkt->stream_index]; ++ av_assert0(st); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ switch(st->codecpar->codec_type) { ++ case AVMEDIA_TYPE_VIDEO: ++ av_assert0(p->state.has_video_stream); ++ exit_status = video_write_packet(s, pkt); ++ if (exit_status < 0) { ++ loge(s, "video_write_packet error\n"); ++ goto finally; ++ } ++ break; ++ case AVMEDIA_TYPE_AUDIO: ++ av_assert0(p->state.has_audio_stream); ++ exit_status = audio_write_packet(s, pkt); ++ if (exit_status < 0) { ++ loge(s, "audio_write_packet error\n"); ++ goto finally; ++ } ++ break; ++ default: ++ loge(s, "unexpected media type\n"); ++ exit_status = AVERROR_BUG; ++ goto finally; ++ } ++ ++finally: ++ ++ return exit_status; ++} ++ ++typedef enum { ++ SENTINEL_APPEARED = 0, ++ SENTINEL_TIMEOUT = -1 ++} SentinelStatus; ++ ++static int wait_for_sentinel(AVFormatContext *s, const char *path, int timeout_ms) ++{ ++ const int poll_ms = 100; ++ const int poll_us = 1000*poll_ms; ++ const int64_t start = av_gettime_relative(); ++ const int64_t timeout_us = 1000*timeout_ms; ++ int status = 0; ++ ++ while ((status = avio_check(path, AVIO_FLAG_READ)) < 0) { ++ if (av_gettime_relative() - start > timeout_us) ++ return SENTINEL_TIMEOUT; ++ logv(s, "waiting for sentinel: %s\n", path); ++ av_usleep(poll_us); ++ } ++ return SENTINEL_APPEARED; ++} ++ ++static const char *sentinel_status_to_str(SentinelStatus st) ++{ ++ switch (st) { ++ case SENTINEL_APPEARED: ++ return "SENTINEL_APPEARED"; ++ case SENTINEL_TIMEOUT: ++ return "SENTINEL_TIMEOUT"; ++ default: ++ return "UNKNOWN"; ++ } ++} ++ ++static void flow_teardown(AVFormatContext *s, FlowState *flow_state) ++{ ++ av_assert0(s && flow_state); ++ ++ MXLContext *p = s->priv_data; ++ ++ if (flow_state->mxl_flow_writer) { ++ av_assert0(p->state.mxl_instance); ++ mxlStatus mxl_status = mxlReleaseFlowWriter(p->state.mxl_instance, ++ flow_state->mxl_flow_writer); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlReleaseFlowWriter %s error %s\n", ++ flow_state->detail, mxl_status_to_str(mxl_status)); ++ flow_state->mxl_flow_writer = NULL; ++ } ++ ++ av_free(flow_state->mxl_flow_config_info); ++ flow_state->mxl_flow_config_info = NULL; ++} ++ ++static int mxl_write_trailer(AVFormatContext *s) ++{ ++ av_assert0(s && s->priv_data); ++ ++ MXLContext *p = s->priv_data; ++ ++ mxlStatus mxl_status = MXL_ERR_UNKNOWN; ++ ++ if (p->teardown_sync_file) { ++ av_assert0(p->teardown_sync_timeout > 0); ++ SentinelStatus sentinel_status = ++ wait_for_sentinel(s, p->teardown_sync_file, p->teardown_sync_timeout); ++ logv(s, "sentinel status = %s\n", sentinel_status_to_str(sentinel_status)); ++ } ++ ++ if (p->state.has_video_stream) { ++ flow_teardown(s, &p->state.video.flow); ++ } ++ ++ if (p->state.has_audio_stream) { ++ flow_teardown(s, &p->state.audio.flow); ++ swr_free(&p->state.audio.swr_context); ++ } ++ ++ if (p->state.mxl_instance) { ++ mxl_status = mxlDestroyInstance(p->state.mxl_instance); ++ if (MXL_STATUS_OK != mxl_status) ++ logw(s, "mxlDestroyInstance error %s\n", mxl_status_to_str(mxl_status)); ++ p->state.mxl_instance = NULL; ++ } ++ ++ return 0; ++} ++ ++static int mxl_query_codec(enum AVCodecID id, int std_compliance) ++{ ++ switch (id) { ++ case AV_CODEC_ID_PCM_F32LE: ++ case AV_CODEC_ID_V210: ++ return 1; ++ default: ++ return 0; ++ } ++} ++ ++// MXL muxer definition ++const FFOutputFormat ff_mxl_muxer = { ++ // AVOutputFormat (public-facing fields) ++ .p.name = "mxl", ++ .p.long_name = NULL_IF_CONFIG_SMALL(MXL_LONG_NAME), ++ .p.extensions = MXL_FLOW_EXT, ++ .p.priv_class = &mxl_muxer_class, ++ .p.flags = AVFMT_NOFILE, ++ ++ .p.audio_codec = AV_CODEC_ID_NONE, ++ .p.video_codec = AV_CODEC_ID_V210, ++ ++ // private state ++ .priv_data_size = sizeof(MXLContext), ++ ++ // muxer callbacks ++ .write_header = mxl_write_header, ++ .write_packet = mxl_write_packet, ++ .write_trailer = mxl_write_trailer, ++ .query_codec = mxl_query_codec, ++}; +diff --git a/libavformat/tests/mxl_diag.c b/libavformat/tests/mxl_diag.c +new file mode 100644 +index 0000000000..699f845ebe +--- /dev/null ++++ b/libavformat/tests/mxl_diag.c +@@ -0,0 +1,1471 @@ ++/* ++ * MXL runtime diagnostics tests for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "libavformat/mxl_diag.h" ++ ++#include ++#include "libavutil/avassert.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define TEST_DIR "/tmp/mxl_diag_test" ++ ++// simulated frame rate ++enum { FPS = 30 }; ++ ++/* ++ * file descriptor leak detection ++ */ ++enum { MAX_TRACKED_FDS = 8, ++ MAX_SCANNED_FDS = 64 }; ++static int startup_open_fds[MAX_TRACKED_FDS]; ++static int startup_open_fd_count; ++ ++static int capture_startup_open_fds(void) ++{ ++ int fd; ++ ++ startup_open_fd_count = 0; ++ ++ for (fd = 0; fd < MAX_SCANNED_FDS; ++fd) { ++ errno = 0; ++ if (fcntl(fd, F_GETFD) != -1 || errno != EBADF) { ++ if (startup_open_fd_count >= MAX_TRACKED_FDS) { ++ fprintf(stderr, "too many open fds at startup\n"); ++ return -1; ++ } ++ startup_open_fds[startup_open_fd_count++] = fd; ++ } ++ } ++ ++ return 0; ++} ++ ++static int fd_is_in_startup_set(int fd) ++{ ++ int i; ++ ++ for (i = 0; i < startup_open_fd_count; ++i) { ++ if (startup_open_fds[i] == fd) ++ return 1; ++ } ++ ++ return 0; ++} ++ ++static int check_open_fds_match_startup(void) ++{ ++ int fd; ++ ++ for (fd = 0; fd < MAX_SCANNED_FDS; ++fd) { ++ int is_open; ++ ++ errno = 0; ++ is_open = (fcntl(fd, F_GETFD) != -1 || errno != EBADF); ++ ++ if (is_open && !fd_is_in_startup_set(fd)) { ++ fprintf(stderr, "unexpected open fd: %d\n", fd); ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++/* ++ * test diag client ++ */ ++typedef struct mxl_diag_client { ++ int fd; ++ char client_path[sizeof(((struct sockaddr_un *)0)->sun_path)]; ++} mxl_diag_client; ++ ++static inline void mxl_diag_client_init_msg_connect(mxl_diag_msg *msg) ++{ ++ av_assert1(msg); ++ int size = sizeof(msg->header); ++ memset(msg, 0, size); ++ msg->header.version = MXL_DIAG_MSG_VERSION; ++ msg->header.type = MXL_DIAG_MSG_CONNECT; ++ msg->header.size = size; ++} ++ ++static inline void mxl_diag_client_init_msg_release(mxl_diag_msg *msg) ++{ ++ av_assert1(msg); ++ int size = sizeof(msg->header); ++ memset(msg, 0, size); ++ msg->header.version = MXL_DIAG_MSG_VERSION; ++ msg->header.type = MXL_DIAG_MSG_RELEASE; ++ msg->header.size = size; ++} ++ ++static int mxl_diag_client_connect(mxl_diag_client *client, ++ const char *server_path, ++ const char *client_path) ++{ ++ struct sockaddr_un local_addr; ++ struct sockaddr_un server_addr; ++ mxl_diag_msg msg; ++ ++ if (!client || !server_path || !client_path) { ++ return AVERROR(EINVAL); ++ } ++ ++ memset(client, 0, sizeof(*client)); ++ client->fd = -1; ++ ++ if (strlen(server_path) >= sizeof(server_addr.sun_path) || ++ strlen(client_path) >= sizeof(local_addr.sun_path)) { ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ client->fd = socket(AF_UNIX, SOCK_DGRAM, 0); ++ if (client->fd < 0) { ++ return AVERROR(errno); ++ } ++ ++ memset(&local_addr, 0, sizeof(local_addr)); ++ local_addr.sun_family = AF_UNIX; ++ strcpy(local_addr.sun_path, client_path); ++ ++ memset(&server_addr, 0, sizeof(server_addr)); ++ server_addr.sun_family = AF_UNIX; ++ strcpy(server_addr.sun_path, server_path); ++ ++ strncpy(client->client_path, client_path, sizeof(client->client_path) - 1); ++ client->client_path[sizeof(client->client_path) - 1] = '\0'; ++ ++ unlink(client_path); ++ ++ if (bind(client->fd, (const struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { ++ int err = errno; ++ close(client->fd); ++ client->fd = -1; ++ unlink(client_path); ++ return AVERROR(err); ++ } ++ ++ if (connect(client->fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ++ int err = errno; ++ close(client->fd); ++ client->fd = -1; ++ unlink(client_path); ++ return AVERROR(err); ++ } ++ ++ mxl_diag_client_init_msg_connect(&msg); ++ ++ if (send(client->fd, &msg, msg.header.size, 0) < 0) { ++ int err = errno; ++ close(client->fd); ++ client->fd = -1; ++ unlink(client_path); ++ return AVERROR(err); ++ } ++ ++ return 0; ++} ++ ++static int mxl_diag_client_release(mxl_diag_client *client) ++{ ++ mxl_diag_msg msg; ++ ++ av_assert0(client); ++ ++ if (!client) { ++ return AVERROR(EINVAL); ++ } ++ ++ if (client->fd < 0) { ++ return AVERROR(EBADF); ++ } ++ ++ mxl_diag_client_init_msg_release(&msg); ++ ++ if (send(client->fd, &msg, msg.header.size, 0) < 0) { ++ return AVERROR(errno); ++ } ++ ++ return 0; ++} ++ ++static int mxl_diag_client_close(mxl_diag_client *client) ++{ ++ int rc = 0; ++ ++ av_assert0(client); ++ ++ if (!client) { ++ return AVERROR(EINVAL); ++ } ++ ++ if (client->fd >= 0) { ++ rc = mxl_diag_client_release(client); ++ ++ if (close(client->fd) < 0 && rc >= 0) { ++ rc = AVERROR(errno); ++ } ++ client->fd = -1; ++ } ++ ++ if (client->client_path[0] != '\0') { ++ if (unlink(client->client_path) < 0 && errno != ENOENT && rc >= 0) { ++ rc = AVERROR(errno); ++ } ++ client->client_path[0] = '\0'; ++ } ++ ++ return rc; ++} ++ ++static int mxl_diag_client_recv(mxl_diag_client *client, ++ mxl_diag_msg *msg) ++{ ++ const int timeout_ms = 100; ++ ++ struct pollfd pfd; ++ ssize_t n; ++ int ret; ++ ++ if (!client || !msg) { ++ return AVERROR(EINVAL); ++ } ++ ++ pfd.fd = client->fd; ++ pfd.events = POLLIN; ++ pfd.revents = 0; ++ ++ ret = poll(&pfd, 1, timeout_ms); ++ if (ret < 0) { ++ return AVERROR(errno); ++ } ++ ++ if (ret == 0) { ++ return AVERROR(ETIMEDOUT); /* timeout */ ++ } ++ ++ n = recv(client->fd, msg, sizeof(*msg), 0); ++ if (n < 0) { ++ return AVERROR(errno); ++ } ++ ++ if ((size_t)n < sizeof(msg->header)) { ++ return AVERROR_INVALIDDATA; ++ } ++ ++ if (msg->header.size < sizeof(msg->header)) { ++ return AVERROR_INVALIDDATA; ++ } ++ ++ if ((size_t)n < msg->header.size) { ++ return AVERROR_INVALIDDATA; ++ } ++ ++ return 0; ++} ++ ++/* ++ * asynchronous tests ++ */ ++ ++static void print_message(const mxl_diag_msg *msg) ++{ ++ if (!msg) { ++ return; ++ } ++ ++ printf("msg: version=%"PRIu16", type=%"PRIu16", size=%"PRIu32"\n", ++ msg->header.version, ++ msg->header.type, ++ msg->header.size); ++ ++ switch (msg->header.type) { ++ case MXL_DIAG_MSG_VIDEO_READ: ++ printf("video_read: timestamp = %"PRIu64 ++ ", exec_dur = %"PRIu64 ++ ", tail_index = %"PRIu64 ++ ", head_index = %"PRIu64", read_index = %"PRIu64 ++ ", mxl_status = %"PRIu32"\n", ++ msg->u.video_read.timestamp, ++ msg->u.video_read.exec_dur, ++ msg->u.video_read.tail_index, ++ msg->u.video_read.head_index, ++ msg->u.video_read.read_index, ++ msg->u.video_read.mxl_status); ++ break; ++ case MXL_DIAG_MSG_AUDIO_READ: ++ printf("audio_read: timestamp = %"PRIu64 ++ ", exec_dur = %"PRIu64 ++ ", tail_index = %"PRIu64 ++ ", head_index = %"PRIu64", read_index = %"PRIu64 ++ ", read_size = %"PRIu32", mxl_status = %"PRIu32"\n", ++ msg->u.audio_read.timestamp, ++ msg->u.audio_read.exec_dur, ++ msg->u.audio_read.tail_index, ++ msg->u.audio_read.head_index, ++ msg->u.audio_read.read_index, ++ msg->u.audio_read.read_size, ++ msg->u.audio_read.mxl_status); ++ break; ++ default: ++ fprintf(stderr, "payload=\n"); ++ av_assert0(false); ++ break; ++ } ++} ++ ++// retry connection every millisecond for 1/FPS seconds ++static int test_client_connect(mxl_diag_client* client, ++ const char* server_path, ++ const char* client_path) ++{ ++ int rc = -1; ++ int retry = 0; ++ do { ++ rc = mxl_diag_client_connect(client, server_path, client_path); ++ if (rc) ++ usleep(1000); ++ } ++ while(rc && retry++ < 1000/FPS); ++ ++ return rc; ++} ++ ++static int test_client_print_messages(const char *server_path) ++{ ++ printf("\ntest: print message stream\n"); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ const char *client_path = TEST_DIR "/test.client.sock"; ++ ++ mxl_diag_client client = {-1, ""}; ++ mxl_diag_msg msg; ++ ++ const int max_messages = 10; ++ ++ int rc = test_client_connect(&client, server_path, client_path); ++ if (rc) { ++ fprintf(stderr, "client connection failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ for (int recv_count = 0; recv_count < max_messages; recv_count++) { ++ rc = mxl_diag_client_recv(&client, &msg); ++ if (rc) { ++ exit_status = rc; ++ goto finally; ++ } ++ print_message(&msg); ++ } ++ ++ rc = mxl_diag_client_close(&client); ++ if (rc) { ++ fprintf(stderr, "client close failed\n"); ++ exit_status = rc; ++ goto finally; ++ } ++ ++ exit_status = 0; ++ ++finally: ++ ++ if (client.fd >= 0) { ++ rc = mxl_diag_client_close(&client); ++ av_assert0(rc == 0); ++ } ++ ++ ++ return exit_status; ++} ++ ++ ++static int test_client_multi_connect(const char* server_path) ++{ ++ printf("\ntest: multiple connect attempts are ignored after the first\n"); ++ ++ const char *client_path_a = TEST_DIR "/test.a.client.sock"; ++ const char *client_path_b = TEST_DIR "/test.b.client.sock"; ++ const char *client_path_c = TEST_DIR "/test.c.client.sock"; ++ ++ // client A should connect and receive a message ++ mxl_diag_client client_a; ++ int rc = test_client_connect(&client_a, server_path, client_path_a); ++ if (rc) { ++ fprintf(stderr, "client A connection failed (%d)\n", rc); ++ return rc; ++ } ++ printf("client A connected\n"); ++ mxl_diag_msg msg_a_1 = {0}; ++ rc = mxl_diag_client_recv(&client_a, &msg_a_1); ++ if (rc) { ++ fprintf(stderr, "client A first recv failed (%d)\n", rc); ++ return rc; ++ } ++ print_message(&msg_a_1); ++ ++ // reconnect is harmless ++ { ++ mxl_diag_msg msg = {0}; ++ ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client A re-connect failed (%d)\n", rc); ++ return rc; ++ } ++ } ++ ++ // client B should connect and be ignored ++ mxl_diag_client client_b; ++ rc = test_client_connect(&client_b, server_path, client_path_b); ++ if (rc) { ++ fprintf(stderr, "client B connection failed (%d)\n", rc); ++ return rc; ++ } ++ mxl_diag_msg msg_b_1 = {0}; ++ rc = mxl_diag_client_recv(&client_b, &msg_b_1); ++ if (rc != AVERROR(ETIMEDOUT)) { ++ fprintf(stderr, "client B recv failed to timeout (%d)\n", rc); ++ return rc; ++ } ++ printf("client B timed out (expected)\n"); ++ // releasing b without closing should not affect a ++ rc = mxl_diag_client_release(&client_b); ++ if (rc) { ++ fprintf(stderr, "client B release failed"); ++ return rc; ++ } ++ ++ // client A should still work ++ mxl_diag_msg msg_a_2 = {0}; ++ rc = mxl_diag_client_recv(&client_a, &msg_a_2); ++ if (rc) { ++ fprintf(stderr, "client A second recv failed (%d)\n", rc); ++ return rc; ++ } ++ print_message(&msg_a_2); ++ printf("client A was unaffected by client B\n"); ++ ++ // client C should connect and be ignored ++ mxl_diag_client client_c; ++ rc = test_client_connect(&client_c, server_path, client_path_c); ++ if (rc) { ++ fprintf(stderr, "client C connection failed (%d)\n", rc); ++ return rc; ++ } ++ mxl_diag_msg msg_c = {0}; ++ rc = mxl_diag_client_recv(&client_c, &msg_c); ++ if (rc != AVERROR(ETIMEDOUT)) { ++ fprintf(stderr, "client C recv failed to timeout (%d)\n", rc); ++ return rc; ++ } ++ printf("client C timed out (expected)\n"); ++ ++ // closing C not affect A ++ rc = mxl_diag_client_close(&client_c); ++ if (rc) { ++ fprintf(stderr, "client C close failed"); ++ return rc; ++ } ++ ++ // client A should still work ++ mxl_diag_msg msg_a_3 = {0}; ++ rc = mxl_diag_client_recv(&client_a, &msg_a_3); ++ if (rc) { ++ fprintf(stderr, "client A second recv failed (%d)\n", rc); ++ return rc; ++ } ++ print_message(&msg_a_3); ++ printf("client A was unaffected by client C\n"); ++ ++ // client B should still timeout ++ mxl_diag_msg msg_b_2 = {0}; ++ rc = mxl_diag_client_recv(&client_b, &msg_b_2); ++ if (rc != AVERROR(ETIMEDOUT)) { ++ fprintf(stderr, "client B recv failed to timeout (%d)\n", rc); ++ return rc; ++ } ++ printf("client B still times out\n"); ++ rc = mxl_diag_client_close(&client_b); ++ av_assert0(rc == 0); ++ ++ rc = mxl_diag_client_close(&client_a); ++ av_assert0(rc == 0); ++ ++ return 0; ++} ++ ++static int test_client_malformed(const char *server_path) ++{ ++ const int malformed_case_count = 7; ++ const int n = malformed_case_count * 2; ++ ++ const char *client_path_a = TEST_DIR "/test.malformed.a.client.sock"; ++ ++ mxl_diag_client client_a; ++ int rc; ++ int i; ++ ++ printf("\ntest: malformed messages from active client are ignored\n"); ++ ++ rc = test_client_connect(&client_a, server_path, client_path_a); ++ if (rc) { ++ fprintf(stderr, "client A connection failed (%d)\n", rc); ++ return rc; ++ } ++ ++ // Confirm A is active. ++ { ++ mxl_diag_msg msg = {0}; ++ rc = mxl_diag_client_recv(&client_a, &msg); ++ if (rc) { ++ fprintf(stderr, "client A initial recv failed (%d)\n", rc); ++ mxl_diag_client_close(&client_a); ++ return rc; ++ } ++ print_message(&msg); ++ } ++ ++ for (i = 0; i < n; ++i) { ++ mxl_diag_msg msg; ++ const void *buf; ++ size_t len; ++ ++ memset(&msg, 0, sizeof(msg)); ++ ++ switch (i % malformed_case_count) { ++ case 0: ++ printf("bad size, message too short\n"); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ buf = &msg; ++ len = sizeof(msg.header)-1; ++ break; ++ ++ case 1: ++ printf("bad size, header size sanity (too small)\n"); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg.header) - 1; ++ buf = &msg; ++ len = sizeof(msg.header); ++ break; ++ ++ case 2: ++ printf("bad size, header size sanity (too big)\n"); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg)+1; ++ buf = &msg; ++ len = sizeof(msg.header); ++ break; ++ ++ case 3: { ++ // this will appear as a "message too short" error in the log ++ printf("unknown type, truncated header\n"); ++ static const unsigned char short_buf[sizeof(msg.header) - 1] = {0}; ++ buf = short_buf; ++ len = sizeof(short_buf); ++ break; ++ } ++ ++ case 4: ++ printf("bad version\n"); ++ msg.header.version = MXL_DIAG_MSG_VERSION + 1; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ buf = &msg; ++ len = msg.header.size; ++ break; ++ ++ case 5: ++ printf("unknown type\n"); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = 9999; ++ msg.header.size = sizeof(msg.header); ++ buf = &msg; ++ len = msg.header.size; ++ break; ++ ++ case 6: { ++ printf("garbage message\n"); ++ unsigned char garbage[128]; ++ size_t j; ++ ++ len = 1 + (size_t)(rand() % sizeof(garbage)); ++ for (j = 0; j < len; ++j) { ++ garbage[j] = (unsigned char)(rand() & 0xff); ++ } ++ ++ buf = garbage; ++ break; ++ } ++ ++ default: ++ av_assert0(false); ++ } ++ ++ if (send(client_a.fd, buf, len, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client A malformed send %d failed (%d)\n", i, rc); ++ mxl_diag_client_close(&client_a); ++ return rc; ++ } ++ } ++ ++ printf("sent %d malformed messages from active client A\n", n); ++ ++ // give the test server time to catch up ++ sleep(n/FPS + 1); ++ ++ // A should still receive normal messages afterward. ++ { ++ mxl_diag_msg msg = {0}; ++ rc = mxl_diag_client_recv(&client_a, &msg); ++ if (rc) { ++ fprintf(stderr, "client A recv after malformed traffic failed (%d)\n", rc); ++ mxl_diag_client_close(&client_a); ++ return rc; ++ } ++ print_message(&msg); ++ printf("client A unaffected by malformed traffic\n"); ++ } ++ ++ rc = mxl_diag_client_close(&client_a); ++ if (rc) { ++ fprintf(stderr, "client A close failed (%d)\n", rc); ++ return rc; ++ } ++ ++ return 0; ++} ++ ++static void test_server_init_msg(mxl_diag_msg *msg, uint64_t base_index) ++{ ++ if (!msg) { ++ return; ++ } ++ ++ if (base_index%2) { ++ mxl_diag_init_msg_video_read( ++ msg, base_index, base_index + 1, base_index + 2, base_index + 3, base_index + 4, ++ MXL_STATUS_OK); ++ } ++ else { ++ mxl_diag_init_msg_audio_read( ++ msg, base_index, base_index + 1, base_index + 2, base_index + 3, base_index + 4, ++ 48000/FPS, MXL_STATUS_OK); ++ } ++} ++ ++typedef struct server_thread { ++ pthread_t thread; ++ pthread_mutex_t mutex; ++ bool stop; ++ bool started; ++ const char* server_path; ++} server_thread; ++ ++// send one message each simulated frame period ++static void *test_server_thread_main(void *arg) ++{ ++ server_thread *t = arg; ++ ++ mxl_diag_server server = {0}; ++ int rc = mxl_diag_init(NULL, &server, t->server_path); ++ if (rc) { ++ fprintf(stderr, "mxl_diag_init failed (%d)\n", rc); ++ return NULL; ++ } ++ ++ static uint64_t base_index = 0; ++ bool stop = false; ++ do { ++ usleep(1000000/FPS); ++ mxl_diag_msg msg = {0}; ++ test_server_init_msg(&msg, base_index++); ++ ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc < 0) ++ fprintf(stderr, "test_server_thread_main mxl_diag_poll error %d\n", rc); ++ ++ rc = mxl_diag_send(NULL, &server, &msg); ++ if (rc < 0) ++ fprintf(stderr, "test_server_thread_main mxl_diag_send error %d\n", rc); ++ ++ pthread_mutex_lock(&t->mutex); ++ stop = t->stop; ++ pthread_mutex_unlock(&t->mutex); ++ } while (!stop); ++ ++ mxl_diag_close(NULL, &server); ++ ++ return NULL; ++} ++ ++ ++static int test_server_thread_start(server_thread *t, ++ const char* server_path) ++{ ++ av_assert0(t && server_path); ++ ++ int exit_status = AVERROR_UNKNOWN; ++ ++ t->stop = false; ++ t->started = false; ++ t->server_path = server_path; ++ t->thread = 0; ++ pthread_mutex_init(&t->mutex, NULL); ++ ++ int rc = pthread_create(&t->thread, NULL, test_server_thread_main, t); ++ if (rc != 0) { ++ exit_status = AVERROR(rc); ++ goto finally; ++ } ++ ++ t->started = true; ++ exit_status = 0; ++ ++finally: ++ ++ return exit_status; ++} ++ ++static int test_server_thread_stop(server_thread *t) ++{ ++ av_assert0(t && t->started); ++ ++ pthread_mutex_lock(&t->mutex); ++ t->stop = true; ++ pthread_mutex_unlock(&t->mutex); ++ ++ int rc = pthread_join(t->thread, NULL); ++ if (rc) { ++ fprintf(stderr, "pthread_join error (%d)\n", rc); ++ return AVERROR(rc); ++ } ++ ++ pthread_mutex_destroy(&t->mutex); ++ ++ t->started = false; ++ ++ return 0; ++} ++ ++/* ++ * synchronous tests ++ */ ++static int test_server_path_collision_non_socket(const char *server_path) ++{ ++ mxl_diag_server server; ++ FILE *fp; ++ int rc; ++ ++ printf("\ntest: init fails if server path exists as a non-socket\n"); ++ ++ unlink(server_path); ++ ++ fp = fopen(server_path, "w"); ++ if (!fp) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "fopen failed (%d)\n", rc); ++ return rc; ++ } ++ fclose(fp); ++ ++ rc = mxl_diag_init(NULL, &server, server_path); ++ if (rc != AVERROR(EEXIST)) { ++ fprintf(stderr, "expected EEXIST, got (%d)\n", rc); ++ unlink(server_path); ++ return rc ? rc : AVERROR(EINVAL); ++ } ++ ++ if (access(server_path, F_OK) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "server path sanity failed (%d)\n", rc); ++ return rc; ++ } ++ ++ unlink(server_path); ++ ++ return 0; ++} ++ ++static int test_server_path_stale_socket_cleanup(const char *server_path) ++{ ++ mxl_diag_server server; ++ int stale_fd = -1; ++ struct sockaddr_un addr; ++ int rc; ++ ++ printf("\ntest: init cleans up stale socket path\n"); ++ ++ unlink(server_path); ++ ++ if (strlen(server_path) >= sizeof(addr.sun_path)) { ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ stale_fd = socket(AF_UNIX, SOCK_DGRAM, 0); ++ if (stale_fd < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "socket failed (%d)\n", rc); ++ return rc; ++ } ++ ++ memset(&addr, 0, sizeof(addr)); ++ addr.sun_family = AF_UNIX; ++ strcpy(addr.sun_path, server_path); ++ ++ if (bind(stale_fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "bind stale socket failed (%d)\n", rc); ++ close(stale_fd); ++ return rc; ++ } ++ ++ close(stale_fd); ++ stale_fd = -1; ++ ++ rc = mxl_diag_init(NULL, &server, server_path); ++ if (rc) { ++ fprintf(stderr, "mxl_diag_init failed (%d)\n", rc); ++ unlink(server_path); ++ return rc; ++ } ++ ++ if (access(server_path, F_OK) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "server path missing after init (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ mxl_diag_close(NULL, &server); ++ ++ if (access(server_path, F_OK) == 0 || errno != ENOENT) { ++ fprintf(stderr, "server path still exists after close\n"); ++ unlink(server_path); ++ return AVERROR(EINVAL); ++ } ++ ++ return 0; ++} ++ ++static int test_release_with_no_active_client(const char *server_path) ++{ ++ const char *client_path_a = TEST_DIR "/test.no_active_release.a.client"; ++ ++ mxl_diag_server server; ++ mxl_diag_client client_a; ++ struct sockaddr_un local_addr; ++ struct sockaddr_un server_addr; ++ mxl_diag_msg msg; ++ int rc; ++ ++ printf("\ntest: release with no active client is ignored\n"); ++ ++ rc = mxl_diag_init(NULL, &server, server_path); ++ if (rc) { ++ fprintf(stderr, "server init failed (%d)\n", rc); ++ return rc; ++ } ++ ++ memset(&client_a, 0, sizeof(client_a)); ++ client_a.fd = -1; ++ ++ if (strlen(server_path) >= sizeof(server_addr.sun_path) || ++ strlen(client_path_a) >= sizeof(local_addr.sun_path)) { ++ mxl_diag_close(NULL, &server); ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ client_a.fd = socket(AF_UNIX, SOCK_DGRAM, 0); ++ if (client_a.fd < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client socket failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ memset(&local_addr, 0, sizeof(local_addr)); ++ local_addr.sun_family = AF_UNIX; ++ strcpy(local_addr.sun_path, client_path_a); ++ ++ memset(&server_addr, 0, sizeof(server_addr)); ++ server_addr.sun_family = AF_UNIX; ++ strcpy(server_addr.sun_path, server_path); ++ ++ strncpy(client_a.client_path, client_path_a, sizeof(client_a.client_path) - 1); ++ client_a.client_path[sizeof(client_a.client_path) - 1] = '\0'; ++ ++ unlink(client_path_a); ++ ++ if (bind(client_a.fd, (const struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client bind failed (%d)\n", rc); ++ close(client_a.fd); ++ client_a.fd = -1; ++ unlink(client_path_a); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ if (connect(client_a.fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client connect failed (%d)\n", rc); ++ close(client_a.fd); ++ client_a.fd = -1; ++ unlink(client_path_a); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ /* aberrant condition under test: sending release to idle server */ ++ av_assert0(!server.has_client); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg.header); ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client release send failed (%d)\n", rc); ++ mxl_diag_client_close(&client_a); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ mxl_diag_poll(NULL, &server); ++ if (server.has_client) { ++ fprintf(stderr, "server unexpectedly activated after idle release\n"); ++ mxl_diag_client_close(&client_a); ++ mxl_diag_close(NULL, &server); ++ return AVERROR(EINVAL); ++ } ++ ++ // connect should work as expected after the aberrant release send ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "client connect send failed (%d)\n", rc); ++ mxl_diag_client_close(&client_a); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ mxl_diag_poll(NULL, &server); ++ if (!server.has_client) { ++ fprintf(stderr, "server failed to activate after valid connect\n"); ++ mxl_diag_client_close(&client_a); ++ mxl_diag_close(NULL, &server); ++ return AVERROR(EINVAL); ++ } ++ ++ // redundant, but should pass ++ rc = mxl_diag_client_close(&client_a); ++ if (rc) { ++ fprintf(stderr, "client close failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ mxl_diag_close(NULL, &server); ++ return 0; ++} ++ ++ ++static int test_connect_after_junk(const char *server_path) ++{ ++ const char *junk_path = TEST_DIR "/test.connect_after_junk.junk.sock"; ++ const char *client_path_a = TEST_DIR "/test.connect_after_junk.a.client.sock"; ++ ++ mxl_diag_server server; ++ mxl_diag_client client_a; ++ struct sockaddr_un junk_addr; ++ struct sockaddr_un server_addr; ++ mxl_diag_msg msg; ++ int junk_fd = -1; ++ int rc; ++ ++ printf("\ntest: valid connect works after junk from unrelated socket\n"); ++ ++ rc = mxl_diag_init(NULL, &server, server_path); ++ if (rc) { ++ fprintf(stderr, "server init failed (%d)\n", rc); ++ return rc; ++ } ++ ++ if (strlen(server_path) >= sizeof(server_addr.sun_path) || ++ strlen(junk_path) >= sizeof(junk_addr.sun_path)) { ++ mxl_diag_close(NULL, &server); ++ return AVERROR(ENAMETOOLONG); ++ } ++ ++ junk_fd = socket(AF_UNIX, SOCK_DGRAM, 0); ++ if (junk_fd < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "junk socket failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ memset(&junk_addr, 0, sizeof(junk_addr)); ++ junk_addr.sun_family = AF_UNIX; ++ strcpy(junk_addr.sun_path, junk_path); ++ ++ memset(&server_addr, 0, sizeof(server_addr)); ++ server_addr.sun_family = AF_UNIX; ++ strcpy(server_addr.sun_path, server_path); ++ ++ unlink(junk_path); ++ ++ if (bind(junk_fd, (const struct sockaddr *)&junk_addr, sizeof(junk_addr)) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "junk bind failed (%d)\n", rc); ++ close(junk_fd); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ if (connect(junk_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "junk connect failed (%d)\n", rc); ++ close(junk_fd); ++ unlink(junk_path); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ /* Send one junk message to the idle server (version too high). */ ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION + 1; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ ++ if (send(junk_fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "junk send failed (%d)\n", rc); ++ close(junk_fd); ++ unlink(junk_path); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ mxl_diag_poll(NULL, &server); ++ ++ if (server.has_client) { ++ fprintf(stderr, "server unexpectedly activated after junk message\n"); ++ close(junk_fd); ++ unlink(junk_path); ++ mxl_diag_close(NULL, &server); ++ return AVERROR(EINVAL); ++ } ++ ++ close(junk_fd); ++ unlink(junk_path); ++ ++ /* Normal client connect should still work afterward. */ ++ rc = test_client_connect(&client_a, server_path, client_path_a); ++ if (rc) { ++ fprintf(stderr, "client A connection failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ mxl_diag_poll(NULL, &server); ++ ++ if (!server.has_client) { ++ fprintf(stderr, "server failed to activate after valid connect\n"); ++ mxl_diag_client_close(&client_a); ++ mxl_diag_close(NULL, &server); ++ return AVERROR(EINVAL); ++ } ++ ++ rc = mxl_diag_client_close(&client_a); ++ if (rc) { ++ fprintf(stderr, "client A close failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ mxl_diag_close(NULL, &server); ++ ++ return 0; ++} ++ ++static int test_poll_malformed_return_codes(const char *server_path) ++{ ++ const char *client_path_a = TEST_DIR "/test.poll_malformed.a.client.sock"; ++ ++ mxl_diag_server server; ++ mxl_diag_client client_a; ++ mxl_diag_msg msg; ++ int rc; ++ ++ printf("\ntest: mxl_diag_poll returns expected codes for malformed messages\n"); ++ ++ rc = mxl_diag_init(NULL, &server, server_path); ++ if (rc) { ++ fprintf(stderr, "server init failed (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_client_connect(&client_a, server_path, client_path_a); ++ if (rc) { ++ fprintf(stderr, "client A connection failed (%d)\n", rc); ++ mxl_diag_close(NULL, &server); ++ return rc; ++ } ++ ++ /* ++ * Drain the initial valid CONNECT sent by test_client_connect(), ++ * so the following poll calls correspond exactly to the malformed ++ * messages below. ++ */ ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != 1) { ++ fprintf(stderr, "initial connect poll returned (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("bad size, datagram smaller than sizeof(msg.header)\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ if (send(client_a.fd, &msg, sizeof(msg.header) - 1, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EDATATOOSMALL)) { ++ fprintf(stderr, "unexpected rc for 'datagram smaller than sizeof(msg.header)' (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("bad size, header.size smaller than sizeof(msg.header)\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg.header)-1; ++ if (send(client_a.fd, &msg, sizeof(msg.header), 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_ESIZETOOSMALL)) { ++ fprintf(stderr, "unexpected rc for 'header.size smaller than sizeof(msg.header)' (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("bad size, header.size bigger than sizeof(msg)\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg) + 1; ++ if (send(client_a.fd, &msg, sizeof(msg.header), 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_ESIZETOOBIG)) { ++ fprintf(stderr, "unexpected rc for 'header.size bigger than sizeof(msg)' (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("bad size, datagram size vs header.size mismatch\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg.header) + 1; ++ if (send(client_a.fd, &msg, sizeof(msg.header), 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_ESIZEMISMATCH)) { ++ fprintf(stderr, "unexpected rc for 'datagram size vs header.size mismatch' (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("bad size, truncated datagram\n"); ++ char buf[sizeof(mxl_diag_msg) + 1]; ++ mxl_diag_msg *truncated_msg = (mxl_diag_msg *)buf; ++ memset(truncated_msg, 0, sizeof(msg)); ++ truncated_msg->header.version = MXL_DIAG_MSG_VERSION; ++ truncated_msg->header.type = MXL_DIAG_MSG_RELEASE; ++ truncated_msg->header.size = sizeof(*truncated_msg); ++ if (send(client_a.fd, truncated_msg, sizeof(buf), 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EDATATRUNCATED)) { ++ fprintf(stderr, "unexpected rc for 'truncated datagram' (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // bad version ++ printf("bad version\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION + 1; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header); ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EBADVERSION)) { ++ fprintf(stderr, "unexpected rc for bad version (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // unknown type ++ printf("unknown type\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = 9999; ++ msg.header.size = sizeof(msg.header); ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EUNKNOWN)) { ++ fprintf(stderr, "unexpected rc for unknown type (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ printf("truncated header\n"); ++ { ++ static const unsigned char short_buf[sizeof(msg.header) - 1] = {0}; ++ if (send(client_a.fd, short_buf, sizeof(short_buf), 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EDATATOOSMALL)) { ++ fprintf(stderr, "unexpected rc for truncated header (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // more data than necessary for connect message ++ printf("connect message size mismatch\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_CONNECT; ++ msg.header.size = sizeof(msg.header)+1; ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EMSGSIZEMISMATCH)) { ++ fprintf(stderr, "unexpected rc for connect message size mismatch (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ // more data than necessary for release message ++ printf("release message size mismatch\n"); ++ memset(&msg, 0, sizeof(msg)); ++ msg.header.version = MXL_DIAG_MSG_VERSION; ++ msg.header.type = MXL_DIAG_MSG_RELEASE; ++ msg.header.size = sizeof(msg.header)+1; ++ if (send(client_a.fd, &msg, msg.header.size, 0) < 0) { ++ rc = AVERROR(errno); ++ fprintf(stderr, "send failed (%d)\n", rc); ++ goto finally; ++ } ++ rc = mxl_diag_poll(NULL, &server); ++ if (rc != AVERROR(MXL_DIAG_EMSGSIZEMISMATCH)) { ++ fprintf(stderr, "unexpected rc for release message size mismatch (%d)\n", rc); ++ rc = AVERROR(EINVAL); ++ goto finally; ++ } ++ ++ ++ ++ ++ rc = 0; ++ ++finally: ++ ++ int close_rc = mxl_diag_client_close(&client_a); ++ if (rc == 0 && close_rc) { ++ rc = close_rc; ++ } ++ ++ mxl_diag_close(NULL, &server); ++ ++ return rc; ++} ++ ++static int run_synchronous_tests(void) ++{ ++ int rc = -1; ++ const char* server_path = TEST_DIR "/sync.server.sock"; ++ ++ rc = test_server_path_collision_non_socket(server_path); ++ if (rc) { ++ fprintf(stderr, "test_server_path_collision_non_socket failed (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_server_path_stale_socket_cleanup(server_path); ++ if (rc) { ++ fprintf(stderr, "test_server_path_stale_socket_cleanup (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_release_with_no_active_client(server_path); ++ if (rc) { ++ fprintf(stderr, "test_release_with_no_active_client (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_connect_after_junk(server_path); ++ if (rc) { ++ fprintf(stderr, "test_connect_after_junk (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_poll_malformed_return_codes(server_path); ++ if (rc) { ++ fprintf(stderr, "test_poll_malformed_return_codes (%d)\n", rc); ++ return rc; ++ } ++ ++ return check_open_fds_match_startup(); ++} ++ ++static int run_asynchronous_tests(void) ++{ ++ int rc = -1; ++ const char* server_path = TEST_DIR "/async.server.sock"; ++ ++ server_thread instr_thread = {0}; ++ rc = test_server_thread_start(&instr_thread, server_path); ++ if (rc) { ++ fprintf(stderr, "test server thread start failed (%d)\n", rc); ++ return rc; ++ } ++ ++ rc = test_client_print_messages(server_path); ++ if (rc) ++ return rc; ++ ++ rc = test_client_multi_connect(server_path); ++ if (rc) ++ return rc; ++ ++ rc = test_client_malformed(server_path); ++ if (rc) ++ return rc; ++ ++ rc = test_server_thread_stop(&instr_thread); ++ if (rc) ++ return rc; ++ ++ unlink(server_path); ++ ++ return check_open_fds_match_startup(); ++} ++ ++int main(void) ++{ ++ printf("testing mxl_diag: mxl diagnostic server\n\n"); ++ ++ capture_startup_open_fds(); ++ ++ av_log_set_level(AV_LOG_QUIET); ++ ++ int rc = -1; ++ ++ if (mkdir(TEST_DIR, 0777) < 0 && errno != EEXIST) { ++ return AVERROR(errno); ++ } ++ ++ rc = run_synchronous_tests(); ++ if (rc) { ++ fprintf(stderr, "synchronous tests failed\n"); ++ goto finally; ++ } ++ ++ rc = run_asynchronous_tests(); ++ if (rc) { ++ fprintf(stderr, "asynchronous tests failed\n"); ++ goto finally; ++ } ++ ++ rmdir(TEST_DIR); ++ ++finally: ++ ++ if (!rc) ++ printf("\nPASS\n"); ++ else ++ printf("\nFAIL\n"); ++ ++ return rc; ++} +diff --git a/libavformat/tests/mxl_json.c b/libavformat/tests/mxl_json.c +new file mode 100644 +index 0000000000..45bd406aa2 +--- /dev/null ++++ b/libavformat/tests/mxl_json.c +@@ -0,0 +1,1076 @@ ++/* ++ * MXL JSON parser tests for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "libavformat/mxl_json.h" ++#include "libavutil/avassert.h" ++#include "libavutil/error.h" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++static const char* token_type_name(const jsmntok_t* tok) { ++ ++ switch(tok->type) { ++ case JSMN_OBJECT: ++ return "objc"; ++ case JSMN_ARRAY: ++ return "arry"; ++ case JSMN_STRING: ++ return "strn"; ++ case JSMN_PRIMITIVE: ++ return "prim"; ++ default: ++ return "unkn"; ++ } ++} ++ ++static void mxl_json_dump_doc(mxl_json_doc *doc) ++{ ++ if (!doc->json) { ++ printf("no json present in doc"); ++ return; ++ } ++ ++ // the original string ++ printf("%s\n", doc->json); ++ ++ // the parsed tokens ++ printf("%3s: %8s %3s %3s %3s\n", "idx", "type", "str", "end", "sz"); ++ for (int i = 0; i < doc->ntok; i++) { ++ jsmntok_t *tok = &doc->tokens[i]; ++ ++ const char* typename = token_type_name(tok); ++ int size = tok->end - tok->start + 1; ++ av_assert0(size > 0); ++ char* value = malloc(size); ++ av_assert0(value); ++ memcpy(value, doc->json + tok->start, size-1); ++ value[size-1] = 0; ++ ++ printf("%03d: %8s %03d %03d %03d \'%s\'\n", ++ i, typename, tok->start, tok->end, tok->size, value); ++ ++ free(value); ++ } ++} ++ ++static const char* err_to_str(int err) ++{ ++ switch(err) { ++ case MXL_JSON_EBADTYPE: ++ return "MXL_JSON_EBADTYPE"; ++ case MXL_JSON_ENOTFOUND: ++ return "MXL_JSON_ENOTFOUND"; ++ case MXL_JSON_NULL: ++ return "MXL_JSON_NULL"; ++ case MXL_JSON_MALFORMED: ++ return "MXL_JSON_MALFORMED"; ++ case MXL_JSON_UNSUPPORTED: ++ return "MXL_JSON_UNSUPPORTED"; ++ case MXL_JSON_EINTERNAL: ++ return "MXL_JSON_EINTERNAL"; ++ case ERANGE: ++ return "ERANGE"; ++ case AVERROR(ENOMEM): ++ return "AVERROR(ENOMEM)"; ++ case AVERROR(EIO): ++ return "AVERROR(EIO)"; ++ case AVERROR(EINVAL): ++ return "AVERROR(EINVAL)"; ++ case AVERROR_INVALIDDATA: ++ return "AVERROR_INVALIDDATA"; ++ case 0: ++ return "SUCCESS"; ++ default: ++ return "unknown"; ++ } ++} ++ ++static char* args_to_str(int depth, va_list ap) { ++ ++ const char sep = '.'; ++ const char* null = "NULL"; ++ ++ // start with len 1 for nul ++ size_t total_len = 1; ++ ++ // copy va list and traverse to compute length ++ va_list ap2; ++ va_copy(ap2, ap); ++ for (int i = 0; i < depth; i++) { ++ const char *s = va_arg(ap2, const char *); ++ total_len += (s ? strlen(s) : strlen(null)); ++ if (i < depth - 1) ++ total_len += 1; // comma ++ } ++ va_end(ap2); ++ ++ char *result = malloc(total_len); ++ if (!result) ++ return NULL; ++ ++ // second pass: build the string ++ char *p = result; ++ for (int i = 0; i < depth; i++) { ++ const char *s = va_arg(ap, const char *); ++ const char *part = s ? s : null; ++ size_t len = strlen(part); ++ memcpy(p, part, len); ++ p += len; ++ if (i < depth - 1) ++ *p++ = sep; ++ } ++ *p = '\0'; ++ ++ return result; ++} ++ ++static const char *null_str(const char* s) { ++ return s == NULL ? "(null)" : s; ++} ++ ++static int verify_json_string(const mxl_json_doc *doc, ++ const char *expected_value, ++ int expected_err, ++ int depth, ...) ++{ ++ va_list ap; ++ ++ va_start(ap, depth); ++ char* args_as_str = args_to_str(depth, ap); ++ va_end(ap); ++ ++ va_start(ap, depth); ++ mxl_json_str str = mxl_json_doc_get_stringv(doc, depth, ap); ++ va_end(ap); ++ ++ int rc = -1; ++ ++ // first: trivial error equality ++ if (expected_err != str.err) { ++ printf("FAIL \"%s\" expected error %s (actual %s)\n", ++ args_as_str, err_to_str(expected_err), err_to_str(str.err)); ++ rc = 1; ++ goto finally; ++ } ++ ++ // second: error aware string value compare ++ if (str.err != 0 && str.value != NULL) { ++ printf("FAIL \"%s\" string non-null value on error\n", ++ args_as_str); ++ rc = 1; ++ goto finally; ++ } ++ // else if no error then str.value must match expected ++ else if (str.err == 0 && strcmp(str.value, expected_value) != 0) { ++ printf("FAIL: \"%s\" str \"%s\" expected \"%s\"\n", ++ args_as_str, ++ str.value ? str.value : "(null)", expected_value); ++ rc = 1; ++ goto finally; ++ } ++ // else no failures, test passes ++ else { ++ rc = 0; ++ printf("PASS \"%s\" = {\"%s\", %s}\n", args_as_str, null_str(str.value), ++ err_to_str(str.err)); ++ } ++ ++finally: ++ free(args_as_str); ++ free(str.value); ++ ++ return rc; ++} ++ ++// The math.h signbit macro is not working with ffmpeg compile flags, ++// this is a compile flag independent alternative. ++static inline int is_neg_zero(double x) { ++ uint64_t u; ++ memcpy(&u, &x, sizeof u); ++ return u == 0x8000000000000000ULL; // IEEE-754 -0.0 value ++} ++ ++// compare two doubles with adapted tolerance ++static int dblcmp(double a, double b) { ++ ++ // must never attempt to compare NaN ++ av_assert0(!isnan(a) && !isnan(b)); ++ ++ // infinity compare ++ if (isinf(a) || isinf(b)) { ++ if (a == b) ++ return 0; ++ else ++ return (a < b) ? -1 : 1; ++ } ++ ++ // zero compare ++ if (a == 0.0 && b == 0.0) { ++ if (is_neg_zero(a) && !is_neg_zero(b)) ++ return -1; ++ else if (!is_neg_zero(a) && is_neg_zero(b)) ++ return 1; ++ else ++ return 0; ++ } ++ ++ // compute relative tolerance ++ double diff = fabs(a - b); ++ double tol = DBL_EPSILON * fmax(fabs(a), fabs(b)); ++ ++ // comparable if within tolerance ++ if (diff <= tol) ++ return 0; ++ else ++ return (a < b) ? -1 : 1; ++} ++ ++static int verify_json_double(const mxl_json_doc *doc, ++ double expected_value, ++ int expected_err, ++ int depth, ...) ++{ ++ va_list ap; ++ ++ va_start(ap, depth); ++ char* args_as_str = args_to_str(depth, ap); ++ va_end(ap); ++ ++ va_start(ap, depth); ++ mxl_json_num dbl = mxl_json_doc_get_doublev(doc, depth, ap); ++ va_end(ap); ++ ++ int rc = -1; ++ ++ // first: trivial error equality ++ if (expected_err != dbl.err) { ++ printf("FAIL \"%s\" expected error %s (actual %s)\n", ++ args_as_str, err_to_str(expected_err), err_to_str(dbl.err)); ++ rc = 1; ++ goto finally; ++ } ++ ++ // second: NaN aware value compare ++ bool both_nan_or_both_compare = 0; ++ if (isnan(expected_value) || isnan(dbl.value)) ++ both_nan_or_both_compare = isnan(expected_value) && isnan(dbl.value); ++ else ++ both_nan_or_both_compare = !dblcmp(expected_value, dbl.value); ++ ++ if (both_nan_or_both_compare) { ++ printf("PASS \"%s\" = {%.17g, %s}\n", ++ args_as_str, dbl.value, err_to_str(dbl.err)); ++ rc = 0; ++ } ++ else { ++ printf("FAIL \"%s\" expected value %.17g (actual %.17g)\n", ++ args_as_str, expected_value, dbl.value); ++ rc = 1; ++ } ++ ++finally: ++ free(args_as_str); ++ ++ return rc; ++} ++ ++// ++// sanity test ++// ++ ++static const char sanity_test_json[] = ++ "{" \ ++ "\"array_a\": [0,1,2,3]," \ ++ "\"object_a\": {\"a\": 1, \"b\": [0,1,2,3], \"c\": {\"x\": \"y\"} }," \ ++ "\"grain_rate\": {\"numerator\": 25, \"denominator\": 1}," \ ++ "\"format\": {\"name\": \"video\"}," \ ++ "\"media_type\": \"video/v210\"," \ ++ "\"frame_width\": 1920," \ ++ "\"array_b\": [{\"m\": n, \"o\": [4,5,6]}, \"p\", [\"q\",7,{\"r\":8}]]," \ ++ "\"object_b\": {\"s\": [9,\"t\",true], \"u\": {\"v\":{\"w\":10}}}," \ ++ "\"a_null\": null," \ ++ "\"a_true\": true," \ ++ "\"a_false\": false," \ ++ "\"array_c\": [0, \"one\", {\"three\": \"four\"}]," \ ++ "\"negnums\": {\"a\": -1.23, \"b\": -.123}," \ ++ "\"expnums\": {\"a\": 1.23e2, \"b\": 1.23e+2, \"c\": 1.23e-2}," \ ++ "\"object_c\": {\"last\": \"final\"}," \ ++ "\"ctrl_char\": {\"text\":\"line1\\nline2\"}" ++ "}"; ++ ++static int sanity_test(void) { ++ ++#define S1(VAL, ERR, P1) \ ++ rc = verify_json_string(&doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++#define S2(VAL, ERR, P1, P2) \ ++ rc = verify_json_string(&doc, VAL, ERR, 2, P1, P2); \ ++ if (rc) goto finally; ++ ++#define D1(VAL, ERR, P1) \ ++ rc = verify_json_double(&doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++#define D2(VAL, ERR, P1, P2) \ ++ rc = verify_json_double(&doc, VAL, ERR, 2, P1, P2); \ ++ if (rc) goto finally; ++#define D4(VAL, ERR, P1, P2, P3, P4) \ ++ rc = verify_json_double(&doc, VAL, ERR, 4, P1, P2, P3, P4); \ ++ if (rc) goto finally; ++ ++ printf("\nsanity test:\n"); ++ ++ mxl_json_doc doc; ++ int rc = -1; ++ ++ if (mxl_json_doc_build(NULL, sanity_test_json, &doc) < 0) { ++ printf("json_doc_build failed\n"); ++ rc = 1; ++ goto finally; ++ } ++ ++ mxl_json_dump_doc(&doc); ++ ++ // happy paths ++ S1("video/v210", 0, "media_type"); ++ S2("video", 0, "format", "name"); ++ S2("final", 0, "object_c", "last"); ++ D1(1920, 0, "frame_width"); ++ D2(25, 0, "grain_rate", "numerator"); ++ D2(1, 0, "grain_rate", "denominator"); ++ D2(1, 0, "object_a", "a"); ++ D4(10, 0, "object_b", "u", "v", "w"); ++ D2(-1.23, 0, "negnums", "a"); ++ D2(-0.123, 0, "negnums", "b"); ++ D2(1.23e2, 0, "expnums", "a"); ++ D2(1.23e+2, 0, "expnums", "b"); ++ D2(1.23e-2, 0, "expnums", "c"); ++ S2("line1\\nline2", 0, "ctrl_char", "text"); ++ ++ // impossible null primitive conversion ++ D1(NAN, MXL_JSON_NULL, "a_null"); ++ S1(NULL, MXL_JSON_NULL, "a_null"); ++ ++ // error: impossible null primitive conversion ++ D1(NAN, MXL_JSON_EBADTYPE, "a_true"); ++ D1(NAN, MXL_JSON_EBADTYPE, "a_false"); ++ S1(NULL, MXL_JSON_EBADTYPE, "a_false"); ++ S1(NULL, MXL_JSON_EBADTYPE, "a_true"); ++ ++ // error: bad type access to supported type ++ D2(NAN, MXL_JSON_EBADTYPE, "format", "name"); ++ S2(NULL, MXL_JSON_EBADTYPE, "grain_rate", "numerator"); ++ ++ // error: bad type access to unsupported array type ++ D1(NAN, MXL_JSON_EBADTYPE, "array_a"); ++ S1(NULL, MXL_JSON_EBADTYPE, "array_b"); ++ ++ // error: path not found ++ D1(NAN, MXL_JSON_ENOTFOUND, "does_not_exist"); ++ D2(NAN, MXL_JSON_ENOTFOUND, "does", "not_exist"); ++ S1(NULL, MXL_JSON_ENOTFOUND, "blah"); ++ S2(NULL, MXL_JSON_ENOTFOUND, "blah", "yadda"); ++ ++finally: ++ mxl_json_doc_release(&doc); ++ ++ if (doc.json || doc.tokens || doc.ntok) { ++ printf("FAIL mxl_json_doc_release did not clear structure values\n"); ++ rc = 1; ++ } ++ ++ return rc; ++ ++#undef D1 ++#undef D2 ++#undef D4 ++#undef S1 ++#undef S2 ++} ++ ++// ++// abberant input tests ++// ++ ++static const char invalid_empty[] = ""; ++static const char valid_empty_object[] = "{}"; ++static const char valid_empty_array[] = "[]"; ++static const char valid_empty_string[] = "\"\""; ++static const char valid_single_number[] = "123.346"; ++static const char valid_single_neg_number[] = "-123.346"; ++static const char valid_single_exp_number[] = "1.23346e2"; ++static const char valid_single_true[] = "true"; ++static const char valid_single_false[] = "false"; ++static const char valid_single_null[] = "null"; ++static const char misspelled_single_null[] = "nul"; ++static const char misspelled_contained_null[] = "{\"misspelled_null\": nul}"; ++static const char capital_contained_null[] = "{\"capital_null\": NULL}"; ++static const char invalid_leading_plus[] = "{\"a\": +123.346}"; ++static const char invalid_exp_number[] = "{\"a\": 1.23346e}"; ++static const char invalid_number_sep[] = "{\"a\": 1,23346}"; ++static const char not_json[] = "this is totally wrong"; ++static const char missing_key_quotes[] = "{foo: \"bar\"}"; ++static const char single_quotes[] = "{'foo': 'bar'}"; ++static const char trailing_comma_object[] = "{ \"foo\": \"bar\", }"; ++static const char trailing_comma_array[] = "[ \"a\", \"b\", ]"; ++static const char missing_both_braces[] = "\"foo\": \"bar\""; ++static const char missing_start_brace[] = "\"foo\": \"bar\"}"; ++static const char missing_end_brace[] = "{\"foo\": \"bar\""; ++static const char dangling_key[] = "{\"foo\":}"; ++static const char abnormal_termination[] = "{\"foo\": \"bar\"} what's this?"; ++static const char unescaped_ctl_char[] = "{ \"text\": \"line1\nline2\" }"; ++static const char unquoted_literal[] = "{\"what\": yes }"; ++static const char nan_value[] = "{\"value\": NaN }"; ++static const char inf_values[] = "{\"a\":Infinity,\"b\":-Infinity," \ ++ "\"c\":Inf, \"d\":-Inf}"; ++static const char valid_overflow_pos_number[] = "{\"a\": 1e400}"; ++static const char valid_overflow_neg_number[] = "{\"a\": -1e400}"; ++static const char valid_underflow_pos_number[] = "{\"a\": 1e-400}"; ++static const char valid_underflow_neg_number[] = "{\"a\": -1e-400}"; ++ ++static int test_input_build(const char* json, mxl_json_doc* doc) ++{ ++ int rc = mxl_json_doc_build(NULL, json, doc); ++ if (rc < 0) ++ printf("json_doc_build failed\n"); ++ return rc; ++} ++ ++static int abberant_input_tests(void) { ++ ++#define TS(VARIANT) \ ++ { \ ++ name = #VARIANT; \ ++ printf("\nSTART abberant test: %s\n", name); \ ++ rc = test_input_build(VARIANT, &doc); \ ++ if (rc) goto finally; \ ++ mxl_json_dump_doc(&doc); \ ++ } ++ ++#define S1(VAL, ERR, P1) \ ++ rc = verify_json_string(&doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++ ++#define D1(VAL, ERR, P1) \ ++ rc = verify_json_double(&doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++ ++#define TE(VARIANT) \ ++ av_assert0(strcmp(name, #VARIANT) == 0); \ ++ av_assert0(!rc); \ ++ printf("PASS abberant: %s\n", name); \ ++ mxl_json_doc_release(&doc); ++ ++#define BUILD_AND_ACCESS_TEST(VARIANT) \ ++ TS(VARIANT); \ ++ S1(NULL, MXL_JSON_ENOTFOUND, "absent"); \ ++ D1(NAN, MXL_JSON_ENOTFOUND, "absent"); \ ++ TE(VARIANT); ++ ++#define BUILD_EXPECT_ERROR_TEST(VARIANT, EXPECTED_ERR) \ ++ { \ ++ name = #VARIANT; \ ++ printf("\nSTART abberant test: %s\n", name); \ ++ rc = test_input_build(VARIANT, &doc); \ ++ if (rc != EXPECTED_ERR) { \ ++ printf("FAIL expected error %s (actual %s)\n", \ ++ err_to_str(EXPECTED_ERR), err_to_str(rc)); \ ++ if (!rc) rc = 1; \ ++ goto finally; \ ++ } \ ++ printf("PASS expected build error: %s (%s)\n", err_to_str(rc), err_to_str(rc)); \ ++ printf("PASS abberant: %s\n", name); \ ++ rc = 0; \ ++ } ++ ++ printf("\nabberant input tests:\n"); ++ ++ int rc = -1; ++ mxl_json_doc doc = {}; ++ const char* name; ++ ++ // verify that the json parses and can be traversed ++ BUILD_AND_ACCESS_TEST(invalid_empty); ++ BUILD_AND_ACCESS_TEST(valid_empty_object); ++ BUILD_AND_ACCESS_TEST(valid_empty_array); ++ BUILD_AND_ACCESS_TEST(valid_empty_string); ++ BUILD_AND_ACCESS_TEST(valid_single_number); ++ BUILD_AND_ACCESS_TEST(valid_single_neg_number); ++ BUILD_AND_ACCESS_TEST(valid_single_exp_number); ++ BUILD_AND_ACCESS_TEST(valid_single_true); ++ BUILD_AND_ACCESS_TEST(valid_single_false); ++ BUILD_AND_ACCESS_TEST(valid_single_null); ++ BUILD_AND_ACCESS_TEST(misspelled_single_null); ++ BUILD_AND_ACCESS_TEST(invalid_leading_plus); ++ BUILD_AND_ACCESS_TEST(invalid_exp_number); ++ BUILD_AND_ACCESS_TEST(invalid_number_sep); ++ BUILD_AND_ACCESS_TEST(not_json); ++ BUILD_AND_ACCESS_TEST(missing_key_quotes); ++ BUILD_AND_ACCESS_TEST(single_quotes); ++ BUILD_AND_ACCESS_TEST(trailing_comma_object); ++ BUILD_AND_ACCESS_TEST(trailing_comma_array); ++ BUILD_AND_ACCESS_TEST(dangling_key); ++ BUILD_AND_ACCESS_TEST(abnormal_termination); ++ BUILD_AND_ACCESS_TEST(unescaped_ctl_char); ++ BUILD_AND_ACCESS_TEST(missing_both_braces); ++ ++ // verify that the json parse fails ++ BUILD_EXPECT_ERROR_TEST(missing_start_brace, AVERROR_INVALIDDATA); ++ BUILD_EXPECT_ERROR_TEST(missing_end_brace, AVERROR_INVALIDDATA); ++ ++ ++ // verfiy failure accessing incorrect fields ++ ++ TS(misspelled_contained_null); ++ S1(NULL, MXL_JSON_ENOTFOUND, "absent"); ++ D1(NAN, MXL_JSON_ENOTFOUND, "absent"); ++ S1(NULL, MXL_JSON_EBADTYPE, "misspelled_null"); ++ D1(NAN, MXL_JSON_EBADTYPE, "misspelled_null"); ++ TE(misspelled_contained_null); ++ ++ TS(capital_contained_null); ++ S1(NULL, MXL_JSON_ENOTFOUND, "absent"); ++ D1(NAN, MXL_JSON_ENOTFOUND, "absent"); ++ S1(NULL, MXL_JSON_EBADTYPE, "capital_null"); ++ D1(NAN, MXL_JSON_EBADTYPE, "capital_null"); ++ TE(capital_contained_null); ++ ++ TS(unquoted_literal); ++ S1(NULL, MXL_JSON_ENOTFOUND, "absent"); ++ D1(NAN, MXL_JSON_ENOTFOUND, "absent"); ++ S1(NULL, MXL_JSON_EBADTYPE, "what"); ++ D1(NAN, MXL_JSON_EBADTYPE, "what"); ++ TE(unquoted_literal); ++ ++ TS(nan_value); ++ S1(NULL, MXL_JSON_ENOTFOUND, "absent"); ++ D1(NAN, MXL_JSON_ENOTFOUND, "absent"); ++ S1(NULL, MXL_JSON_EBADTYPE, "value"); ++ D1(NAN, MXL_JSON_EBADTYPE, "value"); ++ TE(nan_value); ++ ++ TS(inf_values); ++ D1(INFINITY, MXL_JSON_EBADTYPE, "a"); ++ D1(-INFINITY, MXL_JSON_EBADTYPE, "b"); ++ D1(INFINITY, MXL_JSON_EBADTYPE, "c"); ++ D1(-INFINITY, MXL_JSON_EBADTYPE, "d"); ++ TE(inf_values); ++ ++ TS(valid_overflow_pos_number); ++ D1(HUGE_VAL, ERANGE, "a"); ++ TE(valid_overflow_pos_number); ++ ++ TS(valid_overflow_neg_number); ++ D1(-HUGE_VAL, ERANGE, "a"); ++ TE(valid_overflow_neg_number); ++ ++ TS(valid_underflow_pos_number); ++ D1(0.0, ERANGE, "a"); ++ TE(valid_underflow_pos_number); ++ ++ TS(valid_underflow_neg_number); ++ D1(-0.0, ERANGE, "a"); ++ TE(valid_underflow_neg_number); ++ ++finally: ++ ++ return rc; ++ ++#undef BUILD_AND_ACCESS_TEST ++#undef BUILD_EXPECT_ERROR_TEST ++#undef TS ++#undef TE ++#undef S1 ++#undef D1 ++} ++ ++ ++// ++// non "C" locale tests: errant non "C" locale numbers in json string ++// ++ ++enum expect_what { ++ MALFORMED, ++ NOPARSE ++}; ++ ++typedef struct bad_number_case { ++ const char *num; ++ const char *name; ++ enum expect_what expect; ++} bad_number_case; ++ ++static const bad_number_case bad_number_cases[] = { ++ { "3,14", "comma_dec_a", MALFORMED}, ++ { "0,001", "comma_dec_b", MALFORMED }, ++ ++ { "1.234,56", "dot_thou_a", MALFORMED }, ++ { "12.345,67", "dot_thou_b", MALFORMED }, ++ ++ { "1,234.56", "comma_thou_a", MALFORMED}, ++ { "12,345.67", "comma_thou_b", MALFORMED}, ++ ++ { "1 234,56", "space_thou_a", MALFORMED}, ++ { "1 234.56", "space_thou_b", MALFORMED}, ++ ++ { "1" "\xC2\xA0" "234,56", "nbsp_thou_a", NOPARSE }, ++ { "1" "\xC2\xA0" "234.56", "nbsp_thou_b", NOPARSE }, ++ ++ { "1" "\xE2\x80\x89" "234,56", "thin_thou_a", NOPARSE }, ++ { "1" "\xE2\x80\x89" "234.56", "thin_thou_b", NOPARSE }, ++ ++ { "1'234,56", "swiss_thou_a", MALFORMED }, ++ { "1'234.56", "swiss_thou_b", MALFORMED }, ++ ++ { "\xD9\xA3" "\xDB\x8B" "\xD9\xA1" "\xD9\xA4", "arabic_314", NOPARSE }, ++ { "\xD9\xA1" "\xD9\xAC" "\xD9\xA2" "\xD9\xA3" "\xD9\xA4" "\xDB\x8B" ++ "\xD9\xA5" "\xD9\xA6", "arabic_1234", NOPARSE }, ++}; ++ ++static int test_bad_number_case(const bad_number_case* cse) { ++ printf("test %s\n", cse->name); ++ ++ mxl_json_doc doc = {0}; ++ char json[MXL_JSON_MAX_NUMBER_CHARS+1] = {0}; ++ ++ int rc = snprintf(json, sizeof(json), "{ \"%s\": %s }", ++ cse->name, cse->num); ++ if (rc < 0 || (size_t)rc >= sizeof(json)) ++ goto finally; ++ ++ rc = mxl_json_doc_build(NULL, json, &doc); ++ ++ if (!rc) ++ mxl_json_dump_doc(&doc); ++ ++ if (NOPARSE == cse->expect) ++ if (rc == 0) { ++ printf("FAIL expected NOPARSE but parse succeeded\n"); ++ rc = 1; ++ goto finally; ++ } ++ else { ++ printf("PASS expected NOPARSE and parse did not succeed\n"); ++ rc = 0; ++ goto finally; ++ } ++ ++ if (MALFORMED == cse->expect) { ++ av_assert0(!rc); ++ mxl_json_num num = mxl_json_doc_get_double1(&doc, cse->name); ++ ++ if (num.err != MXL_JSON_MALFORMED) { ++ printf("FAIL expected MXL_JSON_MALFORMED error (actual %s)\n", ++ err_to_str(num.err)); ++ rc = 1; ++ goto finally; ++ } ++ else { ++ printf("PASS %s identified as malformed\n", cse->name); ++ rc = 0; ++ goto finally; ++ } ++ } ++ ++finally: ++ if (!rc) ++ mxl_json_doc_release(&doc); ++ ++ return rc; ++} ++ ++static int non_c_locale_tests(void) ++{ ++ printf("\nnon C locale number tests:\n"); ++ ++ int rc = -1; ++ ++ for (int i = 0; i < sizeof(bad_number_cases)/sizeof(bad_number_case); i++) { ++ rc = test_bad_number_case(&bad_number_cases[i]); ++ if (rc) goto finally; ++ } ++ ++finally: ++ return rc; ++} ++ ++ ++// ++// real world MXL json string test ++// ++ ++const char *real_world_mxl_json_str = ++"{\n" ++" \"description\": \"MXL Test File\",\n" ++" \"id\": \"5fbec3b1-1b0f-417d-9059-8b94a47197ef\",\n" ++" \"tags\": {},\n" ++" \"format\": \"urn:x-nmos:format:video\",\n" ++" \"label\": \"MXL Test File\",\n" ++" \"parents\": [],\n" ++" \"media_type\": \"video/v210\",\n" ++" \"grain_rate\": {\n" ++" \"numerator\": 50,\n" ++" \"denominator\": 1\n" ++" },\n" ++" \"frame_width\": 1920,\n" ++" \"frame_height\": 1080,\n" ++" \"colorspace\": \"BT709\",\n" ++" \"components\": [\n" ++" {\n" ++" \"name\": \"Y\",\n" ++" \"width\": 1920,\n" ++" \"height\": 1080,\n" ++" \"bit_depth\": 10\n" ++" },\n" ++" {\n" ++" \"name\": \"Cb\",\n" ++" \"width\": 960,\n" ++" \"height\": 1080,\n" ++" \"bit_depth\": 10\n" ++" },\n" ++" {\n" ++" \"name\": \"Cr\",\n" ++" \"width\": 960,\n" ++" \"height\": 1080,\n" ++" \"bit_depth\": 10\n" ++" }\n" ++" ]\n" ++"}\n"; ++ ++static int check_real_world_values(const mxl_json_doc *doc) ++{ ++#define S1(VAL, ERR, P1) \ ++ rc = verify_json_string(doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++#define S2(VAL, ERR, P1, P2) \ ++ rc = verify_json_string(doc, VAL, ERR, 2, P1, P2); \ ++ if (rc) goto finally; ++#define D1(VAL, ERR, P1) \ ++ rc = verify_json_double(doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++#define D2(VAL, ERR, P1, P2) \ ++ rc = verify_json_double(doc, VAL, ERR, 2, P1, P2); \ ++ if (rc) goto finally; ++ ++ int rc = -1; ++ ++ S1("5fbec3b1-1b0f-417d-9059-8b94a47197ef", 0, "id"); ++ S1( "MXL Test File", 0, "label"); ++ S1("video/v210", 0, "media_type"); ++ S1("urn:x-nmos:format:video", 0, "format"); ++ S1("BT709", 0, "colorspace"); ++ D2(50, 0, "grain_rate", "numerator"); ++ D2(1, 0, "grain_rate", "denominator"); ++ D1(1920, 0, "frame_width"); ++ D1(1080, 0, "frame_height"); ++ ++finally: ++ return rc; ++#undef S1 ++#undef S2 ++#undef D1 ++#undef D2 ++} ++ ++ ++static int real_world_mxl_json_test(void) { ++ ++ printf("\nreal world MXL test:\n"); ++ ++ mxl_json_doc doc; ++ int rc = -1; ++ ++ if (mxl_json_doc_build(NULL, real_world_mxl_json_str, &doc) < 0) { ++ printf("json_doc_build failed\n"); ++ rc = 1; ++ goto finally; ++ } ++ ++ mxl_json_dump_doc(&doc); ++ ++ rc = check_real_world_values(&doc); ++ ++finally: ++ mxl_json_doc_release(&doc); ++ ++ return rc; ++} ++ ++// ++// token capacity tests ++// ++ ++static int token_capacity_test(const unsigned int initial_token_capacity, ++ const unsigned int max_token_capacity, ++ const int expected_error, ++ const char *name) ++{ ++ mxl_json_doc doc; ++ int exit_status = -1; ++ ++ printf("parse with token capacity initial=%u max=%u\n", initial_token_capacity, max_token_capacity); ++ ++ int rc = mxl_json_doc_build2(NULL, real_world_mxl_json_str, &doc, ++ initial_token_capacity, max_token_capacity); ++ if (expected_error != rc) { ++ printf("FAIL json_doc_build status \"%s\"\n", name); ++ exit_status = 1; ++ goto finally; ++ } ++ ++ // expected error and got error? ++ if (expected_error != 0 && rc != 0) { ++ printf("PASS expected build error %s (%d)\n", err_to_str(expected_error), expected_error); ++ exit_status = 0; ++ goto finally; ++ } ++ ++ mxl_json_dump_doc(&doc); ++ ++ rc = check_real_world_values(&doc); ++ if (rc) { ++ printf("FAIL token capacity \"%s\"\n", name); ++ exit_status = 1; ++ goto finally ; ++ } ++ ++ exit_status = 0; ++ ++finally: ++ mxl_json_doc_release(&doc); ++ ++ if (0 == exit_status) ++ printf("PASS token capacity \"%s\"\n", name); ++ ++ return exit_status; ++} ++ ++static int token_capacity_tests(void) ++{ ++ printf("\ntoken_capacity test:\n"); ++ ++ int rc = -1; ++ const unsigned int max_cap_def = MXL_JSON_TOKEN_CAPACITY_MAX_DEFAULT; ++ ++ // initial token capcity tests ++ const unsigned int low_non_zero = 2; ++ if (low_non_zero > MXL_JSON_INITIAL_TOKEN_CAPACITY_DEFAULT) { ++ printf("FAIL MXL_JSON_INITIAL_TOKEN_CAPACITY_DEFAULT low_non_zero test mismatch\n"); ++ rc = 1; ++ goto finally; ++ } ++ ++ rc = token_capacity_test(low_non_zero, max_cap_def, 0, "low non zero initial capacity"); ++ if (rc) goto finally; ++ ++ rc = token_capacity_test(1, 2, AVERROR(ENOMEM), "exceed max capacity"); ++ if (rc) goto finally; ++ ++ rc = token_capacity_test(2, 2, AVERROR(ENOMEM), "initial equal to max"); ++ if (rc) goto finally; ++ ++ rc = token_capacity_test(0, max_cap_def, AVERROR(EINVAL), "zero initial capacity"); ++ if (rc) goto finally; ++ ++ rc = token_capacity_test(2, 1, AVERROR(EINVAL), "initial greater than max"); ++ if (rc) goto finally; ++ ++ rc = token_capacity_test(1, 0, AVERROR(EINVAL), "zero max capacity"); ++ if (rc) goto finally; ++ ++ ++finally: ++ return rc; ++} ++ ++// ++// limit tests: validate implementation limits ++// ++ ++static const char number_over_conversion_limit[] = "{\"too_long\":" ++ //0 1 2 3 4 5 ++ //12345678901234567890123456789012345678901234567890 ++ "11111111111111111111111111111111111111111111111111" ++ "11111111111111111111111111111111111111111111111111" ++ "1111111111111111111111111111}"; ++ ++static const char number_under_conversion_limit[] = "{\"not_too_long\":" ++ //0 1 2 3 4 5 ++ //12345678901234567890123456789012345678901234567890 ++ "11111111111111111111111111111111111111111111111111" ++ "11111111111111111111111111111111111111111111111111" ++ "111111111111111111111111111}"; ++ ++static const char object_depth_over_limit[] = ++ "{\"accessible_a\": 1," ++ "\"too_deep\":" "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":{\"a\":2" ++ "}}}}}}}}}}}}}}}}," ++ "\"inaccessible_b\": 3}"; ++ ++static const char object_depth_under_limit[] = ++ "{\"accessible_a\": 1," ++ "\"not_too_deep\":" "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":{\"a\":" ++ "{\"a\":{\"a\":{\"a\":2" ++ "}}}}}}}}}}}}}}}," ++ "\"accessible_b\": 3}"; ++ ++static int limit_tests(void) ++{ ++ printf("\nlimit tests:\n"); ++ ++ const char* name; ++ mxl_json_doc doc; ++ int rc = -1; ++ ++#define TS(VARIANT) \ ++ { \ ++ name = #VARIANT; \ ++ printf("\nSTART limit test: %s\n", name); \ ++ rc = test_input_build(VARIANT, &doc); \ ++ if (rc) goto finally; \ ++ mxl_json_dump_doc(&doc); \ ++ } ++ ++#define D1(VAL, ERR, P1) \ ++ rc = verify_json_double(&doc, VAL, ERR, 1, P1); \ ++ if (rc) goto finally; ++ ++#define TE(VARIANT) \ ++ av_assert0(strcmp(name, #VARIANT) == 0); \ ++ av_assert0(!rc); \ ++ printf("PASS limit: %s\n", name); \ ++ mxl_json_doc_release(&doc); ++ ++ // over-limit numbes tests are hardcode for 127 max characters ++ if (127 != MXL_JSON_MAX_NUMBER_CHARS) { ++ printf("FAIL MXL_JSON_MAX_NUMBER_CHARS test mismatch\n"); ++ rc = 1; ++ goto finally; ++ } ++ else { ++ printf("PASS MXL_JSON_MAX_NUMBER_CHARS test match\n"); ++ } ++ ++ TS(number_over_conversion_limit); ++ D1(NAN, MXL_JSON_UNSUPPORTED, "too_long"); ++ TE(number_over_conversion_limit); ++ ++ TS(number_under_conversion_limit); ++ D1(1.1111111111111111e126, 0, "not_too_long"); ++ TE(number_under_conversion_limit); ++ ++ // over-limit depth tests are hard coded for a max depth of 16. ++ if (16 != MXL_JSON_MAX_DEPTH) { ++ printf("FAIL MXL_JSON_MAX_DEPTH test mismatch\n"); ++ rc = 1; ++ goto finally; ++ } ++ else { ++ printf("PASS MXL_JSON_MAX_DEPTH test match\n"); ++ } ++ ++ TS(object_depth_over_limit); ++ // works because the over-depth object does not have to be traversed ++ D1(1, 0, "accessible_a"); ++ // fails at the the key length limit ++ rc = verify_json_double(&doc, NAN, MXL_JSON_UNSUPPORTED, 17, ++ "too_deep", ++ "a","a","a","a","a","a","a","a", ++ "a","a","a","a","a","a","a","a"); ++ if (rc) goto finally; ++ // fails to traverse the over-depth object ++ D1(NAN, MXL_JSON_ENOTFOUND, "inaccessible_b"); ++ TE(object_depth_over_limit); ++ ++ ++ TS(object_depth_under_limit); ++ D1(1, 0, "accessible_a"); ++ rc = verify_json_double(&doc, 2, 0, 16, ++ "not_too_deep", ++ "a","a","a","a","a","a","a","a", ++ "a","a","a","a","a","a","a"); ++ if (rc) goto finally; ++ // fails at the key length limit ++ rc = verify_json_double(&doc, NAN, MXL_JSON_UNSUPPORTED, 17, ++ "too_deep", ++ "a","a","a","a","a","a","a","a", ++ "a","a","a","a","a","a","a","a"); ++ if (rc) goto finally; ++ D1(3, 0, "accessible_b"); ++ TE(object_depth_under_limit); ++ ++finally: ++ ++ return rc; ++ ++#undef TS ++#undef TE ++#undef D1 ++} ++ ++int main(void) { ++ ++ printf("testing mxl_json: jsmn based MXL resource definition parser\n"); ++ ++ int rc = -1; ++ ++ // FFmpeg expects C locale, this JSON parser depends on that. ++ if (!setlocale(LC_ALL, "C")) ++ goto finally; ++ ++ rc = sanity_test(); ++ if (rc) goto finally; ++ ++ rc = abberant_input_tests(); ++ if (rc) goto finally; ++ ++ rc = non_c_locale_tests(); ++ if (rc) goto finally; ++ ++ rc = real_world_mxl_json_test(); ++ if (rc) goto finally; ++ ++ rc = token_capacity_tests(); ++ if (rc) goto finally; ++ ++ rc = limit_tests(); ++ if (rc) goto finally; ++ ++finally: ++ ++ if (rc) ++ printf("\nFAIL\n"); ++ else ++ printf("\nPASS\n"); ++ ++ return rc; ++} +diff --git a/libavformat/tests/mxl_loc.c b/libavformat/tests/mxl_loc.c +new file mode 100644 +index 0000000000..221dac8c76 +--- /dev/null ++++ b/libavformat/tests/mxl_loc.c +@@ -0,0 +1,587 @@ ++/* ++ * MXL locator tests for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "libavformat/mxl_loc.h" ++#include "libavutil/error.h" ++#include "libavutil/avassert.h" ++#include "libavutil/log.h" ++ ++#include ++#include ++#include ++ ++typedef struct mxl_loc_const { ++ const char *domain_path; ++ const char * const *flow_ids; ++ int nb_flow_ids; ++} mxl_loc_const; ++ ++typedef struct test_case { ++ const char *name; ++ const char *locator; ++ int expected_rc; ++ mxl_loc_const expected; ++} test_case; ++ ++static void dump_locator(const mxl_loc *loc) ++{ ++ if (!loc) { ++ printf("mxl_loc : (null)\n"); ++ return; ++ } ++ ++ printf("mxl_loc {\n"); ++ printf(" domain_path: \"%s\"\n", loc->domain_path ? loc->domain_path : "(null)"); ++ printf(" nb_flow_ids: %d\n", loc->nb_flow_ids); ++ printf(" flow_ids: [\n"); ++ for (int i = 0; i < loc->nb_flow_ids; i++) ++ printf(" \"%s\"\n", loc->flow_ids[i] ? loc->flow_ids[i] : "(null)"); ++ printf(" ]\n"); ++ printf("}\n"); ++} ++ ++static void dump_expected_locator(const mxl_loc_const *expected) { ++ mxl_loc loc = { ++ .domain_path = (char *)expected->domain_path, ++ .flow_ids = (char **)expected->flow_ids, ++ .nb_flow_ids = expected->nb_flow_ids ++ }; ++ ++ dump_locator(&loc); ++} ++ ++static void dump_test_case(const test_case *tc, int actual_rc, const mxl_loc *actual) ++{ ++ printf("==== test case: %s\n", tc->name); ++ ++ printf("locator: \"%s\"\n", tc->locator ? tc->locator : "(null)"); ++ printf("expected return code: %d\n", tc->expected_rc); ++ printf("actual return code: %d\n", actual_rc); ++ ++ printf("expected locator:\n"); ++ dump_expected_locator(&tc->expected); ++ printf("actual locator:\n"); ++ dump_locator(actual); ++} ++ ++static int compare_loc(const mxl_loc_const *expected, const mxl_loc *actual) ++{ ++ av_assert0(expected && actual); ++ ++ if ((expected->domain_path == NULL) != (actual->domain_path == NULL)) { ++ fprintf(stderr, "compare_loc: domain_path NULL mismatch\n"); ++ return -1; ++ } ++ ++ if (expected->domain_path && actual->domain_path && ++ strcmp(expected->domain_path, actual->domain_path) != 0) { ++ fprintf(stderr, "compare_loc: domain_path mismatch: expected '%s' actual '%s'\n", ++ expected->domain_path, actual->domain_path); ++ return -1; ++ } ++ ++ if ((expected->flow_ids == NULL) != (actual->flow_ids == NULL)) { ++ fprintf(stderr, "compare_loc: flow_ids NULL mismatch\n"); ++ return -1; ++ } ++ ++ if (expected->nb_flow_ids != actual->nb_flow_ids) { ++ fprintf(stderr, "compare_loc: nb_flow_ids mismatch: expected %d actual %d\n", ++ expected->nb_flow_ids, actual->nb_flow_ids); ++ return -1; ++ } ++ ++ for (int i = 0; i < expected->nb_flow_ids; i++) { ++ const char *e = expected->flow_ids[i]; ++ const char *a = actual->flow_ids[i]; ++ ++ if (e == NULL || a == NULL) { ++ fprintf(stderr, "compare_loc: flow_id[%d] NULL: expected %s actual %s\n", ++ i, ++ e ? e : "(null)", ++ a ? a : "(null)"); ++ return -1; ++ } ++ ++ if (strcmp(e, a) != 0) { ++ fprintf(stderr, "compare_loc: flow_id[%d] mismatch: expected '%s' actual '%s'\n", ++ i, e, a); ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++/* returns 0 if invariants are satisfied */ ++static int verify_invariants(int parse_rc, const mxl_loc* actual) { ++ if (!actual) ++ return -1; ++ ++ if (!parse_rc) { ++ /* parse success invariants */ ++ if (actual->domain_path && ++ actual->nb_flow_ids >= 0 && ++ (actual->nb_flow_ids == 0 ? actual->flow_ids == NULL : actual->flow_ids != NULL)) { ++ return 0; ++ } ++ } ++ else { ++ /* parse failure invariants */ ++ if (!actual->domain_path && ++ actual->nb_flow_ids == 0 && actual->flow_ids == NULL) { ++ return 0; ++ } ++ } ++ ++ return -1; ++} ++ ++static int run_test_case(const test_case *tc) ++{ ++ mxl_loc actual = {0}; ++ int test_case_rc = -1; ++ ++ int parse_rc = mxl_loc_parse(NULL, tc->locator, &actual); ++ ++ dump_test_case(tc, parse_rc, &actual); ++ ++ int invariants_rc = verify_invariants(parse_rc, &actual); ++ if (invariants_rc != 0) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: parsed locator invariants violated\n", ++ tc->name); ++ goto finally; ++ } ++ ++ int compare_rc = compare_loc(&tc->expected, &actual); ++ if (compare_rc != 0) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: parsed locator mismatch\n", ++ tc->name); ++ goto finally; ++ } ++ ++ if (parse_rc != tc->expected_rc) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: return code mismatch " ++ "(expected %d got %d)\n", ++ tc->name, tc->expected_rc, parse_rc); ++ goto finally; ++ } ++ ++ test_case_rc = 0; ++ printf("==== test case: %s PASS\n", tc->name); ++ ++finally: ++ ++ if (!parse_rc) ++ mxl_loc_free(&actual); ++ ++ return test_case_rc; ++} ++ ++// expected mxl_loc value on error ++static const mxl_loc_const expected_error_loc = { ++ .domain_path = NULL, ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++}; ++ ++static test_case abs_filesystem_path = { ++ .name = "abs_filesystem_path", ++ .locator = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_root = { ++ .name = "abs_filesystem_path_root", ++ .locator = "/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case rel_filesystem_path = { ++ .name = "rel_filesystem_path", ++ .locator = "mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case rel_filesystem_path_none = { ++ .name = "rel_filesystem_path_none", ++ .locator = "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = ".", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case rel_filesystem_path_dot = { ++ .name = "rel_filesystem_path_dot", ++ .locator = "./71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = ".", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case rel_filesystem_path_dot_dot = { ++ .name = "rel_filesystem_path_dot_dot", ++ .locator = "../71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "..", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_zero_flow = { ++ .name = "abs_filesystem_path_zero_flow", ++ .locator = "/tests/data/tmp/mxl", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/tests/data/tmp/mxl", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_no_flow = { ++ .name = "abs_filesystem_no_flow", ++ .locator = "/dev/shm/mxl", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_no_flow_root = { ++ .name = "abs_filesystem_no_flow_root", ++ .locator = "/", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case rel_filesystem_no_flow = { ++ .name = "rel_filesystem_no_flow", ++ .locator = "rel/shm/mxl", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "rel/shm/mxl", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case rel_filesystem_no_flow_pwd = { ++ .name = "rel_filesystem_no_flow_pwd", ++ .locator = ".", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = ".", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case rel_filesystem_path_trailing_slash = { ++ .name = "rel_filesystem_path_trailing_slash", ++ .locator = "mxl/", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "mxl/", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case rel_filesystem_path_double_sep = { ++ .name = "rel_filesystem_path_double_sep", ++ .locator = "mxl//71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "mxl/", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_wrong_ext = { ++ .name = "abs_filesystem_path_wrong_ext", ++ .locator = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.txt", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.txt", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_path_trailing_ext_junk = { ++ .name = "abs_filesystem_path_trailing_ext_junk", ++ .locator = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow.tmp", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow.tmp", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_path_no_ext = { ++ .name = "abs_filesystem_path_no_ext", ++ .locator = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_path_upcase_id = { ++ .name = "abs_filesystem_path_upcase_id", ++ .locator = "/dev/shm/mxl/71F000DC-2578-4A06-B4EF-3E0E2B1E46E1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = (const char *[]) { ++ "71F000DC-2578-4A06-B4EF-3E0E2B1E46E1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_double_sep_a = { ++ .name = "abs_filesystem_path_double_sep_a", ++ .locator = "//dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "//dev/shm/mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_double_sep_b = { ++ .name = "abs_filesystem_path_double_sep_b", ++ .locator = "/dev/shm//mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm//mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_path_double_sep_c = { ++ .name = "abs_filesystem_path_double_sep_c", ++ .locator = "/dev/shm/mxl//71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl/", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case abs_filesystem_directory_named_flow = { ++ .name = "abs_filesystem_directory_named_flow", ++ .locator = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow/", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow/", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case abs_filesystem_path_bad_id = { ++ .name = "abs_filesystem_path_bad_id", ++ .locator = "/dev/shm/mxl/bad_id.mxl-flow", ++ .expected_rc = AVERROR(EINVAL), ++ .expected = expected_error_loc, ++}; ++ ++static test_case abs_filesystem_path_missing_id = { ++ .name = "abs_filesystem_path_missing_id", ++ .locator = "/dev/shm/mxl/.mxl-flow", ++ .expected_rc = AVERROR(EINVAL), ++ .expected = expected_error_loc, ++}; ++ ++static test_case null_locator = { ++ .name = "null_locator", ++ .locator = NULL, ++ .expected_rc = AVERROR(EINVAL), ++ .expected = expected_error_loc, ++}; ++ ++static test_case empty_locator = { ++ .name = "empty_locator", ++ .locator = "", ++ .expected_rc = AVERROR(ENOENT), ++ .expected = expected_error_loc, ++}; ++ ++static test_case uri_zero_flow = { ++ .name = "uri_zero_flow", ++ .locator = "mxl:///dev/shm/mxl", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0 ++ } ++}; ++ ++static test_case uri_one_flow = { ++ .name = "uri_one_flow", ++ .locator = "mxl:///dev/shm/mxl?id=71f000dc-2578-4a06-b4ef-3e0e2b1e46e1", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ }, ++ .nb_flow_ids = 1 ++ } ++}; ++ ++static test_case uri_two_flow = { ++ .name = "uri_two_flow", ++ .locator = "mxl:///dev/shm/mxl?" ++ "id=71f000dc-2578-4a06-b4ef-3e0e2b1e46e1&" ++ "id=ee43ad87-1ff5-43f5-a895-9cedbcc8884a", ++ .expected_rc = 0, ++ .expected = { ++ .domain_path = "/dev/shm/mxl", ++ .flow_ids = (const char *[]) { ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1", ++ "ee43ad87-1ff5-43f5-a895-9cedbcc8884a" ++ }, ++ .nb_flow_ids = 2 ++ } ++}; ++ ++static const test_case *tests[] = { ++ /* happy paths */ ++ &abs_filesystem_path, ++ &abs_filesystem_path_root, ++ &rel_filesystem_path, ++ &rel_filesystem_path_none, ++ &rel_filesystem_path_dot, ++ &rel_filesystem_path_dot_dot, ++ &abs_filesystem_no_flow, ++ &abs_filesystem_no_flow_root, ++ &rel_filesystem_no_flow, ++ &rel_filesystem_no_flow_pwd, ++ &rel_filesystem_path_trailing_slash, ++ &rel_filesystem_path_double_sep, ++ &abs_filesystem_path_zero_flow, ++ &abs_filesystem_path_wrong_ext, ++ &abs_filesystem_path_trailing_ext_junk, ++ &abs_filesystem_path_no_ext, ++ &abs_filesystem_path_upcase_id, ++ &abs_filesystem_path_double_sep_a, ++ &abs_filesystem_path_double_sep_b, ++ &abs_filesystem_path_double_sep_c, ++ &abs_filesystem_directory_named_flow, ++ &uri_zero_flow, ++ &uri_one_flow, ++ &uri_two_flow, ++ ++ /* aberrant cases */ ++ &abs_filesystem_path_bad_id, ++ &abs_filesystem_path_missing_id, ++ &null_locator, ++ &empty_locator, ++}; ++ ++int main(void) { ++ printf("testing mxl_loc: mxl resource location parser\n\n"); ++ ++ av_log_set_level(AV_LOG_QUIET); ++ ++ int rc = -1; ++ ++ // FFmpeg expects C locale, this URI parser depends on that. ++ if (!setlocale(LC_ALL, "C")) ++ return -1; ++ ++ for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { ++ rc = run_test_case(tests[i]); ++ if (rc) ++ break; ++ } ++ ++ if (rc) ++ printf("\nFAIL\n"); ++ else ++ printf("\nPASS\n"); ++ ++ return rc; ++} +diff --git a/libavformat/tests/mxl_uri.c b/libavformat/tests/mxl_uri.c +new file mode 100644 +index 0000000000..4d78d31616 +--- /dev/null ++++ b/libavformat/tests/mxl_uri.c +@@ -0,0 +1,1393 @@ ++/* ++ * MXL URI parser tests for Media eXchange Layer flows ++ * ++ * Copyright (c) 2025 CBC/Radio-Canada ++ * ++ * This file is part of FFmpeg. ++ * ++ * FFmpeg is free software; you can redistribute it and/or ++ * modify it under the terms of the GNU Lesser General Public ++ * License as published by the Free Software Foundation; either ++ * version 2.1 of the License, or (at your option) any later version. ++ * ++ * FFmpeg is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU Lesser General Public License for more details. ++ * ++ * You should have received a copy of the GNU Lesser General Public ++ * License along with FFmpeg; if not, see . ++ */ ++ ++#include "libavformat/mxl_uri.h" ++#include "libavutil/avassert.h" ++#include "libavutil/error.h" ++#include "libavutil/log.h" ++ ++#include ++#include ++#include ++ ++typedef struct mxl_uri_const { ++ const char *host; ++ int port; ++ const char *domain; ++ const char * const *flow_ids; ++ int nb_flow_ids; ++} mxl_uri_const; ++ ++typedef struct test_case { ++ const char *name; ++ const char *uri; ++ int expected_rc; ++ mxl_uri_const expected; ++} test_case; ++ ++static int compare_uri(const mxl_uri_const *expected, const mxl_uri *actual) ++{ ++ av_assert0(expected && actual); ++ ++ if (expected->port != actual->port) { ++ fprintf(stderr, "compare_uri: port mismatch: expected %d actual %d\n", ++ expected->port, actual->port); ++ return -1; ++ } ++ ++ if ((expected->host == NULL) != (actual->host == NULL)) { ++ fprintf(stderr, "compare_uri: host NULL mismatch: expected %s actual %s\n", ++ expected->host ? expected->host : "(null)", ++ actual->host ? actual->host : "(null)"); ++ return -1; ++ } ++ ++ if (expected->host && strcmp(expected->host, actual->host) != 0) { ++ fprintf(stderr, "compare_uri: host mismatch: expected '%s' actual '%s'\n", ++ expected->host, actual->host); ++ return -1; ++ } ++ ++ if ((expected->domain == NULL) != (actual->domain == NULL)) { ++ fprintf(stderr, "compare_uri: domain NULL mismatch: expected %s actual %s\n", ++ expected->domain ? expected->domain : "(null)", ++ actual->domain ? actual->domain : "(null)"); ++ return -1; ++ } ++ ++ if (expected->domain && actual->domain && ++ strcmp(expected->domain, actual->domain) != 0) { ++ fprintf(stderr, "compare_uri: domain mismatch: expected '%s' actual '%s'\n", ++ expected->domain, actual->domain); ++ return -1; ++ } ++ ++ if ((expected->flow_ids == NULL) != (actual->flow_ids == NULL)) { ++ fprintf(stderr, "compare_uri: flow_ids pointer mismatch\n"); ++ return -1; ++ } ++ ++ if (expected->nb_flow_ids != actual->nb_flow_ids) { ++ fprintf(stderr, "compare_uri: flow_id count mismatch: expected %d actual %d\n", ++ expected->nb_flow_ids, actual->nb_flow_ids); ++ return -1; ++ } ++ ++ for (int i = 0; i < expected->nb_flow_ids; i++) { ++ const char *e = expected->flow_ids[i]; ++ const char *a = actual->flow_ids[i]; ++ ++ if (e == NULL || a == NULL) { ++ fprintf(stderr, "compare_uri: flow_id[%d] NULL: expected %s actual %s\n", ++ i, ++ e ? e : "(null)", ++ a ? a : "(null)"); ++ return -1; ++ } ++ ++ if (strcmp(e, a) != 0) { ++ fprintf(stderr, "compare_uri: flow_id[%d] mismatch: expected '%s' actual '%s'\n", ++ i, e, a); ++ return -1; ++ } ++ } ++ ++ return 0; ++} ++ ++static void dump_uri(const mxl_uri *u) ++{ ++ if (!u) { ++ printf("mxl_uri: (null)\n"); ++ return; ++ } ++ ++ printf("mxl_uri {\n"); ++ ++ printf(" host: \"%s\"\n", u->host ? u->host : "(null)"); ++ printf(" port: %d\n", u->port); ++ printf(" domain: \"%s\"\n", u->domain ? u->domain : "(null)"); ++ ++ printf(" flow_ids (%d):\n", u->nb_flow_ids); ++ for (int i = 0; i < u->nb_flow_ids; i++) { ++ printf(" [%d] \"%s\"\n", i, ++ (u->flow_ids && u->flow_ids[i]) ? u->flow_ids[i] : "(null)"); ++ } ++ ++ printf("}\n"); ++} ++ ++static void dump_expected_uri(const mxl_uri_const* expected_uri) ++{ ++ mxl_uri uri = { ++ .host = (char *)expected_uri->host, ++ .port = expected_uri->port, ++ .domain = (char *)expected_uri->domain, ++ .flow_ids = (char **)expected_uri->flow_ids, ++ .nb_flow_ids = expected_uri->nb_flow_ids ++ }; ++ ++ dump_uri(&uri); ++} ++ ++static void dump_test_case(const test_case *tc, int actual_parse_rc, const mxl_uri *actual) ++{ ++ printf("==== test case: %s\n", tc->name); ++ ++ printf("URI: \"%s\"\n", tc->uri); ++ ++ printf("expected return code: %d\n", tc->expected_rc); ++ printf("actual return code: %d\n", actual_parse_rc); ++ ++ printf("expected URI:\n"); ++ dump_expected_uri(&tc->expected); ++ printf("actual URI:\n"); ++ dump_uri(actual); ++} ++ ++/* returns 0 if invariants are satisfied */ ++static int verify_invariants(int parse_rc, const mxl_uri* actual) { ++ if (!actual) ++ return -1; ++ ++ if (!parse_rc) { ++ /* parse success invariants */ ++ if (actual->host && actual->port >= -1 && actual->domain && ++ actual->nb_flow_ids >= 0 && ++ (actual->nb_flow_ids == 0 ? !actual->flow_ids : actual->flow_ids)) { ++ return 0; ++ } ++ } ++ else { ++ /* parse failure invariants */ ++ if (!actual->domain && ++ actual->nb_flow_ids == 0 && actual->flow_ids == NULL) { ++ return 0; ++ } ++ } ++ ++ return -1; ++} ++ ++static int run_test_case(const test_case *tc) ++{ ++ mxl_uri actual = {0}; ++ int invariants_rc = -1; ++ int compare_rc = -1; ++ int test_case_rc = -1; ++ ++ int parse_rc = mxl_parse_uri(NULL, tc->uri, &actual); ++ ++ dump_test_case(tc, parse_rc, &actual); ++ ++ invariants_rc = verify_invariants(parse_rc, &actual); ++ if (invariants_rc != 0) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: parsed URI invariants violated\n", ++ tc->name); ++ goto finally; ++ } ++ ++ compare_rc = compare_uri(&tc->expected, &actual); ++ if (compare_rc != 0) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: parsed URI mismatch\n", ++ tc->name); ++ goto finally; ++ } ++ ++ if (parse_rc != tc->expected_rc) { ++ fprintf(stderr, ++ "==== test '%s' FAIL: return code mismatch " ++ "(expected %d got %d)\n", ++ tc->name, tc->expected_rc, parse_rc); ++ goto finally; ++ } ++ ++ test_case_rc = 0; ++ printf("==== test case: %s PASS\n", tc->name); ++ ++finally: ++ if (!parse_rc) ++ mxl_uri_free(&actual); ++ ++ return test_case_rc; ++} ++ ++// expected mxl_uri value on error ++static const mxl_uri_const expected_error_uri = { ++ .host = NULL, ++ .port = -1, ++ .domain = NULL, ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++}; ++ ++static test_case zero_id = { ++ .name = "zero_id", ++ ++ .uri = "mxl://host1.local/domain/path", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++ }, ++}; ++ ++static test_case one_id = { ++ .name = "one_id", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case two_id = { ++ .name = "two_id", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case three_id = { ++ .name = "three_id", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc&" ++ "id=2b528ffe-c94c-454f-984a-35c98c717006", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ "2b528ffe-c94c-454f-984a-35c98c717006" ++ }, ++ ++ .nb_flow_ids = 3, ++ }, ++}; ++ ++static test_case two_id_upcase = { ++ .name = "two_id_upcase", ++ ++ .uri = ++ "MXL://HOST1.LOCAL/DOMAIN/PATH?" ++ "id=3645F1E2-90FC-49CB-AF9C-236C2AC124CD&" ++ "id=876FC244-9C99-48A8-A461-927E43A05BDC", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "HOST1.LOCAL", ++ .port = -1, ++ .domain = "/DOMAIN/PATH", ++ ++ .flow_ids = (const char *[]) { ++ "3645F1E2-90FC-49CB-AF9C-236C2AC124CD", ++ "876FC244-9C99-48A8-A461-927E43A05BDC", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case two_id_upcase_key = { ++ .name = "two_id_upcase_key", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "ID=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case unknown_query_param = { ++ .name = "unknown_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "unknown=unknown&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case unknown_flag_query_param = { ++ .name = "unknown_flag_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "empty&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case empty_query = { ++ .name = "empty_query", ++ ++ .uri = "mxl://host1.local/domain/path?", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++ }, ++}; ++ ++static test_case ipv4 = { ++ .name = "ipv4", ++ ++ .uri = ++ "mxl://10.1.2.3:5000/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "10.1.2.3", ++ .port = 5000, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1", ++ "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case zero_port = { ++ .name = "zero_port", ++ ++ .uri = ++ "mxl://10.1.2.3:0/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case ipv6 = { ++ .name = "ipv6", ++ ++ .uri = ++ "mxl://[2001:db8::2]/domain/path?" ++ "id=1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92&" ++ "id=3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "2001:db8::2", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92", ++ "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case ipv6_with_port = { ++ .name = "ipv6_with_port", ++ ++ .uri = ++ "mxl://[2001:db8::2]:5000/domain/path?" ++ "id=1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92&" ++ "id=3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "2001:db8::2", ++ .port = 5000, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92", ++ "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case malformed_ipv6_bracket = { ++ .name = "malformed_ipv6_bracket", ++ ++ .uri = ++ "mxl://[2001:db8::2/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case bad_scheme = { ++ .name = "bad_scheme", ++ ++ .uri = ++ "ftp://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case no_scheme = { ++ .name = "no_scheme", ++ ++ .uri = ++ "/host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case foo_after_scheme = { ++ .name = "foo_after_scheme", ++ ++ .uri = "mxl:foo", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case null_uri = { ++ .name = "null_uri", ++ ++ .uri = NULL, ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_uri = { ++ .name = "empty_uri", ++ ++ .uri = "", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L ++_Static_assert( ++ MXL_SCHEME_MAX == 4, ++ "Update equal_max_scheme and greater_max_scheme tests if MXL_SCHEME_MAX changes"); ++#endif ++// scheme too long (length == MXL_SCHEME_MAX) ++static test_case equal_max_scheme = { ++ .name = "equal_max_scheme", ++ ++ .uri = ++ "aaaa:/host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(ENAMETOOLONG), ++ ++ .expected = expected_error_uri ++}; ++ ++// scheme too long (length > MXL_SCHEME_MAX) ++static test_case greater_max_scheme = { ++ .name = "greater_max_scheme", ++ ++ .uri = ++ "aaaaa:/host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(ENAMETOOLONG), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_middle_id_query_param = { ++ .name = "empty_middle_id_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_end_id_query_param = { ++ .name = "empty_end_id_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc&" ++ "id=", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case bad_separator_query_param = { ++ .name = "bad_separator_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case bad_uuid_query_param = { ++ .name = "bad_uuid_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=X645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++// special case -1 port value, caller must interpret as "not set" ++static test_case valid_negative_one_port = { ++ .name = "valid_negative_one_port", ++ ++ .uri = ++ "mxl://10.1.2.3:-1/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "10.1.2.3", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1", ++ "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case invalid_negative_port = { ++ .name = "invalid_negative_port", ++ ++ .uri = ++ "mxl://host1.local:-2/domain/path?" ++ "id=645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case invalid_empty_port = { ++ .name = "invalid_empty_port", ++ ++ .uri = ++ "mxl://10.1.2.3:/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case invalid_non_integer_port = { ++ .name = "invalid_non_integer_port", ++ ++ .uri = ++ "mxl://10.1.2.3:abc/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case valid_at_range_limit_port = { ++ .name = "valid_at_range_limit_port", ++ ++ .uri = ++ "mxl://host1.local:65535/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = 65535, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case invalid_out_of_range_port = { ++ .name = "invalid_out_of_range_port", ++ ++ .uri = ++ "mxl://10.1.2.3:65536/domain/path?" ++ "id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&" ++ "id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case only_unknown_query_param = { ++ .name = "only_unknown_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?foo=bar&baz=qux", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++ }, ++}; ++ ++static test_case leading_amp_query_param = { ++ .name = "leading_amp_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "&id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case double_separator_query_param = { ++ .name = "double_separator_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case invalid_no_domain_path = { ++ .name = "invalid_no_domain_path", ++ ++ .uri = ++ "mxl://host1.local?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case fragment_after_query = { ++ .name = "fragment_after_query", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd#fragment", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case percent_encoded_in_uuid_query_value = { ++ .name = "percent_encoded_in_uuid_query_value", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd%26extra", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case percent_encoded_in_other_query_value = { ++ .name = "percent_encoded_in_other_query_value", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "other=%26extra&" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc", ++ }, ++ ++ .nb_flow_ids = 2, ++ }, ++}; ++ ++static test_case percent_encoded_key = { ++ .name = "percent_encoded_key", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "%69d=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ .flow_ids = NULL, ++ .nb_flow_ids = 0, ++ }, ++}; ++ ++static test_case trailing_amp_query_param = { ++ .name = "trailing_amp_query_param", ++ ++ .uri = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "host1.local", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]){ ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case userinfo_in_authority = { ++ .name = "userinfo_in_authority", ++ ++ .uri = ++ "mxl://user@host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case userpass_in_authority = { ++ .name = "userpass_in_authority", ++ ++ .uri = ++ "mxl://user:pass@host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case multiple_at_userinfo = { ++ .name = "multiple_at_userinfo", ++ ++ .uri = ++ "mxl://a@b@host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case local_domain_triple_slash = { ++ .name = "local_domain_triple_slash", ++ ++ .uri = ++ "mxl:///domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "", ++ .port = -1, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case local_domain_looks_like_port_but_is_not = { ++ .name = "local_domain_looks_like_port_but_is_not", ++ ++ .uri = ++ "mxl:///domain/path:5000?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "", ++ .port = -1, ++ .domain = "/domain/path:5000", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case local_domain_single_slash = { ++ .name = "local_domain_single_slash", ++ ++ .uri = ++ "mxl:/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case userinfo_with_empty_host = { ++ .name = "userinfo_with_empty_host", ++ ++ .uri = ++ "mxl://user@/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_host_with_port = { ++ .name = "empty_host_with_port", ++ ++ .uri = ++ "mxl://:5000/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "", ++ .port = 5000, ++ .domain = "/domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case query_without_path = { ++ .name = "query_without_path", ++ ++ .uri = ++ "mxl://?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case double_leading_slash_path = { ++ .name = "double_leading_slash_path", ++ ++ .uri = ++ "mxl:////domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = 0, ++ ++ .expected = { ++ .host = "", ++ .port = -1, ++ .domain = "//domain/path", ++ ++ .flow_ids = (const char *[]) { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ }, ++ ++ .nb_flow_ids = 1, ++ }, ++}; ++ ++static test_case empty_ipv6_host = { ++ .name = "empty_ipv6_host", ++ ++ .uri = ++ "mxl://[]/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_ipv6_host_with_port = { ++ .name = "empty_ipv6_host_with_port", ++ ++ .uri = ++ "mxl://[]:5000/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_domain_path = { ++ .name = "empty_domain_path", ++ ++ .uri = ++ "mxl://host1.local/?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_domain_path_no_query = { ++ .name = "empty_domain_path_no_query", ++ ++ .uri = "mxl://host1.local", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case empty_domain_path_no_query_trailing_slash = { ++ .name = "empty_domain_path_no_query_trailing_slash", ++ ++ .uri = "mxl://host1.local/", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++static test_case non_unique_flow_ids = { ++ .name = "non_unique_flow_ids", ++ ++ .uri = ++ "mxl:///host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc&" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ ++ .expected_rc = AVERROR(EINVAL), ++ ++ .expected = expected_error_uri ++}; ++ ++ ++static const test_case *tests[] = { ++ /* happy path cases */ ++ &zero_id, ++ &one_id, ++ &two_id, ++ &three_id, ++ &two_id_upcase, ++ &two_id_upcase_key, ++ &unknown_query_param, ++ &unknown_flag_query_param, ++ &ipv4, ++ &ipv6, ++ &ipv6_with_port, ++ &valid_negative_one_port, ++ &only_unknown_query_param, ++ &local_domain_triple_slash, ++ &local_domain_single_slash, ++ ++ /* boundary cases */ ++ &zero_port, ++ &fragment_after_query, ++ &valid_at_range_limit_port, ++ &invalid_out_of_range_port, ++ &empty_query, ++ &leading_amp_query_param, ++ &double_separator_query_param, ++ &percent_encoded_in_other_query_value, ++ &percent_encoded_key, ++ &trailing_amp_query_param, ++ &local_domain_looks_like_port_but_is_not, ++ &empty_host_with_port, ++ &userinfo_with_empty_host, ++ &double_leading_slash_path, ++ &query_without_path, ++ &empty_ipv6_host, ++ &empty_ipv6_host_with_port, ++ &empty_domain_path, ++ &empty_domain_path_no_query, ++ &empty_domain_path_no_query_trailing_slash, ++ &non_unique_flow_ids, ++ ++ /* aberrant cases */ ++ &bad_scheme, ++ &no_scheme, ++ &foo_after_scheme, ++ &null_uri, ++ &empty_uri, ++ &equal_max_scheme, ++ &greater_max_scheme, ++ &empty_middle_id_query_param, ++ &empty_end_id_query_param, ++ &bad_separator_query_param, ++ &bad_uuid_query_param, ++ &invalid_negative_port, ++ &invalid_empty_port, ++ &invalid_non_integer_port, ++ &invalid_no_domain_path, ++ &malformed_ipv6_bracket, ++ &percent_encoded_in_uuid_query_value, ++ &userinfo_in_authority, ++ &userpass_in_authority, ++ &multiple_at_userinfo ++}; ++ ++static int run_uri_limit_test(const char *name, ++ size_t uri_len, ++ int expected_rc) ++{ ++ static char uri_buf[MXL_URI_MAX + 2]; ++ ++ const char *prefix = ++ "mxl://host1.local/domain/path?" ++ "id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++ "id=876fc244-9c99-48a8-a461-927e43a05bdc&" ++ "junk="; ++ ++ size_t p = strlen(prefix); ++ ++ av_assert0(uri_len >= p); ++ av_assert0(uri_len < sizeof(uri_buf)); ++ ++ memcpy(uri_buf, prefix, p); ++ ++ for (size_t i = p; i < uri_len; i++) ++ uri_buf[i] = 'a'; ++ ++ uri_buf[uri_len] = '\0'; ++ ++ test_case tc = { ++ .name = name, ++ .uri = uri_buf, ++ .expected_rc = expected_rc, ++ }; ++ ++ if (expected_rc == 0) { ++ tc.expected.host = "host1.local"; ++ tc.expected.port = -1; ++ tc.expected.domain = "/domain/path"; ++ ++ static const char *ids[] = { ++ "3645f1e2-90fc-49cb-af9c-236c2ac124cd", ++ "876fc244-9c99-48a8-a461-927e43a05bdc" ++ }; ++ ++ tc.expected.flow_ids = ids; ++ tc.expected.nb_flow_ids = 2; ++ } else { ++ tc.expected = expected_error_uri; ++ } ++ ++ return run_test_case(&tc); ++} ++ ++static int test_mxl_uri_free(void) ++{ ++ ++ // sanity check freeing a null ++ mxl_uri_free(NULL); ++ ++ // Parse a known good uri that sets all fields, validate, then free. ++ mxl_uri parsed = {0}; ++ int rc = mxl_parse_uri(NULL, ipv4.uri, &parsed); ++ if (rc) ++ return rc; ++ rc = compare_uri(&ipv4.expected, &parsed); ++ if (rc) ++ return rc; ++ ++ // every field should be set ++ if (parsed.host == NULL || parsed.port == 0 || parsed.port == 1 || ++ parsed.domain == NULL || ++ parsed.flow_ids == NULL || parsed.nb_flow_ids == 0) { ++ fprintf(stderr, "==== test_mxl_uri_free FAIL: parsed uri not set\n"); ++ return -1; ++ } ++ ++ mxl_uri_free(&parsed); ++ ++ // every field should be clear ++ if (parsed.host != NULL || parsed.port != 0 || parsed.domain != NULL || ++ parsed.flow_ids != NULL || parsed.nb_flow_ids != 0) { ++ fprintf(stderr, "==== test_mxl_uri_free FAIL: freed mxl_uri not cleared\n"); ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static int test_scheme_prefix_check(void) ++{ ++ ++ if (!mxl_uri_is_mxl_scheme(zero_id.uri)) { ++ fprintf(stderr, "==== test_scheme_prefix_check FAIL: zero_id\n"); ++ return -1; ++ } ++ ++ if (!mxl_uri_is_mxl_scheme(two_id_upcase.uri)) { ++ fprintf(stderr, "==== test_scheme_prefix_check FAIL: two_id_upcase\n"); ++ return -1; ++ } ++ ++ if (!mxl_uri_is_mxl_scheme(foo_after_scheme.uri)) { ++ fprintf(stderr, "==== test_scheme_prefix_check FAIL: foo_scheme\n"); ++ return -1; ++ } ++ ++ if (mxl_uri_is_mxl_scheme(bad_scheme.uri)) { ++ fprintf(stderr, "==== test_scheme_prefix_check FAIL: bad_scheme\n"); ++ return -1; ++ } ++ ++ printf("testing mxl_uri: mxl URI parser\n\n"); ++ ++ av_log_set_level(AV_LOG_QUIET); ++ ++ int rc = -1; ++ ++ // FFmpeg expects C locale, this URI parser depends on that. ++ if (!setlocale(LC_ALL, "C")) ++ return -1; ++ ++ for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { ++ rc = run_test_case(tests[i]); ++ if (rc) ++ goto finally; ++ } ++ ++ // URI max length limit tests use generated URI, hence are separate. ++ rc = run_uri_limit_test("equal_max_len_url", MXL_URI_MAX, 0); ++ if (rc) ++ goto finally; ++ rc = run_uri_limit_test("greater_max_len_url", MXL_URI_MAX + 1, AVERROR(ENAMETOOLONG)); ++ if (rc) ++ goto finally; ++ ++ // test freeing a mxl_uri parse result ++ rc = test_mxl_uri_free(); ++ if (rc) ++ goto finally; ++ ++ // test scheme prefix check (i.e. starts with "mxl:") ++ rc = test_scheme_prefix_check(); ++ ++finally: ++ ++ if (rc) ++ printf("\nFAIL\n"); ++ else ++ printf("\nPASS\n"); ++ ++ return rc; ++} +diff --git a/tests/Makefile b/tests/Makefile +index 9b70145015..3271662444 100644 +--- a/tests/Makefile ++++ b/tests/Makefile +@@ -219,6 +219,10 @@ include $(SRC_PATH)/tests/fate/mpeg4.mak + include $(SRC_PATH)/tests/fate/mpegps.mak + include $(SRC_PATH)/tests/fate/mpegts.mak + include $(SRC_PATH)/tests/fate/mxf.mak ++include $(SRC_PATH)/tests/fate/mxl.mak ++include $(SRC_PATH)/tests/fate/ogg-vorbis.mak ++include $(SRC_PATH)/tests/fate/ogg-flac.mak ++include $(SRC_PATH)/tests/fate/ogg-opus.mak + include $(SRC_PATH)/tests/fate/oma.mak + include $(SRC_PATH)/tests/fate/opus.mak + include $(SRC_PATH)/tests/fate/pcm.mak +diff --git a/tests/fate/mxl.mak b/tests/fate/mxl.mak +new file mode 100644 +index 0000000000..50c979baf3 +--- /dev/null ++++ b/tests/fate/mxl.mak +@@ -0,0 +1,240 @@ ++fate-mxl-json: libavformat/tests/mxl_json$(EXESUF) ++fate-mxl-json: CMD = run libavformat/tests/mxl_json$(EXESUF) ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-json) ++ ++fate-mxl-uri: libavformat/tests/mxl_uri$(EXESUF) ++fate-mxl-uri: CMD = run libavformat/tests/mxl_uri$(EXESUF) ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-uri) ++ ++fate-mxl-loc: libavformat/tests/mxl_loc$(EXESUF) ++fate-mxl-loc: CMD = run libavformat/tests/mxl_loc$(EXESUF) ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-loc) ++ ++fate-mxl-diag: libavformat/tests/mxl_diag$(EXESUF) ++fate-mxl-diag: CMD = run libavformat/tests/mxl_diag$(EXESUF) ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-diag) ++ ++# Note: MXL_OPTIONS is defined explicitly (even though it matches the ++# default MXL value) to make the test's dependency on this ++# history_duration setting unambiguous and intentional. ++ ++MXL_TMP_DIR := $(TARGET_PATH)/tests/data/tmp/mxl ++MXL_OPTIONS := "{"\"urn:x-mxl:option:history_duration/v1.0\"":100000000}" ++MXL_VIDEO_FLOW_ID := 717f834b-4224-4c9b-8a64-ecb7726803b8 ++MXL_AUDIO_FLOW_ID := ff39f65b-d760-4a7c-808d-4f2778de5658 ++MXL_AUDIO_MAX_SAMPLES := 2559 ++MXL_AUDIO_SAMPLES_PER_PACKET := 512 ++MXL_SENTINEL_TIMEOUT := 4000 ++DEMUX_RETRY_SLEEP := 0.3 ++DEMUX_RETRY_LIMIT := 10 ++ ++# Use this to construct macro arguments that require "," characters ++COMMA := , ++ ++# Use this to normalize local paths ++FILTER_PWD_FROM_MXL_URI = sed "s|mxl://$$(pwd)|mxl:///path/to|g" ++ ++# Use to normalize the log prefix in "[mxl 0xXXXXXXXX]" messages ++FILTER_MXL_LOG_PREFIX = sed -E "s/(\[mxl @ 0x)[[:xdigit:]]+(\])/\1ADDR\2/g" ++ ++# Grep out normalized mxl log messages, and the final "Invalid ++# argument" or "Input/output error" message ++PROBE_LOG_FILTER = $(FILTER_PWD_FROM_MXL_URI) | $(FILTER_MXL_LOG_PREFIX) | \ ++ grep -E "^\[mxl @ 0xADDR\]|: Invalid argument$$|: Input/output error$$" ++ ++# Usage: $(call MXL_MUX_DEMUX_TEST,,,,) ++# ++# Run an MXL test in an isolated per-test temporary directory. ++# ++# The macro creates $(MXL_TMP_DIR)/, after removing any ++# previous copy of that directory, and changes to that directory. A ++# local MXL domain directory is created in the working directory at ++# ./domain, and `mux-command` is executed in the background. ++# ++# It then retries the supplied `demux-command` until it succeeds or ++# the retry limit is reached. On success it touches the local ++# ./sentinel file so the `mux-command` can perform synchronized ++# teardown. The macro waits for the muxer to exit before completing. ++# ++define MXL_MUX_DEMUX_TEST ++ ( \ ++ set -e; \ ++ export MXL_LOG_LEVEL=off; \ ++ TEST_DIR="$(MXL_TMP_DIR)/$(strip $(1))"; \ ++ rm -rf "$$TEST_DIR"; \ ++ mkdir -p "$$TEST_DIR"; \ ++ cd "$$TEST_DIR"; \ ++ rm -f sentinel; \ ++ mkdir domain; \ ++ echo $(MXL_OPTIONS) > domain/options.json; \ ++ $(2) & \ ++ MUX_PID=$$!; \ ++ trap "kill $$MUX_PID 2>/dev/null || true" INT TERM EXIT; \ ++ set +e; \ ++ RETRIES=0; \ ++ while [ $$RETRIES -lt $(RETRY_LIMIT) ]; do \ ++ sleep $(RETRY_SLEEP); \ ++ OUTFILE=$$(mktemp "$$TEST_DIR"/demux.out.XXXXXX) || exit 1; \ ++ $(3) > "$$OUTFILE" 2>&1; \ ++ STATUS=$$?; \ ++ $(FILTER_PWD_FROM_MXL_URI) < "$$OUTFILE"; \ ++ rm -f "$$OUTFILE"; \ ++ if [ $$STATUS -eq 0 ]; then \ ++ touch sentinel; \ ++ break; \ ++ else \ ++ RETRIES=$$((RETRIES+1)); \ ++ fi; \ ++ done; \ ++ set -e; \ ++ if [ $$RETRIES -ge $(RETRY_LIMIT) ]; then echo "$(4) failed"; exit 1; fi; \ ++ wait $$MUX_PID; \ ++ trap - INT TERM EXIT; \ ++ ) ++endef ++ ++# Usage: $(call MXL_PROBE,) ++# e.g. $(call MXL_PROBE,domain/.mxl-flow) ++# $(call MXL_PROBE,mxl:///domain?id=) ++define MXL_PROBE ++$(TARGET_PATH)/ffprobe -hide_banner -v verbose -f mxl -i "$(1)" 2>&1 | $(PROBE_LOG_FILTER) ++endef ++ ++fate-mxl-video-encdec: CMD = \ ++ $(call MXL_MUX_DEMUX_TEST, \ ++ mxl-video-encdec, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error -re \ ++ -f lavfi -i testsrc2=size=1920x1080:rate=50 -frames:v 5 -c:v v210 \ ++ -f mxl -video_flow_id $(MXL_VIDEO_FLOW_ID) \ ++ -teardown_sync_file sentinel -teardown_sync_timeout $(MXL_SENTINEL_TIMEOUT) \ ++ domain, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error \ ++ -f mxl -max_video_frames 5 -grain_index_init 2 \ ++ -i domain/$(MXL_VIDEO_FLOW_ID).mxl-flow \ ++ -f framemd5 pipe:1, \ ++ demuxer) ++fate-mxl-video-encdec: REF = $(SRC_PATH)/tests/ref/fate/mxl-video-encdec ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-video-encdec)) ++ ++fate-mxl-audio-encdec: CMD = \ ++ $(call MXL_MUX_DEMUX_TEST, \ ++ mxl-audio-encdec, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -re -v error \ ++ -f lavfi -i "anoisesrc=sample_rate=48000:nb_samples=$(MXL_AUDIO_SAMPLES_PER_PACKET):seed=0$(COMMA)aformat=sample_fmts=flt:channel_layouts=stereo$(COMMA)atrim=end_sample=$(MXL_AUDIO_MAX_SAMPLES)" \ ++ -map 0:a:0 -c:a pcm_f32le \ ++ -f mxl -audio_flow_id $(MXL_AUDIO_FLOW_ID) \ ++ -teardown_sync_file sentinel -teardown_sync_timeout $(MXL_SENTINEL_TIMEOUT) \ ++ domain, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error \ ++ -f mxl -max_audio_samples $(MXL_AUDIO_MAX_SAMPLES) -max_audio_samples_per_read $(MXL_AUDIO_SAMPLES_PER_PACKET) -grain_index_init 2 \ ++ -i domain/$(MXL_AUDIO_FLOW_ID).mxl-flow \ ++ -f framemd5 pipe:1, \ ++ demuxer) ++fate-mxl-audio-encdec: REF = $(SRC_PATH)/tests/ref/fate/mxl-audio-encdec ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-audio-encdec)) ++ ++fate-mxl-av-encdec: CMD = \ ++ $(call MXL_MUX_DEMUX_TEST, \ ++ mxl-av-encdec, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error -re \ ++ -f lavfi -i testsrc2=size=1920x1080:rate=50 \ ++ -f lavfi -i "anoisesrc=sample_rate=48000:nb_samples=$(MXL_AUDIO_SAMPLES_PER_PACKET):seed=0$(COMMA)aformat=sample_fmts=flt:channel_layouts=stereo$(COMMA)atrim=end_sample=$(MXL_AUDIO_MAX_SAMPLES)" \ ++ -map 0:v:0 -map 1:a:0 -frames:v 5 \ ++ -c:v v210 -c:a pcm_f32le \ ++ -f mxl -video_flow_id $(MXL_VIDEO_FLOW_ID) -audio_flow_id $(MXL_AUDIO_FLOW_ID) \ ++ -teardown_sync_file sentinel -teardown_sync_timeout $(MXL_SENTINEL_TIMEOUT) \ ++ domain, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error \ ++ -f mxl -max_video_frames 5 \ ++ -max_audio_samples $(MXL_AUDIO_MAX_SAMPLES) -max_audio_samples_per_read $(MXL_AUDIO_SAMPLES_PER_PACKET) \ ++ -grain_index_init 2 \ ++ -i "mxl://$$(pwd)/domain?id=$(MXL_VIDEO_FLOW_ID)&id=$(MXL_AUDIO_FLOW_ID)" \ ++ -f framemd5 pipe:1, \ ++ demuxer) ++fate-mxl-av-encdec: REF = $(SRC_PATH)/tests/ref/fate/mxl-av-encdec ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-av-encdec)) ++ ++fate-mxl-video-probe: CMD = \ ++ $(call MXL_MUX_DEMUX_TEST, \ ++ mxl-video-probe, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error -re \ ++ -f lavfi -i testsrc2=size=1920x1080:rate=50 -frames:v 5 -c:v v210 \ ++ -f mxl -video_flow_id $(MXL_VIDEO_FLOW_ID) \ ++ -teardown_sync_file sentinel \ ++ -teardown_sync_timeout $(MXL_SENTINEL_TIMEOUT) \ ++ domain, \ ++ $(TARGET_PATH)/ffprobe -hide_banner -v info \ ++ mxl://$$(pwd)/domain?id=$(MXL_VIDEO_FLOW_ID) 2>&1, \ ++ ffprobe) ++fate-mxl-video-probe: REF = $(SRC_PATH)/tests/ref/fate/mxl-video-probe ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-video-probe)) ++ ++# uri test if demuxer is enabled ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-uri) ++ ++# video encode/decode test if both muxer and demuxer are enabled ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-audio-probe)) ++ ++fate-mxl-av-probe: CMD = \ ++ $(call MXL_MUX_DEMUX_TEST, \ ++ mxl-audio-video-probe, \ ++ $(TARGET_PATH)/ffmpeg -hide_banner -v error -re \ ++ -f lavfi -i testsrc2=size=1920x1080:rate=50 \ ++ -f lavfi -i "anoisesrc=sample_rate=48000:nb_samples=$(MXL_AUDIO_SAMPLES_PER_PACKET):seed=0$(COMMA)aformat=sample_fmts=flt:channel_layouts=stereo$(COMMA)atrim=end_sample=$(MXL_AUDIO_MAX_SAMPLES)" \ ++ -map 0:v:0 -map 1:a:0 -frames:v 5 \ ++ -c:v v210 -c:a pcm_f32le \ ++ -f mxl -video_flow_id $(MXL_VIDEO_FLOW_ID) -audio_flow_id $(MXL_AUDIO_FLOW_ID) \ ++ -teardown_sync_file sentinel -teardown_sync_timeout $(MXL_SENTINEL_TIMEOUT) \ ++ domain, \ ++ $(TARGET_PATH)/ffprobe -hide_banner -v info \ ++ "mxl://$$(pwd)/domain?id=$(MXL_VIDEO_FLOW_ID)&id=$(MXL_AUDIO_FLOW_ID)" 2>&1, \ ++ ffprobe) ++fate-mxl-av-probe: REF = $(SRC_PATH)/tests/ref/fate/mxl-av-probe ++FATE-yes += \ ++ $(if $(filter yes,$(CONFIG_MXL_DEMUXER)), \ ++ $(if $(filter yes,$(CONFIG_MXL_MUXER)),fate-mxl-av-probe)) ++ ++fate-mxl-bad-loc: CMD = (\ ++ $(call MXL_PROBE,/domain/without/flow); \ ++ $(call MXL_PROBE,mxl:///domain/without/flow); \ ++ $(call MXL_PROBE,/domain/bad-flow-id.mxl-flow); \ ++ $(call MXL_PROBE,mxl:///domain?id=bad-flow-id); \ ++ $(call MXL_PROBE,/domain/bad-$(MXL_VIDEO_FLOW_ID).mxl-flow); \ ++ $(call MXL_PROBE,mxl:///domain?id=bad-$(MXL_VIDEO_FLOW_ID)); \ ++ $(call MXL_PROBE,mxl:///domain/duplicate_flows?id=$(MXL_VIDEO_FLOW_ID)&id=$(MXL_VIDEO_FLOW_ID)); \ ++ $(call MXL_PROBE,mxl://with-host/domain?id=$(MXL_VIDEO_FLOW_ID)); \ ++ true \ ++ ) ++fate-mxl-bad-loc: REF = $(SRC_PATH)/tests/ref/fate/mxl-bad-loc ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-bad-loc) ++ ++fate-mxl-bad-domain: CMD = (\ ++ export MXL_LOG_LEVEL=off; \ ++ $(call MXL_PROBE,/domain/does/not/exist/$(MXL_VIDEO_FLOW_ID).mxl-flow); \ ++ $(call MXL_PROBE,mxl:///domain/does/not/exist?id=$(MXL_VIDEO_FLOW_ID)); \ ++ true \ ++ ) ++fate-mxl-bad-domain: REF = $(SRC_PATH)/tests/ref/fate/mxl-bad-domain ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-bad-domain) ++ ++fate-mxl-bad-flow: CMD = (\ ++ export MXL_LOG_LEVEL=off; \ ++ TEST_DIR="$(MXL_TMP_DIR)/bad_flow"; \ ++ rm -rf "$$TEST_DIR"; \ ++ mkdir -p "$$TEST_DIR"/domain; \ ++ (cd "$$TEST_DIR"; $(call MXL_PROBE,domain/$(MXL_VIDEO_FLOW_ID).mxl-flow)); \ ++ $(call MXL_PROBE,mxl://"$$TEST_DIR"/domain?id=$(MXL_VIDEO_FLOW_ID)); \ ++ true \ ++ ) ++fate-mxl-bad-flow: REF = $(SRC_PATH)/tests/ref/fate/mxl-bad-flow ++FATE-yes += $(if $(filter yes,$(CONFIG_MXL_DEMUXER)),fate-mxl-bad-flow) +diff --git a/tests/ref/fate/mxl-audio-encdec b/tests/ref/fate/mxl-audio-encdec +new file mode 100644 +index 0000000000..8b8569c2f8 +--- /dev/null ++++ b/tests/ref/fate/mxl-audio-encdec +@@ -0,0 +1,15 @@ ++#format: frame checksums ++#version: 2 ++#hash: MD5 ++#software: Lavf62.6.101 ++#tb 0: 1/48000 ++#media_type 0: audio ++#codec_id 0: pcm_s16le ++#sample_rate 0: 48000 ++#channel_layout_name 0: stereo ++#stream#, dts, pts, duration, size, hash ++0, 0, 0, 512, 2048, 53bec1b67fed459ad5cae29e678ea3b2 ++0, 512, 512, 512, 2048, d79f812809c33e70086b4fcf4fb929af ++0, 1024, 1024, 512, 2048, 0d07cdebfd31e4dc50fd606d150a8070 ++0, 1536, 1536, 512, 2048, 673b3cffcac431e42d0ceabfceae6bb1 ++0, 2048, 2048, 511, 2044, 3b4f890dc442919fe145dbe84796f30c +diff --git a/tests/ref/fate/mxl-audio-probe b/tests/ref/fate/mxl-audio-probe +new file mode 100644 +index 0000000000..4970010311 +--- /dev/null ++++ b/tests/ref/fate/mxl-audio-probe +@@ -0,0 +1,9 @@ ++Input #0, mxl, from 'mxl:///path/to/domain?id=ff39f65b-d760-4a7c-808d-4f2778de5658': ++ Duration: N/A, bitrate: N/A ++ Stream #0:0: Audio: pcm_f32le, 48000 Hz, 2 channels, flt, 3072 kb/s ++ Metadata: ++ mxl_id : ff39f65b-d760-4a7c-808d-4f2778de5658 ++ mxl_description : FFmpeg audio stream ++ mxl_label : FFmpeg audio stream ++ mxl_format : urn:x-nmos:format:audio ++ mxl_media_type : audio/float32 +diff --git a/tests/ref/fate/mxl-av-encdec b/tests/ref/fate/mxl-av-encdec +new file mode 100644 +index 0000000000..9035786829 +--- /dev/null ++++ b/tests/ref/fate/mxl-av-encdec +@@ -0,0 +1,25 @@ ++#format: frame checksums ++#version: 2 ++#hash: MD5 ++#software: Lavf62.6.101 ++#tb 0: 1/50 ++#media_type 0: video ++#codec_id 0: rawvideo ++#dimensions 0: 1920x1080 ++#sar 0: 1/1 ++#tb 1: 1/48000 ++#media_type 1: audio ++#codec_id 1: pcm_s16le ++#sample_rate 1: 48000 ++#channel_layout_name 1: stereo ++#stream#, dts, pts, duration, size, hash ++0, 0, 0, 1, 8294400, 687ba6aecec048f446ee3cbe03f611b3 ++1, 0, 0, 512, 2048, 53bec1b67fed459ad5cae29e678ea3b2 ++1, 512, 512, 512, 2048, d79f812809c33e70086b4fcf4fb929af ++0, 1, 1, 1, 8294400, fa8087d706277bffe0181b862c9e8252 ++1, 1024, 1024, 512, 2048, 0d07cdebfd31e4dc50fd606d150a8070 ++1, 1536, 1536, 512, 2048, 673b3cffcac431e42d0ceabfceae6bb1 ++0, 2, 2, 1, 8294400, 19190a35fe54888aa56367a6672cb02d ++1, 2048, 2048, 511, 2044, 3b4f890dc442919fe145dbe84796f30c ++0, 3, 3, 1, 8294400, 13b4e70275b3e18080ea2411a1c7e47c ++0, 4, 4, 1, 8294400, 34c7f0e53d40f8da58a1d48510d425a8 +diff --git a/tests/ref/fate/mxl-av-probe b/tests/ref/fate/mxl-av-probe +new file mode 100644 +index 0000000000..ca09979e63 +--- /dev/null ++++ b/tests/ref/fate/mxl-av-probe +@@ -0,0 +1,17 @@ ++Input #0, mxl, from 'mxl:///path/to/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8&id=ff39f65b-d760-4a7c-808d-4f2778de5658': ++ Duration: N/A, bitrate: N/A ++ Stream #0:0: Video: v210 (v210 / 0x30313276), yuv422p10le(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 50 fps, 50 tbr, 50 tbn ++ Metadata: ++ mxl_id : 717f834b-4224-4c9b-8a64-ecb7726803b8 ++ mxl_description : FFmpeg video stream ++ mxl_label : FFmpeg video stream ++ mxl_format : urn:x-nmos:format:video ++ mxl_media_type : video/v210 ++ mxl_colorspace : BT709 ++ Stream #0:1: Audio: pcm_f32le, 48000 Hz, 2 channels, flt, 3072 kb/s ++ Metadata: ++ mxl_id : ff39f65b-d760-4a7c-808d-4f2778de5658 ++ mxl_description : FFmpeg audio stream ++ mxl_label : FFmpeg audio stream ++ mxl_format : urn:x-nmos:format:audio ++ mxl_media_type : audio/float32 +diff --git a/tests/ref/fate/mxl-bad-domain b/tests/ref/fate/mxl-bad-domain +new file mode 100644 +index 0000000000..b54a5ee7e0 +--- /dev/null ++++ b/tests/ref/fate/mxl-bad-domain +@@ -0,0 +1,8 @@ ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: /domain/does/not/exist/717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow ++[mxl @ 0xADDR] mxlCreateInstance error ++/domain/does/not/exist/717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow: Input/output error ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///domain/does/not/exist?id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] mxlCreateInstance error ++mxl:///domain/does/not/exist?id=717f834b-4224-4c9b-8a64-ecb7726803b8: Input/output error +diff --git a/tests/ref/fate/mxl-bad-flow b/tests/ref/fate/mxl-bad-flow +new file mode 100644 +index 0000000000..10cef99911 +--- /dev/null ++++ b/tests/ref/fate/mxl-bad-flow +@@ -0,0 +1,8 @@ ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: domain/717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow ++[mxl @ 0xADDR] mxlCreateFlowReader error MXL_ERR_FLOW_NOT_FOUND for flow ID 717f834b-4224-4c9b-8a64-ecb7726803b8 ++domain/717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow: Input/output error ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///path/to/tests/data/tmp/mxl/bad_flow/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] mxlCreateFlowReader error MXL_ERR_FLOW_NOT_FOUND for flow ID 717f834b-4224-4c9b-8a64-ecb7726803b8 ++mxl:///path/to/tests/data/tmp/mxl/bad_flow/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8: Input/output error +diff --git a/tests/ref/fate/mxl-bad-loc b/tests/ref/fate/mxl-bad-loc +new file mode 100644 +index 0000000000..47e765f3b1 +--- /dev/null ++++ b/tests/ref/fate/mxl-bad-loc +@@ -0,0 +1,43 @@ ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: /domain/without/flow ++[mxl @ 0xADDR] locator requires at least one flow ID ++[mxl @ 0xADDR] invalid locator: /domain/without/flow ++/domain/without/flow: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///domain/without/flow ++[mxl @ 0xADDR] locator requires at least one flow ID ++[mxl @ 0xADDR] invalid locator: mxl:///domain/without/flow ++mxl:///domain/without/flow: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: /domain/bad-flow-id.mxl-flow ++[mxl @ 0xADDR] LOC URI flow ID is not a valid uuid: bad-flow-id ++[mxl @ 0xADDR] failed to parse resource locator: /domain/bad-flow-id.mxl-flow ++/domain/bad-flow-id.mxl-flow: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///domain?id=bad-flow-id ++[mxl @ 0xADDR] URI invalid flow ID parameter: bad-flow-id ++[mxl @ 0xADDR] LOC URI scheme found but parse failed: "mxl:///domain?id=bad-flow-id" ++[mxl @ 0xADDR] failed to parse resource locator: mxl:///domain?id=bad-flow-id ++mxl:///domain?id=bad-flow-id: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: /domain/bad-717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow ++[mxl @ 0xADDR] LOC URI flow ID is not a valid uuid: bad-717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] failed to parse resource locator: /domain/bad-717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow ++/domain/bad-717f834b-4224-4c9b-8a64-ecb7726803b8.mxl-flow: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///domain?id=bad-717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] URI invalid flow ID parameter: bad-717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] LOC URI scheme found but parse failed: "mxl:///domain?id=bad-717f834b-4224-4c9b-8a64-ecb7726803b8" ++[mxl @ 0xADDR] failed to parse resource locator: mxl:///domain?id=bad-717f834b-4224-4c9b-8a64-ecb7726803b8 ++mxl:///domain?id=bad-717f834b-4224-4c9b-8a64-ecb7726803b8: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl:///domain/duplicate_flows?id=717f834b-4224-4c9b-8a64-ecb7726803b8&id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] URI locator flow ID not unique: 717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] LOC URI scheme found but parse failed: "mxl:///domain/duplicate_flows?id=717f834b-4224-4c9b-8a64-ecb7726803b8&id=717f834b-4224-4c9b-8a64-ecb7726803b8" ++[mxl @ 0xADDR] failed to parse resource locator: mxl:///domain/duplicate_flows?id=717f834b-4224-4c9b-8a64-ecb7726803b8&id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++mxl:///domain/duplicate_flows?id=717f834b-4224-4c9b-8a64-ecb7726803b8&id=717f834b-4224-4c9b-8a64-ecb7726803b8: Invalid argument ++[mxl @ 0xADDR] mxl AVFormatContext: ++[mxl @ 0xADDR] url: mxl://with-host/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++[mxl @ 0xADDR] LOC URI host not supported: "mxl://with-host/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8" ++[mxl @ 0xADDR] failed to parse resource locator: mxl://with-host/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8 ++mxl://with-host/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8: Invalid argument +diff --git a/tests/ref/fate/mxl-diag b/tests/ref/fate/mxl-diag +new file mode 100644 +index 0000000000..35c233d251 +--- /dev/null ++++ b/tests/ref/fate/mxl-diag +@@ -0,0 +1,82 @@ ++testing mxl_diag: mxl diagnostic server ++ ++ ++test: init fails if server path exists as a non-socket ++ ++test: init cleans up stale socket path ++ ++test: release with no active client is ignored ++ ++test: valid connect works after junk from unrelated socket ++ ++test: mxl_diag_poll returns expected codes for malformed messages ++bad size, datagram smaller than sizeof(msg.header) ++bad size, header.size smaller than sizeof(msg.header) ++bad size, header.size bigger than sizeof(msg) ++bad size, datagram size vs header.size mismatch ++bad size, truncated datagram ++bad version ++unknown type ++truncated header ++connect message size mismatch ++release message size mismatch ++ ++test: print message stream ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 0, exec_dur = 1, tail_index = 2, head_index = 3, read_index = 4, read_size = 1600, mxl_status = 0 ++msg: version=1, type=3, size=64 ++video_read: timestamp = 1, exec_dur = 2, tail_index = 3, head_index = 4, read_index = 5, mxl_status = 0 ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 2, exec_dur = 3, tail_index = 4, head_index = 5, read_index = 6, read_size = 1600, mxl_status = 0 ++msg: version=1, type=3, size=64 ++video_read: timestamp = 3, exec_dur = 4, tail_index = 5, head_index = 6, read_index = 7, mxl_status = 0 ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 4, exec_dur = 5, tail_index = 6, head_index = 7, read_index = 8, read_size = 1600, mxl_status = 0 ++msg: version=1, type=3, size=64 ++video_read: timestamp = 5, exec_dur = 6, tail_index = 7, head_index = 8, read_index = 9, mxl_status = 0 ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 6, exec_dur = 7, tail_index = 8, head_index = 9, read_index = 10, read_size = 1600, mxl_status = 0 ++msg: version=1, type=3, size=64 ++video_read: timestamp = 7, exec_dur = 8, tail_index = 9, head_index = 10, read_index = 11, mxl_status = 0 ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 8, exec_dur = 9, tail_index = 10, head_index = 11, read_index = 12, read_size = 1600, mxl_status = 0 ++msg: version=1, type=3, size=64 ++video_read: timestamp = 9, exec_dur = 10, tail_index = 11, head_index = 12, read_index = 13, mxl_status = 0 ++ ++test: multiple connect attempts are ignored after the first ++client A connected ++msg: version=1, type=3, size=64 ++video_read: timestamp = 11, exec_dur = 12, tail_index = 13, head_index = 14, read_index = 15, mxl_status = 0 ++client B timed out (expected) ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 12, exec_dur = 13, tail_index = 14, head_index = 15, read_index = 16, read_size = 1600, mxl_status = 0 ++client A was unaffected by client B ++client C timed out (expected) ++msg: version=1, type=3, size=64 ++video_read: timestamp = 13, exec_dur = 14, tail_index = 15, head_index = 16, read_index = 17, mxl_status = 0 ++client A was unaffected by client C ++client B still times out ++ ++test: malformed messages from active client are ignored ++msg: version=1, type=4, size=64 ++audio_read: timestamp = 22, exec_dur = 23, tail_index = 24, head_index = 25, read_index = 26, read_size = 1600, mxl_status = 0 ++bad size, message too short ++bad size, header size sanity (too small) ++bad size, header size sanity (too big) ++unknown type, truncated header ++bad version ++unknown type ++garbage message ++bad size, message too short ++bad size, header size sanity (too small) ++bad size, header size sanity (too big) ++unknown type, truncated header ++bad version ++unknown type ++garbage message ++sent 14 malformed messages from active client A ++msg: version=1, type=3, size=64 ++video_read: timestamp = 23, exec_dur = 24, tail_index = 25, head_index = 26, read_index = 27, mxl_status = 0 ++client A unaffected by malformed traffic ++ ++PASS +diff --git a/tests/ref/fate/mxl-json b/tests/ref/fate/mxl-json +new file mode 100644 +index 0000000000..5b0162c29a +--- /dev/null ++++ b/tests/ref/fate/mxl-json +@@ -0,0 +1,1041 @@ ++testing mxl_json: jsmn based MXL resource definition parser ++ ++sanity test: ++{"array_a": [0,1,2,3],"object_a": {"a": 1, "b": [0,1,2,3], "c": {"x": "y"} },"grain_rate": {"numerator": 25, "denominator": 1},"format": {"name": "video"},"media_type": "video/v210","frame_width": 1920,"array_b": [{"m": n, "o": [4,5,6]}, "p", ["q",7,{"r":8}]],"object_b": {"s": [9,"t",true], "u": {"v":{"w":10}}},"a_null": null,"a_true": true,"a_false": false,"array_c": [0, "one", {"three": "four"}],"negnums": {"a": -1.23, "b": -.123},"expnums": {"a": 1.23e2, "b": 1.23e+2, "c": 1.23e-2},"object_c": {"last": "final"},"ctrl_char": {"text":"line1\nline2"}} ++idx: type str end sz ++000: objc 000 557 016 '{"array_a": [0,1,2,3],"object_a": {"a": 1, "b": [0,1,2,3], "c": {"x": "y"} },"grain_rate": {"numerator": 25, "denominator": 1},"format": {"name": "video"},"media_type": "video/v210","frame_width": 1920,"array_b": [{"m": n, "o": [4,5,6]}, "p", ["q",7,{"r":8}]],"object_b": {"s": [9,"t",true], "u": {"v":{"w":10}}},"a_null": null,"a_true": true,"a_false": false,"array_c": [0, "one", {"three": "four"}],"negnums": {"a": -1.23, "b": -.123},"expnums": {"a": 1.23e2, "b": 1.23e+2, "c": 1.23e-2},"object_c": {"last": "final"},"ctrl_char": {"text":"line1\nline2"}}' ++001: strn 002 009 001 'array_a' ++002: arry 012 021 004 '[0,1,2,3]' ++003: prim 013 014 000 '0' ++004: prim 015 016 000 '1' ++005: prim 017 018 000 '2' ++006: prim 019 020 000 '3' ++007: strn 023 031 001 'object_a' ++008: objc 034 076 003 '{"a": 1, "b": [0,1,2,3], "c": {"x": "y"} }' ++009: strn 036 037 001 'a' ++010: prim 040 041 000 '1' ++011: strn 044 045 001 'b' ++012: arry 048 057 004 '[0,1,2,3]' ++013: prim 049 050 000 '0' ++014: prim 051 052 000 '1' ++015: prim 053 054 000 '2' ++016: prim 055 056 000 '3' ++017: strn 060 061 001 'c' ++018: objc 064 074 001 '{"x": "y"}' ++019: strn 066 067 001 'x' ++020: strn 071 072 000 'y' ++021: strn 078 088 001 'grain_rate' ++022: objc 091 126 002 '{"numerator": 25, "denominator": 1}' ++023: strn 093 102 001 'numerator' ++024: prim 105 107 000 '25' ++025: strn 110 121 001 'denominator' ++026: prim 124 125 000 '1' ++027: strn 128 134 001 'format' ++028: objc 137 154 001 '{"name": "video"}' ++029: strn 139 143 001 'name' ++030: strn 147 152 000 'video' ++031: strn 156 166 001 'media_type' ++032: strn 170 180 000 'video/v210' ++033: strn 183 194 001 'frame_width' ++034: prim 197 201 000 '1920' ++035: strn 203 210 001 'array_b' ++036: arry 213 259 003 '[{"m": n, "o": [4,5,6]}, "p", ["q",7,{"r":8}]]' ++037: objc 214 236 002 '{"m": n, "o": [4,5,6]}' ++038: strn 216 217 001 'm' ++039: prim 220 221 000 'n' ++040: strn 224 225 001 'o' ++041: arry 228 235 003 '[4,5,6]' ++042: prim 229 230 000 '4' ++043: prim 231 232 000 '5' ++044: prim 233 234 000 '6' ++045: strn 239 240 000 'p' ++046: arry 243 258 003 '["q",7,{"r":8}]' ++047: strn 245 246 000 'q' ++048: prim 248 249 000 '7' ++049: objc 250 257 001 '{"r":8}' ++050: strn 252 253 001 'r' ++051: prim 255 256 000 '8' ++052: strn 261 269 001 'object_b' ++053: objc 272 312 002 '{"s": [9,"t",true], "u": {"v":{"w":10}}}' ++054: strn 274 275 001 's' ++055: arry 278 290 003 '[9,"t",true]' ++056: prim 279 280 000 '9' ++057: strn 282 283 000 't' ++058: prim 285 289 000 'true' ++059: strn 293 294 001 'u' ++060: objc 297 311 001 '{"v":{"w":10}}' ++061: strn 299 300 001 'v' ++062: objc 302 310 001 '{"w":10}' ++063: strn 304 305 001 'w' ++064: prim 307 309 000 '10' ++065: strn 314 320 001 'a_null' ++066: prim 323 327 000 'null' ++067: strn 329 335 001 'a_true' ++068: prim 338 342 000 'true' ++069: strn 344 351 001 'a_false' ++070: prim 354 359 000 'false' ++071: strn 361 368 001 'array_c' ++072: arry 371 400 003 '[0, "one", {"three": "four"}]' ++073: prim 372 373 000 '0' ++074: strn 376 379 000 'one' ++075: objc 382 399 001 '{"three": "four"}' ++076: strn 384 389 001 'three' ++077: strn 393 397 000 'four' ++078: strn 402 409 001 'negnums' ++079: objc 412 436 002 '{"a": -1.23, "b": -.123}' ++080: strn 414 415 001 'a' ++081: prim 418 423 000 '-1.23' ++082: strn 426 427 001 'b' ++083: prim 430 435 000 '-.123' ++084: strn 438 445 001 'expnums' ++085: objc 448 489 003 '{"a": 1.23e2, "b": 1.23e+2, "c": 1.23e-2}' ++086: strn 450 451 001 'a' ++087: prim 454 460 000 '1.23e2' ++088: strn 463 464 001 'b' ++089: prim 467 474 000 '1.23e+2' ++090: strn 477 478 001 'c' ++091: prim 481 488 000 '1.23e-2' ++092: strn 491 499 001 'object_c' ++093: objc 502 519 001 '{"last": "final"}' ++094: strn 504 508 001 'last' ++095: strn 512 517 000 'final' ++096: strn 521 530 001 'ctrl_char' ++097: objc 533 556 001 '{"text":"line1\nline2"}' ++098: strn 535 539 001 'text' ++099: strn 542 554 000 'line1\nline2' ++PASS "media_type" = {"video/v210", SUCCESS} ++PASS "format.name" = {"video", SUCCESS} ++PASS "object_c.last" = {"final", SUCCESS} ++PASS "frame_width" = {1920, SUCCESS} ++PASS "grain_rate.numerator" = {25, SUCCESS} ++PASS "grain_rate.denominator" = {1, SUCCESS} ++PASS "object_a.a" = {1, SUCCESS} ++PASS "object_b.u.v.w" = {10, SUCCESS} ++PASS "negnums.a" = {-1.23, SUCCESS} ++PASS "negnums.b" = {-0.123, SUCCESS} ++PASS "expnums.a" = {123, SUCCESS} ++PASS "expnums.b" = {123, SUCCESS} ++PASS "expnums.c" = {0.0123, SUCCESS} ++PASS "ctrl_char.text" = {"line1\nline2", SUCCESS} ++PASS "a_null" = {nan, MXL_JSON_NULL} ++PASS "a_null" = {"(null)", MXL_JSON_NULL} ++PASS "a_true" = {nan, MXL_JSON_EBADTYPE} ++PASS "a_false" = {nan, MXL_JSON_EBADTYPE} ++PASS "a_false" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "a_true" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "format.name" = {nan, MXL_JSON_EBADTYPE} ++PASS "grain_rate.numerator" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "array_a" = {nan, MXL_JSON_EBADTYPE} ++PASS "array_b" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "does_not_exist" = {nan, MXL_JSON_ENOTFOUND} ++PASS "does.not_exist" = {nan, MXL_JSON_ENOTFOUND} ++PASS "blah" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "blah.yadda" = {"(null)", MXL_JSON_ENOTFOUND} ++ ++abberant input tests: ++ ++START abberant test: invalid_empty ++ ++idx: type str end sz ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: invalid_empty ++ ++START abberant test: valid_empty_object ++{} ++idx: type str end sz ++000: objc 000 002 000 '{}' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_empty_object ++ ++START abberant test: valid_empty_array ++[] ++idx: type str end sz ++000: arry 000 002 000 '[]' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_empty_array ++ ++START abberant test: valid_empty_string ++"" ++idx: type str end sz ++000: strn 001 001 000 '' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_empty_string ++ ++START abberant test: valid_single_number ++123.346 ++idx: type str end sz ++000: prim 000 007 000 '123.346' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_number ++ ++START abberant test: valid_single_neg_number ++-123.346 ++idx: type str end sz ++000: prim 000 008 000 '-123.346' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_neg_number ++ ++START abberant test: valid_single_exp_number ++1.23346e2 ++idx: type str end sz ++000: prim 000 009 000 '1.23346e2' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_exp_number ++ ++START abberant test: valid_single_true ++true ++idx: type str end sz ++000: prim 000 004 000 'true' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_true ++ ++START abberant test: valid_single_false ++false ++idx: type str end sz ++000: prim 000 005 000 'false' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_false ++ ++START abberant test: valid_single_null ++null ++idx: type str end sz ++000: prim 000 004 000 'null' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: valid_single_null ++ ++START abberant test: misspelled_single_null ++nul ++idx: type str end sz ++000: prim 000 003 000 'nul' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: misspelled_single_null ++ ++START abberant test: invalid_leading_plus ++{"a": +123.346} ++idx: type str end sz ++000: objc 000 015 001 '{"a": +123.346}' ++001: strn 002 003 001 'a' ++002: prim 006 014 000 '+123.346' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: invalid_leading_plus ++ ++START abberant test: invalid_exp_number ++{"a": 1.23346e} ++idx: type str end sz ++000: objc 000 015 001 '{"a": 1.23346e}' ++001: strn 002 003 001 'a' ++002: prim 006 014 000 '1.23346e' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: invalid_exp_number ++ ++START abberant test: invalid_number_sep ++{"a": 1,23346} ++idx: type str end sz ++000: objc 000 014 002 '{"a": 1,23346}' ++001: strn 002 003 001 'a' ++002: prim 006 007 000 '1' ++003: prim 008 013 000 '23346' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: invalid_number_sep ++ ++START abberant test: not_json ++this is totally wrong ++idx: type str end sz ++000: prim 000 004 000 'this' ++001: prim 005 007 000 'is' ++002: prim 008 015 000 'totally' ++003: prim 016 021 000 'wrong' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: not_json ++ ++START abberant test: missing_key_quotes ++{foo: "bar"} ++idx: type str end sz ++000: objc 000 012 001 '{foo: "bar"}' ++001: prim 001 004 001 'foo' ++002: strn 007 010 000 'bar' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: missing_key_quotes ++ ++START abberant test: single_quotes ++{'foo': 'bar'} ++idx: type str end sz ++000: objc 000 014 001 '{'foo': 'bar'}' ++001: prim 001 006 001 ''foo'' ++002: prim 008 013 000 ''bar'' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: single_quotes ++ ++START abberant test: trailing_comma_object ++{ "foo": "bar", } ++idx: type str end sz ++000: objc 000 017 001 '{ "foo": "bar", }' ++001: strn 003 006 001 'foo' ++002: strn 010 013 000 'bar' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: trailing_comma_object ++ ++START abberant test: trailing_comma_array ++[ "a", "b", ] ++idx: type str end sz ++000: arry 000 013 002 '[ "a", "b", ]' ++001: strn 003 004 000 'a' ++002: strn 008 009 000 'b' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: trailing_comma_array ++ ++START abberant test: dangling_key ++{"foo":} ++idx: type str end sz ++000: objc 000 008 001 '{"foo":}' ++001: strn 002 005 000 'foo' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: dangling_key ++ ++START abberant test: abnormal_termination ++{"foo": "bar"} what's this? ++idx: type str end sz ++000: objc 000 014 001 '{"foo": "bar"}' ++001: strn 002 005 001 'foo' ++002: strn 009 012 000 'bar' ++003: prim 015 021 000 'what's' ++004: prim 022 027 000 'this?' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: abnormal_termination ++ ++START abberant test: unescaped_ctl_char ++{ "text": "line1 ++line2" } ++idx: type str end sz ++000: objc 000 025 001 '{ "text": "line1 ++line2" }' ++001: strn 003 007 001 'text' ++002: strn 011 022 000 'line1 ++line2' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: unescaped_ctl_char ++ ++START abberant test: missing_both_braces ++"foo": "bar" ++idx: type str end sz ++000: strn 001 004 001 'foo' ++001: strn 008 011 000 'bar' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS abberant: missing_both_braces ++ ++START abberant test: missing_start_brace ++json_doc_build failed ++PASS expected build error: AVERROR_INVALIDDATA (AVERROR_INVALIDDATA) ++PASS abberant: missing_start_brace ++ ++START abberant test: missing_end_brace ++json_doc_build failed ++PASS expected build error: AVERROR_INVALIDDATA (AVERROR_INVALIDDATA) ++PASS abberant: missing_end_brace ++ ++START abberant test: misspelled_contained_null ++{"misspelled_null": nul} ++idx: type str end sz ++000: objc 000 024 001 '{"misspelled_null": nul}' ++001: strn 002 017 001 'misspelled_null' ++002: prim 020 023 000 'nul' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS "misspelled_null" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "misspelled_null" = {nan, MXL_JSON_EBADTYPE} ++PASS abberant: misspelled_contained_null ++ ++START abberant test: capital_contained_null ++{"capital_null": NULL} ++idx: type str end sz ++000: objc 000 022 001 '{"capital_null": NULL}' ++001: strn 002 014 001 'capital_null' ++002: prim 017 021 000 'NULL' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS "capital_null" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "capital_null" = {nan, MXL_JSON_EBADTYPE} ++PASS abberant: capital_contained_null ++ ++START abberant test: unquoted_literal ++{"what": yes } ++idx: type str end sz ++000: objc 000 014 001 '{"what": yes }' ++001: strn 002 006 001 'what' ++002: prim 009 012 000 'yes' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS "what" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "what" = {nan, MXL_JSON_EBADTYPE} ++PASS abberant: unquoted_literal ++ ++START abberant test: nan_value ++{"value": NaN } ++idx: type str end sz ++000: objc 000 015 001 '{"value": NaN }' ++001: strn 002 007 001 'value' ++002: prim 010 013 000 'NaN' ++PASS "absent" = {"(null)", MXL_JSON_ENOTFOUND} ++PASS "absent" = {nan, MXL_JSON_ENOTFOUND} ++PASS "value" = {"(null)", MXL_JSON_EBADTYPE} ++PASS "value" = {nan, MXL_JSON_EBADTYPE} ++PASS abberant: nan_value ++ ++START abberant test: inf_values ++{"a":Infinity,"b":-Infinity,"c":Inf, "d":-Inf} ++idx: type str end sz ++000: objc 000 046 004 '{"a":Infinity,"b":-Infinity,"c":Inf, "d":-Inf}' ++001: strn 002 003 001 'a' ++002: prim 005 013 000 'Infinity' ++003: strn 015 016 001 'b' ++004: prim 018 027 000 '-Infinity' ++005: strn 029 030 001 'c' ++006: prim 032 035 000 'Inf' ++007: strn 038 039 001 'd' ++008: prim 041 045 000 '-Inf' ++PASS "a" = {inf, MXL_JSON_EBADTYPE} ++PASS "b" = {-inf, MXL_JSON_EBADTYPE} ++PASS "c" = {inf, MXL_JSON_EBADTYPE} ++PASS "d" = {-inf, MXL_JSON_EBADTYPE} ++PASS abberant: inf_values ++ ++START abberant test: valid_overflow_pos_number ++{"a": 1e400} ++idx: type str end sz ++000: objc 000 012 001 '{"a": 1e400}' ++001: strn 002 003 001 'a' ++002: prim 006 011 000 '1e400' ++PASS "a" = {inf, ERANGE} ++PASS abberant: valid_overflow_pos_number ++ ++START abberant test: valid_overflow_neg_number ++{"a": -1e400} ++idx: type str end sz ++000: objc 000 013 001 '{"a": -1e400}' ++001: strn 002 003 001 'a' ++002: prim 006 012 000 '-1e400' ++PASS "a" = {-inf, ERANGE} ++PASS abberant: valid_overflow_neg_number ++ ++START abberant test: valid_underflow_pos_number ++{"a": 1e-400} ++idx: type str end sz ++000: objc 000 013 001 '{"a": 1e-400}' ++001: strn 002 003 001 'a' ++002: prim 006 012 000 '1e-400' ++PASS "a" = {0, ERANGE} ++PASS abberant: valid_underflow_pos_number ++ ++START abberant test: valid_underflow_neg_number ++{"a": -1e-400} ++idx: type str end sz ++000: objc 000 014 001 '{"a": -1e-400}' ++001: strn 002 003 001 'a' ++002: prim 006 013 000 '-1e-400' ++PASS "a" = {-0, ERANGE} ++PASS abberant: valid_underflow_neg_number ++ ++non C locale number tests: ++test comma_dec_a ++{ "comma_dec_a": 3,14 } ++idx: type str end sz ++000: objc 000 023 002 '{ "comma_dec_a": 3,14 }' ++001: strn 003 014 001 'comma_dec_a' ++002: prim 017 018 000 '3' ++003: prim 019 021 000 '14' ++PASS comma_dec_a identified as malformed ++test comma_dec_b ++{ "comma_dec_b": 0,001 } ++idx: type str end sz ++000: objc 000 024 002 '{ "comma_dec_b": 0,001 }' ++001: strn 003 014 001 'comma_dec_b' ++002: prim 017 018 000 '0' ++003: prim 019 022 000 '001' ++PASS comma_dec_b identified as malformed ++test dot_thou_a ++{ "dot_thou_a": 1.234,56 } ++idx: type str end sz ++000: objc 000 026 002 '{ "dot_thou_a": 1.234,56 }' ++001: strn 003 013 001 'dot_thou_a' ++002: prim 016 021 000 '1.234' ++003: prim 022 024 000 '56' ++PASS dot_thou_a identified as malformed ++test dot_thou_b ++{ "dot_thou_b": 12.345,67 } ++idx: type str end sz ++000: objc 000 027 002 '{ "dot_thou_b": 12.345,67 }' ++001: strn 003 013 001 'dot_thou_b' ++002: prim 016 022 000 '12.345' ++003: prim 023 025 000 '67' ++PASS dot_thou_b identified as malformed ++test comma_thou_a ++{ "comma_thou_a": 1,234.56 } ++idx: type str end sz ++000: objc 000 028 002 '{ "comma_thou_a": 1,234.56 }' ++001: strn 003 015 001 'comma_thou_a' ++002: prim 018 019 000 '1' ++003: prim 020 026 000 '234.56' ++PASS comma_thou_a identified as malformed ++test comma_thou_b ++{ "comma_thou_b": 12,345.67 } ++idx: type str end sz ++000: objc 000 029 002 '{ "comma_thou_b": 12,345.67 }' ++001: strn 003 015 001 'comma_thou_b' ++002: prim 018 020 000 '12' ++003: prim 021 027 000 '345.67' ++PASS comma_thou_b identified as malformed ++test space_thou_a ++{ "space_thou_a": 1 234,56 } ++idx: type str end sz ++000: objc 000 028 002 '{ "space_thou_a": 1 234,56 }' ++001: strn 003 015 002 'space_thou_a' ++002: prim 018 019 000 '1' ++003: prim 020 023 000 '234' ++004: prim 024 026 000 '56' ++PASS space_thou_a identified as malformed ++test space_thou_b ++{ "space_thou_b": 1 234.56 } ++idx: type str end sz ++000: objc 000 028 001 '{ "space_thou_b": 1 234.56 }' ++001: strn 003 015 002 'space_thou_b' ++002: prim 018 019 000 '1' ++003: prim 020 026 000 '234.56' ++PASS space_thou_b identified as malformed ++test nbsp_thou_a ++PASS expected NOPARSE and parse did not succeed ++test nbsp_thou_b ++PASS expected NOPARSE and parse did not succeed ++test thin_thou_a ++PASS expected NOPARSE and parse did not succeed ++test thin_thou_b ++PASS expected NOPARSE and parse did not succeed ++test swiss_thou_a ++{ "swiss_thou_a": 1'234,56 } ++idx: type str end sz ++000: objc 000 028 002 '{ "swiss_thou_a": 1'234,56 }' ++001: strn 003 015 001 'swiss_thou_a' ++002: prim 018 023 000 '1'234' ++003: prim 024 026 000 '56' ++PASS swiss_thou_a identified as malformed ++test swiss_thou_b ++{ "swiss_thou_b": 1'234.56 } ++idx: type str end sz ++000: objc 000 028 001 '{ "swiss_thou_b": 1'234.56 }' ++001: strn 003 015 001 'swiss_thou_b' ++002: prim 018 026 000 '1'234.56' ++PASS swiss_thou_b identified as malformed ++test arabic_314 ++PASS expected NOPARSE and parse did not succeed ++test arabic_1234 ++PASS expected NOPARSE and parse did not succeed ++ ++real world MXL test: ++{ ++ "description": "MXL Test File", ++ "id": "5fbec3b1-1b0f-417d-9059-8b94a47197ef", ++ "tags": {}, ++ "format": "urn:x-nmos:format:video", ++ "label": "MXL Test File", ++ "parents": [], ++ "media_type": "video/v210", ++ "grain_rate": { ++ "numerator": 50, ++ "denominator": 1 ++ }, ++ "frame_width": 1920, ++ "frame_height": 1080, ++ "colorspace": "BT709", ++ "components": [ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ] ++} ++ ++idx: type str end sz ++000: objc 000 662 012 '{ ++ "description": "MXL Test File", ++ "id": "5fbec3b1-1b0f-417d-9059-8b94a47197ef", ++ "tags": {}, ++ "format": "urn:x-nmos:format:video", ++ "label": "MXL Test File", ++ "parents": [], ++ "media_type": "video/v210", ++ "grain_rate": { ++ "numerator": 50, ++ "denominator": 1 ++ }, ++ "frame_width": 1920, ++ "frame_height": 1080, ++ "colorspace": "BT709", ++ "components": [ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ] ++}' ++001: strn 005 016 001 'description' ++002: strn 020 033 000 'MXL Test File' ++003: strn 039 041 001 'id' ++004: strn 045 081 000 '5fbec3b1-1b0f-417d-9059-8b94a47197ef' ++005: strn 087 091 001 'tags' ++006: objc 094 096 000 '{}' ++007: strn 101 107 001 'format' ++008: strn 111 134 000 'urn:x-nmos:format:video' ++009: strn 140 145 001 'label' ++010: strn 149 162 000 'MXL Test File' ++011: strn 168 175 001 'parents' ++012: arry 178 180 000 '[]' ++013: strn 185 195 001 'media_type' ++014: strn 199 209 000 'video/v210' ++015: strn 215 225 001 'grain_rate' ++016: objc 228 275 002 '{ ++ "numerator": 50, ++ "denominator": 1 ++ }' ++017: strn 235 244 001 'numerator' ++018: prim 247 249 000 '50' ++019: strn 256 267 001 'denominator' ++020: prim 270 271 000 '1' ++021: strn 280 291 001 'frame_width' ++022: prim 294 298 000 '1920' ++023: strn 303 315 001 'frame_height' ++024: prim 318 322 000 '1080' ++025: strn 327 337 001 'colorspace' ++026: strn 341 346 000 'BT709' ++027: strn 352 362 001 'components' ++028: arry 365 660 003 '[ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ]' ++029: objc 371 462 004 '{ ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++030: strn 380 384 001 'name' ++031: strn 388 389 000 'Y' ++032: strn 399 404 001 'width' ++033: prim 407 411 000 '1920' ++034: strn 420 426 001 'height' ++035: prim 429 433 000 '1080' ++036: strn 442 451 001 'bit_depth' ++037: prim 454 456 000 '10' ++038: objc 468 559 004 '{ ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++039: strn 477 481 001 'name' ++040: strn 485 487 000 'Cb' ++041: strn 497 502 001 'width' ++042: prim 505 508 000 '960' ++043: strn 517 523 001 'height' ++044: prim 526 530 000 '1080' ++045: strn 539 548 001 'bit_depth' ++046: prim 551 553 000 '10' ++047: objc 565 656 004 '{ ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++048: strn 574 578 001 'name' ++049: strn 582 584 000 'Cr' ++050: strn 594 599 001 'width' ++051: prim 602 605 000 '960' ++052: strn 614 620 001 'height' ++053: prim 623 627 000 '1080' ++054: strn 636 645 001 'bit_depth' ++055: prim 648 650 000 '10' ++PASS "id" = {"5fbec3b1-1b0f-417d-9059-8b94a47197ef", SUCCESS} ++PASS "label" = {"MXL Test File", SUCCESS} ++PASS "media_type" = {"video/v210", SUCCESS} ++PASS "format" = {"urn:x-nmos:format:video", SUCCESS} ++PASS "colorspace" = {"BT709", SUCCESS} ++PASS "grain_rate.numerator" = {50, SUCCESS} ++PASS "grain_rate.denominator" = {1, SUCCESS} ++PASS "frame_width" = {1920, SUCCESS} ++PASS "frame_height" = {1080, SUCCESS} ++ ++token_capacity test: ++parse with token capacity initial=2 max=8192 ++{ ++ "description": "MXL Test File", ++ "id": "5fbec3b1-1b0f-417d-9059-8b94a47197ef", ++ "tags": {}, ++ "format": "urn:x-nmos:format:video", ++ "label": "MXL Test File", ++ "parents": [], ++ "media_type": "video/v210", ++ "grain_rate": { ++ "numerator": 50, ++ "denominator": 1 ++ }, ++ "frame_width": 1920, ++ "frame_height": 1080, ++ "colorspace": "BT709", ++ "components": [ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ] ++} ++ ++idx: type str end sz ++000: objc 000 662 012 '{ ++ "description": "MXL Test File", ++ "id": "5fbec3b1-1b0f-417d-9059-8b94a47197ef", ++ "tags": {}, ++ "format": "urn:x-nmos:format:video", ++ "label": "MXL Test File", ++ "parents": [], ++ "media_type": "video/v210", ++ "grain_rate": { ++ "numerator": 50, ++ "denominator": 1 ++ }, ++ "frame_width": 1920, ++ "frame_height": 1080, ++ "colorspace": "BT709", ++ "components": [ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ] ++}' ++001: strn 005 016 001 'description' ++002: strn 020 033 000 'MXL Test File' ++003: strn 039 041 001 'id' ++004: strn 045 081 000 '5fbec3b1-1b0f-417d-9059-8b94a47197ef' ++005: strn 087 091 001 'tags' ++006: objc 094 096 000 '{}' ++007: strn 101 107 001 'format' ++008: strn 111 134 000 'urn:x-nmos:format:video' ++009: strn 140 145 001 'label' ++010: strn 149 162 000 'MXL Test File' ++011: strn 168 175 001 'parents' ++012: arry 178 180 000 '[]' ++013: strn 185 195 001 'media_type' ++014: strn 199 209 000 'video/v210' ++015: strn 215 225 001 'grain_rate' ++016: objc 228 275 002 '{ ++ "numerator": 50, ++ "denominator": 1 ++ }' ++017: strn 235 244 001 'numerator' ++018: prim 247 249 000 '50' ++019: strn 256 267 001 'denominator' ++020: prim 270 271 000 '1' ++021: strn 280 291 001 'frame_width' ++022: prim 294 298 000 '1920' ++023: strn 303 315 001 'frame_height' ++024: prim 318 322 000 '1080' ++025: strn 327 337 001 'colorspace' ++026: strn 341 346 000 'BT709' ++027: strn 352 362 001 'components' ++028: arry 365 660 003 '[ ++ { ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }, ++ { ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ } ++ ]' ++029: objc 371 462 004 '{ ++ "name": "Y", ++ "width": 1920, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++030: strn 380 384 001 'name' ++031: strn 388 389 000 'Y' ++032: strn 399 404 001 'width' ++033: prim 407 411 000 '1920' ++034: strn 420 426 001 'height' ++035: prim 429 433 000 '1080' ++036: strn 442 451 001 'bit_depth' ++037: prim 454 456 000 '10' ++038: objc 468 559 004 '{ ++ "name": "Cb", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++039: strn 477 481 001 'name' ++040: strn 485 487 000 'Cb' ++041: strn 497 502 001 'width' ++042: prim 505 508 000 '960' ++043: strn 517 523 001 'height' ++044: prim 526 530 000 '1080' ++045: strn 539 548 001 'bit_depth' ++046: prim 551 553 000 '10' ++047: objc 565 656 004 '{ ++ "name": "Cr", ++ "width": 960, ++ "height": 1080, ++ "bit_depth": 10 ++ }' ++048: strn 574 578 001 'name' ++049: strn 582 584 000 'Cr' ++050: strn 594 599 001 'width' ++051: prim 602 605 000 '960' ++052: strn 614 620 001 'height' ++053: prim 623 627 000 '1080' ++054: strn 636 645 001 'bit_depth' ++055: prim 648 650 000 '10' ++PASS "id" = {"5fbec3b1-1b0f-417d-9059-8b94a47197ef", SUCCESS} ++PASS "label" = {"MXL Test File", SUCCESS} ++PASS "media_type" = {"video/v210", SUCCESS} ++PASS "format" = {"urn:x-nmos:format:video", SUCCESS} ++PASS "colorspace" = {"BT709", SUCCESS} ++PASS "grain_rate.numerator" = {50, SUCCESS} ++PASS "grain_rate.denominator" = {1, SUCCESS} ++PASS "frame_width" = {1920, SUCCESS} ++PASS "frame_height" = {1080, SUCCESS} ++PASS token capacity "low non zero initial capacity" ++parse with token capacity initial=1 max=2 ++PASS expected build error AVERROR(ENOMEM) (-12) ++PASS token capacity "exceed max capacity" ++parse with token capacity initial=2 max=2 ++PASS expected build error AVERROR(ENOMEM) (-12) ++PASS token capacity "initial equal to max" ++parse with token capacity initial=0 max=8192 ++PASS expected build error AVERROR(EINVAL) (-22) ++PASS token capacity "zero initial capacity" ++parse with token capacity initial=2 max=1 ++PASS expected build error AVERROR(EINVAL) (-22) ++PASS token capacity "initial greater than max" ++parse with token capacity initial=1 max=0 ++PASS expected build error AVERROR(EINVAL) (-22) ++PASS token capacity "zero max capacity" ++ ++limit tests: ++PASS MXL_JSON_MAX_NUMBER_CHARS test match ++ ++START limit test: number_over_conversion_limit ++{"too_long":11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111} ++idx: type str end sz ++000: objc 000 141 001 '{"too_long":11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}' ++001: strn 002 010 001 'too_long' ++002: prim 012 140 000 '11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' ++PASS "too_long" = {nan, MXL_JSON_UNSUPPORTED} ++PASS limit: number_over_conversion_limit ++ ++START limit test: number_under_conversion_limit ++{"not_too_long":1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111} ++idx: type str end sz ++000: objc 000 144 001 '{"not_too_long":1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111}' ++001: strn 002 014 001 'not_too_long' ++002: prim 016 143 000 '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' ++PASS "not_too_long" = {1.111111111111111e+126, SUCCESS} ++PASS limit: number_under_conversion_limit ++PASS MXL_JSON_MAX_DEPTH test match ++ ++START limit test: object_depth_over_limit ++{"accessible_a": 1,"too_deep":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}}},"inaccessible_b": 3} ++idx: type str end sz ++000: objc 000 148 003 '{"accessible_a": 1,"too_deep":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}}},"inaccessible_b": 3}' ++001: strn 002 014 001 'accessible_a' ++002: prim 017 018 000 '1' ++003: strn 020 028 001 'too_deep' ++004: objc 030 127 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}}}' ++005: strn 032 033 001 'a' ++006: objc 035 126 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}}' ++007: strn 037 038 001 'a' ++008: objc 040 125 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}' ++009: strn 042 043 001 'a' ++010: objc 045 124 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}' ++011: strn 047 048 001 'a' ++012: objc 050 123 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}' ++013: strn 052 053 001 'a' ++014: objc 055 122 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}' ++015: strn 057 058 001 'a' ++016: objc 060 121 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}' ++017: strn 062 063 001 'a' ++018: objc 065 120 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}' ++019: strn 067 068 001 'a' ++020: objc 070 119 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}' ++021: strn 072 073 001 'a' ++022: objc 075 118 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}' ++023: strn 077 078 001 'a' ++024: objc 080 117 001 '{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}' ++025: strn 082 083 001 'a' ++026: objc 085 116 001 '{"a":{"a":{"a":{"a":{"a":2}}}}}' ++027: strn 087 088 001 'a' ++028: objc 090 115 001 '{"a":{"a":{"a":{"a":2}}}}' ++029: strn 092 093 001 'a' ++030: objc 095 114 001 '{"a":{"a":{"a":2}}}' ++031: strn 097 098 001 'a' ++032: objc 100 113 001 '{"a":{"a":2}}' ++033: strn 102 103 001 'a' ++034: objc 105 112 001 '{"a":2}' ++035: strn 107 108 001 'a' ++036: prim 110 111 000 '2' ++037: strn 129 143 001 'inaccessible_b' ++038: prim 146 147 000 '3' ++PASS "accessible_a" = {1, SUCCESS} ++PASS "too_deep.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" = {nan, MXL_JSON_UNSUPPORTED} ++PASS "inaccessible_b" = {nan, MXL_JSON_ENOTFOUND} ++PASS limit: object_depth_over_limit ++ ++START limit test: object_depth_under_limit ++{"accessible_a": 1,"not_too_deep":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}},"accessible_b": 3} ++idx: type str end sz ++000: objc 000 144 003 '{"accessible_a": 1,"not_too_deep":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}},"accessible_b": 3}' ++001: strn 002 014 001 'accessible_a' ++002: prim 017 018 000 '1' ++003: strn 020 032 001 'not_too_deep' ++004: objc 034 125 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}}' ++005: strn 036 037 001 'a' ++006: objc 039 124 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}}' ++007: strn 041 042 001 'a' ++008: objc 044 123 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}}' ++009: strn 046 047 001 'a' ++010: objc 049 122 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}}' ++011: strn 051 052 001 'a' ++012: objc 054 121 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}}' ++013: strn 056 057 001 'a' ++014: objc 059 120 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}}' ++015: strn 061 062 001 'a' ++016: objc 064 119 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}}' ++017: strn 066 067 001 'a' ++018: objc 069 118 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}}' ++019: strn 071 072 001 'a' ++020: objc 074 117 001 '{"a":{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}}' ++021: strn 076 077 001 'a' ++022: objc 079 116 001 '{"a":{"a":{"a":{"a":{"a":{"a":2}}}}}}' ++023: strn 081 082 001 'a' ++024: objc 084 115 001 '{"a":{"a":{"a":{"a":{"a":2}}}}}' ++025: strn 086 087 001 'a' ++026: objc 089 114 001 '{"a":{"a":{"a":{"a":2}}}}' ++027: strn 091 092 001 'a' ++028: objc 094 113 001 '{"a":{"a":{"a":2}}}' ++029: strn 096 097 001 'a' ++030: objc 099 112 001 '{"a":{"a":2}}' ++031: strn 101 102 001 'a' ++032: objc 104 111 001 '{"a":2}' ++033: strn 106 107 001 'a' ++034: prim 109 110 000 '2' ++035: strn 127 139 001 'accessible_b' ++036: prim 142 143 000 '3' ++PASS "accessible_a" = {1, SUCCESS} ++PASS "not_too_deep.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" = {2, SUCCESS} ++PASS "too_deep.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" = {nan, MXL_JSON_UNSUPPORTED} ++PASS "accessible_b" = {3, SUCCESS} ++PASS limit: object_depth_under_limit ++ ++PASS +diff --git a/tests/ref/fate/mxl-loc b/tests/ref/fate/mxl-loc +new file mode 100644 +index 0000000000..43567bcbe5 +--- /dev/null ++++ b/tests/ref/fate/mxl-loc +@@ -0,0 +1,564 @@ ++testing mxl_loc: mxl resource location parser ++ ++==== test case: abs_filesystem_path ++locator: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: abs_filesystem_path PASS ++==== test case: abs_filesystem_path_root ++locator: "/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: abs_filesystem_path_root PASS ++==== test case: rel_filesystem_path ++locator: "mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: rel_filesystem_path PASS ++==== test case: rel_filesystem_path_none ++locator: "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: rel_filesystem_path_none PASS ++==== test case: rel_filesystem_path_dot ++locator: "./71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: rel_filesystem_path_dot PASS ++==== test case: rel_filesystem_path_dot_dot ++locator: "../71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: ".." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: ".." ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: rel_filesystem_path_dot_dot PASS ++==== test case: abs_filesystem_no_flow ++locator: "/dev/shm/mxl" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_no_flow PASS ++==== test case: abs_filesystem_no_flow_root ++locator: "/" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_no_flow_root PASS ++==== test case: rel_filesystem_no_flow ++locator: "rel/shm/mxl" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "rel/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "rel/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: rel_filesystem_no_flow PASS ++==== test case: rel_filesystem_no_flow_pwd ++locator: "." ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "." ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: rel_filesystem_no_flow_pwd PASS ++==== test case: rel_filesystem_path_trailing_slash ++locator: "mxl/" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "mxl/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "mxl/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: rel_filesystem_path_trailing_slash PASS ++==== test case: rel_filesystem_path_double_sep ++locator: "mxl//71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "mxl/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "mxl/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: rel_filesystem_path_double_sep PASS ++==== test case: abs_filesystem_path_zero_flow ++locator: "/tests/data/tmp/mxl" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/tests/data/tmp/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/tests/data/tmp/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_zero_flow PASS ++==== test case: abs_filesystem_path_wrong_ext ++locator: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.txt" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.txt" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.txt" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_wrong_ext PASS ++==== test case: abs_filesystem_path_trailing_ext_junk ++locator: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow.tmp" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow.tmp" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow.tmp" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_trailing_ext_junk PASS ++==== test case: abs_filesystem_path_no_ext ++locator: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_no_ext PASS ++==== test case: abs_filesystem_path_upcase_id ++locator: "/dev/shm/mxl/71F000DC-2578-4A06-B4EF-3E0E2B1E46E1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71F000DC-2578-4A06-B4EF-3E0E2B1E46E1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71F000DC-2578-4A06-B4EF-3E0E2B1E46E1" ++ ] ++} ++==== test case: abs_filesystem_path_upcase_id PASS ++==== test case: abs_filesystem_path_double_sep_a ++locator: "//dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "//dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "//dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: abs_filesystem_path_double_sep_a PASS ++==== test case: abs_filesystem_path_double_sep_b ++locator: "/dev/shm//mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm//mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm//mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: abs_filesystem_path_double_sep_b PASS ++==== test case: abs_filesystem_path_double_sep_c ++locator: "/dev/shm/mxl//71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: abs_filesystem_path_double_sep_c PASS ++==== test case: abs_filesystem_directory_named_flow ++locator: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow/" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl/71f000dc-2578-4a06-b4ef-3e0e2b1e46e1.mxl-flow/" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_directory_named_flow PASS ++==== test case: uri_zero_flow ++locator: "mxl:///dev/shm/mxl" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: uri_zero_flow PASS ++==== test case: uri_one_flow ++locator: "mxl:///dev/shm/mxl?id=71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 1 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ ] ++} ++==== test case: uri_one_flow PASS ++==== test case: uri_two_flow ++locator: "mxl:///dev/shm/mxl?id=71f000dc-2578-4a06-b4ef-3e0e2b1e46e1&id=ee43ad87-1ff5-43f5-a895-9cedbcc8884a" ++expected return code: 0 ++actual return code: 0 ++expected locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 2 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ "ee43ad87-1ff5-43f5-a895-9cedbcc8884a" ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "/dev/shm/mxl" ++ nb_flow_ids: 2 ++ flow_ids: [ ++ "71f000dc-2578-4a06-b4ef-3e0e2b1e46e1" ++ "ee43ad87-1ff5-43f5-a895-9cedbcc8884a" ++ ] ++} ++==== test case: uri_two_flow PASS ++==== test case: abs_filesystem_path_bad_id ++locator: "/dev/shm/mxl/bad_id.mxl-flow" ++expected return code: -22 ++actual return code: -22 ++expected locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_bad_id PASS ++==== test case: abs_filesystem_path_missing_id ++locator: "/dev/shm/mxl/.mxl-flow" ++expected return code: -22 ++actual return code: -22 ++expected locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: abs_filesystem_path_missing_id PASS ++==== test case: null_locator ++locator: "(null)" ++expected return code: -22 ++actual return code: -22 ++expected locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: null_locator PASS ++==== test case: empty_locator ++locator: "" ++expected return code: -2 ++actual return code: -2 ++expected locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++actual locator: ++mxl_loc { ++ domain_path: "(null)" ++ nb_flow_ids: 0 ++ flow_ids: [ ++ ] ++} ++==== test case: empty_locator PASS ++ ++PASS +diff --git a/tests/ref/fate/mxl-uri b/tests/ref/fate/mxl-uri +new file mode 100644 +index 0000000000..490a96e2c2 +--- /dev/null ++++ b/tests/ref/fate/mxl-uri +@@ -0,0 +1,1138 @@ ++testing mxl_uri: mxl URI parser ++ ++==== test case: zero_id ++URI: "mxl://host1.local/domain/path" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++==== test case: zero_id PASS ++==== test case: one_id ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: one_id PASS ++==== test case: two_id ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: two_id PASS ++==== test case: three_id ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc&id=2b528ffe-c94c-454f-984a-35c98c717006" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (3): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++ [2] "2b528ffe-c94c-454f-984a-35c98c717006" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (3): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++ [2] "2b528ffe-c94c-454f-984a-35c98c717006" ++} ++==== test case: three_id PASS ++==== test case: two_id_upcase ++URI: "MXL://HOST1.LOCAL/DOMAIN/PATH?id=3645F1E2-90FC-49CB-AF9C-236C2AC124CD&id=876FC244-9C99-48A8-A461-927E43A05BDC" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "HOST1.LOCAL" ++ port: -1 ++ domain: "/DOMAIN/PATH" ++ flow_ids (2): ++ [0] "3645F1E2-90FC-49CB-AF9C-236C2AC124CD" ++ [1] "876FC244-9C99-48A8-A461-927E43A05BDC" ++} ++actual URI: ++mxl_uri { ++ host: "HOST1.LOCAL" ++ port: -1 ++ domain: "/DOMAIN/PATH" ++ flow_ids (2): ++ [0] "3645F1E2-90FC-49CB-AF9C-236C2AC124CD" ++ [1] "876FC244-9C99-48A8-A461-927E43A05BDC" ++} ++==== test case: two_id_upcase PASS ++==== test case: two_id_upcase_key ++URI: "mxl://host1.local/domain/path?ID=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: two_id_upcase_key PASS ++==== test case: unknown_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&unknown=unknown&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: unknown_query_param PASS ++==== test case: unknown_flag_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&empty&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: unknown_flag_query_param PASS ++==== test case: ipv4 ++URI: "mxl://10.1.2.3:5000/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "10.1.2.3" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1" ++ [1] "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++} ++actual URI: ++mxl_uri { ++ host: "10.1.2.3" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1" ++ [1] "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++} ++==== test case: ipv4 PASS ++==== test case: ipv6 ++URI: "mxl://[2001:db8::2]/domain/path?id=1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92&id=3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "2001:db8::2" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92" ++ [1] "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++} ++actual URI: ++mxl_uri { ++ host: "2001:db8::2" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92" ++ [1] "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++} ++==== test case: ipv6 PASS ++==== test case: ipv6_with_port ++URI: "mxl://[2001:db8::2]:5000/domain/path?id=1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92&id=3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "2001:db8::2" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92" ++ [1] "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++} ++actual URI: ++mxl_uri { ++ host: "2001:db8::2" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "1e9c4c74-6c44-4f7d-b8a3-3e4f9a4e8a92" ++ [1] "3fbcf2a1-2e8a-4a4d-9c8f-0d7e4a6b7c51" ++} ++==== test case: ipv6_with_port PASS ++==== test case: valid_negative_one_port ++URI: "mxl://10.1.2.3:-1/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "10.1.2.3" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1" ++ [1] "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++} ++actual URI: ++mxl_uri { ++ host: "10.1.2.3" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1" ++ [1] "5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++} ++==== test case: valid_negative_one_port PASS ++==== test case: only_unknown_query_param ++URI: "mxl://host1.local/domain/path?foo=bar&baz=qux" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++==== test case: only_unknown_query_param PASS ++==== test case: local_domain_triple_slash ++URI: "mxl:///domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: local_domain_triple_slash PASS ++==== test case: local_domain_single_slash ++URI: "mxl:/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: local_domain_single_slash PASS ++==== test case: zero_port ++URI: "mxl://10.1.2.3:0/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: zero_port PASS ++==== test case: fragment_after_query ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd#fragment" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: fragment_after_query PASS ++==== test case: valid_at_range_limit_port ++URI: "mxl://host1.local:65535/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: 65535 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: 65535 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: valid_at_range_limit_port PASS ++==== test case: invalid_out_of_range_port ++URI: "mxl://10.1.2.3:65536/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: invalid_out_of_range_port PASS ++==== test case: empty_query ++URI: "mxl://host1.local/domain/path?" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++==== test case: empty_query PASS ++==== test case: leading_amp_query_param ++URI: "mxl://host1.local/domain/path?&id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: leading_amp_query_param PASS ++==== test case: double_separator_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: double_separator_query_param PASS ++==== test case: percent_encoded_in_other_query_value ++URI: "mxl://host1.local/domain/path?other=%26extra&id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: percent_encoded_in_other_query_value PASS ++==== test case: percent_encoded_key ++URI: "mxl://host1.local/domain/path?%69d=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (0): ++} ++==== test case: percent_encoded_key PASS ++==== test case: trailing_amp_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: trailing_amp_query_param PASS ++==== test case: local_domain_looks_like_port_but_is_not ++URI: "mxl:///domain/path:5000?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "/domain/path:5000" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "/domain/path:5000" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: local_domain_looks_like_port_but_is_not PASS ++==== test case: empty_host_with_port ++URI: "mxl://:5000/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "" ++ port: 5000 ++ domain: "/domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: empty_host_with_port PASS ++==== test case: userinfo_with_empty_host ++URI: "mxl://user@/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: userinfo_with_empty_host PASS ++==== test case: double_leading_slash_path ++URI: "mxl:////domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "//domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++actual URI: ++mxl_uri { ++ host: "" ++ port: -1 ++ domain: "//domain/path" ++ flow_ids (1): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++} ++==== test case: double_leading_slash_path PASS ++==== test case: query_without_path ++URI: "mxl://?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: query_without_path PASS ++==== test case: empty_ipv6_host ++URI: "mxl://[]/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_ipv6_host PASS ++==== test case: empty_ipv6_host_with_port ++URI: "mxl://[]:5000/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_ipv6_host_with_port PASS ++==== test case: empty_domain_path ++URI: "mxl://host1.local/?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_domain_path PASS ++==== test case: empty_domain_path_no_query ++URI: "mxl://host1.local" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_domain_path_no_query PASS ++==== test case: empty_domain_path_no_query_trailing_slash ++URI: "mxl://host1.local/" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_domain_path_no_query_trailing_slash PASS ++==== test case: bad_scheme ++URI: "ftp://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: bad_scheme PASS ++==== test case: no_scheme ++URI: "/host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: no_scheme PASS ++==== test case: null_uri ++URI: "(null)" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: null_uri PASS ++==== test case: empty_uri ++URI: "" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_uri PASS ++==== test case: equal_max_scheme ++URI: "aaaa:/host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -36 ++actual return code: -36 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: equal_max_scheme PASS ++==== test case: greater_max_scheme ++URI: "aaaaa:/host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -36 ++actual return code: -36 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: greater_max_scheme PASS ++==== test case: empty_middle_id_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_middle_id_query_param PASS ++==== test case: empty_end_id_query_param ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc&id=" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: empty_end_id_query_param PASS ++==== test case: bad_separator_query_param ++URI: "mxl://host1.local/domain/path?id=id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: bad_separator_query_param PASS ++==== test case: bad_uuid_query_param ++URI: "mxl://host1.local/domain/path?id=X645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: bad_uuid_query_param PASS ++==== test case: invalid_negative_port ++URI: "mxl://host1.local:-2/domain/path?id=645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: invalid_negative_port PASS ++==== test case: invalid_empty_port ++URI: "mxl://10.1.2.3:/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: invalid_empty_port PASS ++==== test case: invalid_non_integer_port ++URI: "mxl://10.1.2.3:abc/domain/path?id=8c8e8a37-4a5b-4e0a-9f0b-2e3e71e3f5e1&id=5d9d1a64-7a77-4b54-8e32-6e6e8b0b9a44" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: invalid_non_integer_port PASS ++==== test case: invalid_no_domain_path ++URI: "mxl://host1.local?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: invalid_no_domain_path PASS ++==== test case: malformed_ipv6_bracket ++URI: "mxl://[2001:db8::2/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: malformed_ipv6_bracket PASS ++==== test case: percent_encoded_in_uuid_query_value ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd%26extra" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: percent_encoded_in_uuid_query_value PASS ++==== test case: userinfo_in_authority ++URI: "mxl://user@host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: userinfo_in_authority PASS ++==== test case: userpass_in_authority ++URI: "mxl://user:pass@host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: userpass_in_authority PASS ++==== test case: multiple_at_userinfo ++URI: "mxl://a@b@host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++expected return code: -22 ++actual return code: -22 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: multiple_at_userinfo PASS ++==== test case: equal_max_len_url ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc&junk=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ++expected return code: 0 ++actual return code: 0 ++expected URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++actual URI: ++mxl_uri { ++ host: "host1.local" ++ port: -1 ++ domain: "/domain/path" ++ flow_ids (2): ++ [0] "3645f1e2-90fc-49cb-af9c-236c2ac124cd" ++ [1] "876fc244-9c99-48a8-a461-927e43a05bdc" ++} ++==== test case: equal_max_len_url PASS ++==== test case: greater_max_len_url ++URI: "mxl://host1.local/domain/path?id=3645f1e2-90fc-49cb-af9c-236c2ac124cd&id=876fc244-9c99-48a8-a461-927e43a05bdc&junk=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ++expected return code: -36 ++actual return code: -36 ++expected URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++actual URI: ++mxl_uri { ++ host: "(null)" ++ port: -1 ++ domain: "(null)" ++ flow_ids (0): ++} ++==== test case: greater_max_len_url PASS ++ ++PASS +diff --git a/tests/ref/fate/mxl-video-encdec b/tests/ref/fate/mxl-video-encdec +new file mode 100644 +index 0000000000..f266e1ff74 +--- /dev/null ++++ b/tests/ref/fate/mxl-video-encdec +@@ -0,0 +1,15 @@ ++#format: frame checksums ++#version: 2 ++#hash: MD5 ++#software: Lavf62.6.101 ++#tb 0: 1/50 ++#media_type 0: video ++#codec_id 0: rawvideo ++#dimensions 0: 1920x1080 ++#sar 0: 1/1 ++#stream#, dts, pts, duration, size, hash ++0, 0, 0, 1, 8294400, 687ba6aecec048f446ee3cbe03f611b3 ++0, 1, 1, 1, 8294400, fa8087d706277bffe0181b862c9e8252 ++0, 2, 2, 1, 8294400, 19190a35fe54888aa56367a6672cb02d ++0, 3, 3, 1, 8294400, 13b4e70275b3e18080ea2411a1c7e47c ++0, 4, 4, 1, 8294400, 34c7f0e53d40f8da58a1d48510d425a8 +diff --git a/tests/ref/fate/mxl-video-probe b/tests/ref/fate/mxl-video-probe +new file mode 100644 +index 0000000000..5c38dcfbe8 +--- /dev/null ++++ b/tests/ref/fate/mxl-video-probe +@@ -0,0 +1,10 @@ ++Input #0, mxl, from 'mxl:///path/to/domain?id=717f834b-4224-4c9b-8a64-ecb7726803b8': ++ Duration: N/A, bitrate: N/A ++ Stream #0:0: Video: v210 (v210 / 0x30313276), yuv422p10le(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 50 fps, 50 tbr, 50 tbn ++ Metadata: ++ mxl_id : 717f834b-4224-4c9b-8a64-ecb7726803b8 ++ mxl_description : FFmpeg video stream ++ mxl_label : FFmpeg video stream ++ mxl_format : urn:x-nmos:format:video ++ mxl_media_type : video/v210 ++ mxl_colorspace : BT709 +-- +2.43.0 + diff --git a/deps/ffmpeg-patches/Dockerfile.mkpatch b/deps/ffmpeg-patches/Dockerfile.mkpatch new file mode 100644 index 00000000..a1499fce --- /dev/null +++ b/deps/ffmpeg-patches/Dockerfile.mkpatch @@ -0,0 +1,32 @@ +# Build the MXL "0008" patch on top of the existing FFmpeg patch stack. +# +# Usage from the repository root: +# +# docker build -f deps/ffmpeg-patches/Dockerfile.mkpatch \ +# -t avplumber-mxl-mkpatch:local deps/ffmpeg-patches +# docker run --rm \ +# -v "$PWD/deps/ffmpeg-patches:/out" \ +# -v "$PWD/deps/ffmpeg-patches:/patches:ro" \ +# avplumber-mxl-mkpatch:local +# +# On success this writes: +# deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch +# deps/ffmpeg-patches/expected-tree.txt (paste into README.md) +# +# On cherry-pick conflicts the container exits with instructions; drop +# into it with `-it --entrypoint bash` on the same image to finish by hand. + +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* + +COPY mkpatch-0008-mxl.sh /usr/local/bin/mkpatch-0008-mxl +COPY mkpatch-finish.sh /usr/local/bin/mkpatch-finish +RUN chmod +x /usr/local/bin/mkpatch-0008-mxl /usr/local/bin/mkpatch-finish + +ENTRYPOINT ["/usr/local/bin/mkpatch-0008-mxl"] diff --git a/deps/ffmpeg-patches/README.md b/deps/ffmpeg-patches/README.md index b4633f24..993ec283 100644 --- a/deps/ffmpeg-patches/README.md +++ b/deps/ffmpeg-patches/README.md @@ -8,11 +8,12 @@ composition and media-input workflows. - Upstream repository: `https://github.com/FFmpeg/FFmpeg` - Upstream tag: `n7.1.5` - Upstream commit: `3a0867c2bfda4a4d4309ca1a8cbdc6175e67f587` -- Expected patched tree: `52361f7251069ef74fbb41460e6e1b65d6f9947c` +- Expected patched tree: `f4eebb6a255d1f4b76eb795885102d1cfea8cd08` + (`52361f7251069ef74fbb41460e6e1b65d6f9947c` for the 0001-0007 subset) ## Series -The seven patches are ordered by filename and grouped by feature rather than by +The eight patches are ordered by filename and grouped by feature rather than by the chronology of incomplete ports and follow-up fixes: 1. `0001-swscale-aarch64-argb-yuva420p.patch` — AArch64 fast color conversion. @@ -25,9 +26,60 @@ the chronology of incomplete ports and follow-up fixes: handling. 6. `0006-avdevice-v4l2-compat.patch` — V4L2 timestamp compatibility. 7. `0007-avdevice-ndi-v5.patch` — NDI v5 device registration and documentation. +8. `0008-avformat-libmxl-demuxer-muxer.patch` — [MXL](https://github.com/dmf-mxl/mxl) + demuxer, muxer, URI parser, monitoring/diag interface, and + `--enable-libmxl` configure glue. Squashed from 32 upstream commits on + `cbcrc/FFmpeg` branch `dmf-mxl/master` (pinned at `5c5d593`); the MXL + SDK is pinned to `v1.1.0-beta-1`. Consumed by `demos/mxl/Dockerfile`. Each patch message lists the original exported FFmpeg commits it replaces. +### Regenerating `0008-avformat-libmxl-demuxer-muxer.patch` + +Producing the patch (Docker only — works on Linux and macOS hosts): + +```bash +docker build -f deps/ffmpeg-patches/Dockerfile.mkpatch \ + -t avplumber-mxl-mkpatch:local deps/ffmpeg-patches +docker run --rm \ + -v "$PWD/deps/ffmpeg-patches:/out" \ + -v "$PWD/deps/ffmpeg-patches:/patches:ro" \ + avplumber-mxl-mkpatch:local +``` + +On success the container writes: + +* `deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch` +* `deps/ffmpeg-patches/expected-tree.txt` — paste this hash into the + "Expected patched tree" line above. + +Then run the verifier to confirm the tree matches: + +```bash +deps/ffmpeg-patches/verify.sh /path/to/any/FFmpeg +``` + +If the cherry-pick hits conflicts (usually `configure`, +`libavformat/allformats.c`, or `libavformat/Makefile`), the container +exits with instructions. Re-run it with `--entrypoint bash` to finish +by hand: + +```bash +docker run --rm -it \ + -v "$PWD/deps/ffmpeg-patches:/out" \ + -v "$PWD/deps/ffmpeg-patches:/patches:ro" \ + --entrypoint bash avplumber-mxl-mkpatch:local +# inside: /usr/local/bin/mkpatch-0008-mxl +# fix conflicts under /tmp/ffmpeg-mxl-build/ffmpeg, then: +# git cherry-pick --continue +# /usr/local/bin/mkpatch-finish +``` + +Once landed, remove this "Pending" subsection and add the patch to the +numbered list. The `demos/mxl/Dockerfile` startup check +(`ffmpeg -demuxers | grep mxl`) will then pass and the demo becomes +runnable end-to-end. + The old FFmpeg `af_whisper` port is intentionally absent. Speech-to-text belongs in an AVPlumber node and is not part of this FFmpeg variant. diff --git a/deps/ffmpeg-patches/mkpatch-0008-mxl.sh b/deps/ffmpeg-patches/mkpatch-0008-mxl.sh new file mode 100755 index 00000000..92d0df28 --- /dev/null +++ b/deps/ffmpeg-patches/mkpatch-0008-mxl.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# Generate deps/ffmpeg-patches/0008-avformat-libmxl-demuxer-muxer.patch +# by rebasing the MXL commits from dmf-mxl/FFmpeg onto n7.1.5 on top of +# the existing patch stack. +# +# Runs inside a plain Linux container (see mkpatch-0008-mxl.docker.sh). +# All state lives in /tmp; the resulting patch is written to /out. +# +# The MXL commit set is identified by two heuristics: +# 1. commits reachable from dmf-mxl/master but not from FFmpeg n7.1.5 +# that touch libavformat/mxl*, +# 2. plus any of those commits whose subject line contains "mxl" +# (case-insensitive) — catches the configure/allformats/Makefile +# glue commits that don't add mxl* files themselves. +# +# The cherry-picks are then squashed into a single provenance-preserving +# patch so verify.sh keeps its "one feature per numbered patch" invariant. + +set -euo pipefail + +FFMPEG_TAG="${FFMPEG_TAG:-n7.1.5}" +# cbcrc's guidance-for-building-ffmpeg-with-mxl (scripts/get-src.sh) +# hosts the MXL FFmpeg patches at cbcrc/FFmpeg, branch dmf-mxl/master. +MXL_REMOTE_URL="${MXL_REMOTE_URL:-https://github.com/cbcrc/FFmpeg.git}" +MXL_REMOTE_REF="${MXL_REMOTE_REF:-dmf-mxl/master}" +MXL_PIN="${MXL_PIN:-5c5d593}" +OUT_DIR="${OUT_DIR:-/out}" +PATCH_DIR="${PATCH_DIR:-/patches}" +WORK_DIR="${WORK_DIR:-/tmp/ffmpeg-mxl-build}" + +log() { printf '[mkpatch] %s\n' "$*"; } + +test -d "$PATCH_DIR" || { echo "expected patch input dir at $PATCH_DIR" >&2; exit 2; } +test -d "$OUT_DIR" || { echo "expected patch output dir at $OUT_DIR" >&2; exit 2; } + +rm -rf "$WORK_DIR" +mkdir -p "$WORK_DIR" + +log "cloning FFmpeg $FFMPEG_TAG" +git clone --quiet --branch "$FFMPEG_TAG" --depth 1 \ + https://github.com/FFmpeg/FFmpeg.git "$WORK_DIR/ffmpeg" + +cd "$WORK_DIR/ffmpeg" +git config user.name "avplumber mxl builder" +git config user.email "avplumber-mxl@local" + +log "applying existing 0001-0007 stack" +# shellcheck disable=SC2046 +git am $(ls "$PATCH_DIR"/0[0-9][0-9][0-9]-*.patch | sort) + +BASE_AFTER_STACK="$(git rev-parse HEAD)" + +log "fetching MXL FFmpeg fork ($MXL_REMOTE_URL @ $MXL_REMOTE_REF, pin=$MXL_PIN)" +git remote add mxl-fork "$MXL_REMOTE_URL" +# The fork's dmf-mxl/master branch carries the MXL commits; it can be +# many commits above the base so no --depth here. +git fetch --no-tags --quiet mxl-fork "$MXL_REMOTE_REF:refs/remotes/mxl-fork/master" +git rev-parse --verify "$MXL_PIN^{commit}" >/dev/null 2>&1 || \ + git fetch --no-tags --quiet mxl-fork "$MXL_PIN" + +MXL_TIP="$MXL_PIN" +MERGE_BASE="$(git merge-base "$MXL_TIP" "$FFMPEG_TAG" || true)" +if [ -z "$MERGE_BASE" ]; then + log "no shared history with $MXL_TIP — falling back to $FFMPEG_TAG as range base" + MERGE_BASE="$FFMPEG_TAG" +fi +log "range base: $MERGE_BASE" + +log "collecting MXL commits" +mapfile -t MXL_COMMITS < <( + { + git log --reverse --format=%H "$MERGE_BASE".."$MXL_TIP" -- 'libavformat/mxl*' + git log --reverse --format='%H %s' "$MERGE_BASE".."$MXL_TIP" \ + | awk 'tolower($0) ~ /mxl/ { print $1 }' + } | awk '!seen[$0]++' +) + +if [ "${#MXL_COMMITS[@]}" -eq 0 ]; then + echo "no MXL commits found in $MXL_REMOTE_REF; nothing to do" >&2 + exit 3 +fi + +log "picking ${#MXL_COMMITS[@]} commit(s):" +for c in "${MXL_COMMITS[@]}"; do + printf ' %s %s\n' "$c" "$(git log -1 --format=%s "$c")" +done + +log "cherry-picking onto n7.1.5 + existing stack (-X theirs, auto-resolve)" +# --keep-redundant-commits lets pure-metadata cherry-picks succeed. +# -X theirs auto-resolves content-conflicts in additive files. +# For rename/delete cases -X can't handle, we fall through to a manual +# loop: prefer the "theirs" version (the commit being picked), and if +# the path was removed there, remove it. Since we squash to one patch +# anyway, the end state is what matters. + +auto_resolve_conflicts() { + local pending + pending="$(git diff --name-only --diff-filter=U)" + if [ -z "$pending" ]; then + return 1 + fi + while IFS= read -r path; do + [ -z "$path" ] && continue + if git checkout --theirs -- "$path" 2>/dev/null; then + git add -- "$path" + else + git rm -f -- "$path" >/dev/null + fi + done <<<"$pending" + # Also handle unmerged entries that only appear in --name-only + # via `git status --porcelain=v1` ("DU"/"UD"/"AA"/"DD"). + while IFS= read -r line; do + local code path2 + code="${line:0:2}" + path2="${line:3}" + case "$code" in + DU|UD|DD|AU|UA|AA|UU) + if git checkout --theirs -- "$path2" 2>/dev/null; then + git add -- "$path2" + else + git rm -f -- "$path2" >/dev/null 2>&1 || true + fi + ;; + esac + done < <(git status --porcelain=v1) + return 0 +} + +set +e +git cherry-pick --keep-redundant-commits \ + --strategy=recursive -X theirs -x "${MXL_COMMITS[@]}" +rc=$? +while [ "$rc" -ne 0 ]; do + if ! auto_resolve_conflicts; then + log "cherry-pick failed with no unresolved paths — aborting" + exit 4 + fi + if git diff --cached --quiet; then + log "empty commit after resolve — skipping" + git cherry-pick --skip + else + GIT_EDITOR=true git cherry-pick --continue + fi + rc=$? +done +set -e + +exec bash /usr/local/bin/mkpatch-finish "$BASE_AFTER_STACK" diff --git a/deps/ffmpeg-patches/mkpatch-finish.sh b/deps/ffmpeg-patches/mkpatch-finish.sh new file mode 100755 index 00000000..caf49674 --- /dev/null +++ b/deps/ffmpeg-patches/mkpatch-finish.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Squash the MXL cherry-picks into a single provenance-preserving patch +# and write 0008-avformat-libmxl-demuxer-muxer.patch to /out. +# +# $1 = commit hash of "base after 0001-0007 applied". Passed by mkpatch-0008-mxl.sh +# after a successful cherry-pick, or by the user manually after resolving +# conflicts and running `git cherry-pick --continue`. + +set -eu +set -o pipefail +FFMPEG_TAG="${FFMPEG_TAG:-n7.1.5}" + +BASE_AFTER_STACK="${1:-}" +if [ -z "$BASE_AFTER_STACK" ]; then + # If invoked manually, recover the base by walking back over MXL commits. + BASE_AFTER_STACK="$(git log --format=%H --grep='cherry picked from' --invert-grep -1)" +fi + +OUT_DIR="${OUT_DIR:-/out}" +OUT_FILE="$OUT_DIR/0008-avformat-libmxl-demuxer-muxer.patch" + +log() { printf '[mkpatch] %s\n' "$*"; } + +log "collecting authorship from cherry-picked commits" +AUTHOR_TRAILERS="$( + git log --format='Co-authored-by: %an <%ae>' "$BASE_AFTER_STACK"..HEAD \ + | awk '!seen[$0]++' +)" +ORIGINAL_COMMITS="$( + git log --format=' * %h %s' "$BASE_AFTER_STACK"..HEAD +)" + +log "squashing cherry-picks to a single commit" +git reset --soft "$BASE_AFTER_STACK" + +MSG_FILE="$(mktemp)" +{ + printf 'avformat/mxl: add libmxl demuxer and muxer\n\n' + printf 'Adds the MXL demuxer, muxer, and --enable-libmxl configure glue.\n' + printf 'Cherry-picked from https://github.com/cbcrc/FFmpeg (branch\n' + printf 'dmf-mxl/master) and rebased onto %s on top of the existing\n' "$FFMPEG_TAG" + printf 'avplumber patch stack (0001-0007).\n\n' + printf 'Original commits:\n%s\n\n' "$ORIGINAL_COMMITS" + [ -n "$AUTHOR_TRAILERS" ] && printf '%s\n' "$AUTHOR_TRAILERS" +} >"$MSG_FILE" + +GIT_COMMITTER_NAME="avplumber mxl builder" \ +GIT_COMMITTER_EMAIL="avplumber-mxl@local" \ +git commit --quiet -F "$MSG_FILE" \ + --author="avplumber mxl builder " +rm -f "$MSG_FILE" + +log "writing patch to $OUT_FILE" +git format-patch -1 --stdout HEAD >"$OUT_FILE" + +NEW_TREE="$(git rev-parse HEAD^{tree})" +log "new expected patched tree: $NEW_TREE" +printf '%s\n' "$NEW_TREE" >"$OUT_DIR/expected-tree.txt" + +log "done." +log "next: update 'Expected patched tree' in deps/ffmpeg-patches/README.md" +log " to: $NEW_TREE" +log "and rerun deps/ffmpeg-patches/verify.sh against a fresh $FFMPEG_TAG checkout." diff --git a/deps/ffmpeg-patches/verify.sh b/deps/ffmpeg-patches/verify.sh index f0913bb3..a7fd2f8d 100755 --- a/deps/ffmpeg-patches/verify.sh +++ b/deps/ffmpeg-patches/verify.sh @@ -2,8 +2,8 @@ set -euo pipefail base_commit=3a0867c2bfda4a4d4309ca1a8cbdc6175e67f587 -expected_tree=52361f7251069ef74fbb41460e6e1b65d6f9947c -expected_patch_count=7 +expected_tree=f4eebb6a255d1f4b76eb795885102d1cfea8cd08 +expected_patch_count=8 if [[ $# -ne 1 ]]; then echo "usage: $0 /path/to/FFmpeg" >&2 From 08fcf3e1bc0bdcb8eb234077f3fc02e0a294cbc6 Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Wed, 9 Sep 2026 15:06:57 +0200 Subject: [PATCH 2/5] Add MXL round-trip demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds demos/mxl/, a demo that publishes into and reads back from an MXL shared-memory domain through avplumber's generic input/output nodes (format="mxl"), with no MXL-specific node types. The Dockerfile mirrors demos/mixer/Dockerfile: bases on nvidia/cuda:11.7.1-devel-ubuntu22.04, installs gcc-13 (required by the MXL SDK) alongside the distro gcc-11, brings up vcpkg + Rust 1.88.0, builds libmxl v1.1.0-beta-1, applies the full 8-patch stack, and configures FFmpeg with the CUDA composition suite plus --enable-libmxl --enable-demuxer=mxl --enable-muxer=mxl --enable-protocol=mxl. avplumber's python_module is built with HAVE_CUDA=1 HAVE_NVCC=1 so the same image also runs mixer-class workloads on real NVIDIA hosts. The demo runs without a GPU too — CUDA init fails silently, MXL round-trip works. mxl_demo.py builds two graphs in one process: writer: lavfi testsrc -> decode -> rescale(yuv422p10le) -> AssumeVideoFormat -> enc(v210) -> mux -> Output(format="mxl") reader: Input(format="mxl") -> demux -> dec(v210) -> rescale(yuv420p) -> AssumeVideoFormat -> enc(mpeg4) -> mux -> fragmented mp4 Verified end-to-end on colima (aarch64, no GPU): the reader produces 28k+ mpeg4 packets of the testsrc pattern in a 10-second run. The fragmented-mp4 output stays playable on SIGINT. Known gaps (documented in demos/mxl/README.md): * options={"blocking":"1"} does not currently reach the MXL demuxer's private AVOptions through avplumber's input node; worked around with auto_restart:"group" on the reader input. * Reader runs at wall-clock max — a RealtimeVideoFrame node between decode and encode would cap it to the source frame rate. Co-Authored-By: Claude Opus 4.7 --- demos/README.md | 3 + demos/mxl/Dockerfile | 218 +++++++++++++++++++++++++ demos/mxl/README.md | 123 ++++++++++++++ demos/mxl/mxl_demo.py | 299 ++++++++++++++++++++++++++++++++++ demos/mxl/requirements.txt | 2 + demos/mxl/test-media/.gitkeep | 0 6 files changed, 645 insertions(+) create mode 100644 demos/mxl/Dockerfile create mode 100644 demos/mxl/README.md create mode 100644 demos/mxl/mxl_demo.py create mode 100644 demos/mxl/requirements.txt create mode 100644 demos/mxl/test-media/.gitkeep diff --git a/demos/README.md b/demos/README.md index 80af485d..beead22a 100644 --- a/demos/README.md +++ b/demos/README.md @@ -89,4 +89,7 @@ docker compose --env-file demos/dmabuf-browser/.env.example \ a bundled animated page, Electron, GPU capture, Janus, and browser preview. - [CUDA overlay validation](cuda-overlay/README.md#run): run `./demos/cuda-overlay/run.sh` to build, generate fixtures, and compare results. +- [MXL round-trip](mxl/README.md): publish a file into an MXL shared-memory + domain and read it back. Linux-only, no GPU required, but depends on the + pending `0008-avformat-libmxl-demuxer-muxer.patch`. diff --git a/demos/mxl/Dockerfile b/demos/mxl/Dockerfile new file mode 100644 index 00000000..2ff2a7d7 --- /dev/null +++ b/demos/mxl/Dockerfile @@ -0,0 +1,218 @@ +# MXL demo build image (NVIDIA-enabled). +# +# Mirrors ../mixer/Dockerfile: builds patched FFmpeg on top of an +# NVIDIA CUDA base image and produces avplumber's python_module with +# CUDA support. Adds libmxl SDK and the 0008 MXL patch so the same +# image can also run the MXL demo. Once this proves stable it can +# fold into demos/mixer/Dockerfile itself. + +FROM nvidia/cuda:11.7.1-devel-ubuntu22.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG AVPLUMBER_REVISION=workspace +ARG FFMPEG_TAG=n7.1.5 +ARG MXL_TAG=v1.1.0-beta-1 +ARG NV_CODEC_HEADERS_TAG=n12.1.14.0 + +# gcc-13 is required by the MXL SDK (C++20 features + spdlog headers). +# Ubuntu 22.04 default is gcc-11, so pull gcc-13 from the ubuntu +# toolchain PPA and expose it under a distinct name. FFmpeg and +# avplumber keep using the distro's default gcc-11. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + software-properties-common \ + && add-apt-repository -y ppa:ubuntu-toolchain-r/test \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + autoconf \ + automake \ + bison \ + build-essential \ + ca-certificates \ + cmake \ + curl \ + flex \ + g++-13 \ + gcc-13 \ + git \ + libboost-all-dev \ + libcurl4-openssl-dev \ + libgnutls28-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libpcap-dev \ + libssl-dev \ + libtool \ + libx11-dev \ + make \ + nasm \ + ninja-build \ + pkg-config \ + python3 \ + python3-dev \ + python3-numpy \ + python3-pil \ + python3-pip \ + tar \ + unzip \ + xxd \ + zip \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# --- nv-codec-headers (headers-only, required by 0002 CUDA suite) ------- +RUN git clone --quiet --branch "${NV_CODEC_HEADERS_TAG}" --depth 1 \ + https://github.com/FFmpeg/nv-codec-headers.git /tmp/nv-codec-headers \ + && make -C /tmp/nv-codec-headers install PREFIX=/usr/local \ + && rm -rf /tmp/nv-codec-headers + +# Ubuntu 22.04 ships cmake 3.22 but MXL requires >= 3.24. Upgrade via +# pip — installs a modern cmake wheel to /usr/local/bin ahead of the +# distro's /usr/bin/cmake so both MXL and FFmpeg pick it up. +RUN python3 -m pip install --no-cache-dir --upgrade 'cmake>=3.28' + +# --- Rust (needed by MXL's ada-url dep pulled through vcpkg) ------------ +ENV RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo +ENV PATH=/opt/cargo/bin:$PATH +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain 1.88.0 --profile minimal \ + && rustc --version + +# --- vcpkg (MXL's CMake toolchain) ------------------------------------- +ENV VCPKG_ROOT=/opt/vcpkg +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +RUN git clone --quiet https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" \ + && "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + +# --- MXL SDK ------------------------------------------------------------ +# Built with gcc-13 (required by the SDK), FFmpeg + avplumber below +# still use the distro gcc-11 that the CUDA suite is validated with. +RUN git clone --quiet --branch "${MXL_TAG}" --depth 1 \ + https://github.com/dmf-mxl/mxl.git /tmp/mxl \ + && CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 \ + cmake -S /tmp/mxl -B /tmp/mxl/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_MANIFEST_MODE=ON \ + -DBUILD_DOCS=OFF \ + -DBUILD_TESTS=OFF \ + && cmake --build /tmp/mxl/build --parallel "$(nproc)" \ + && cmake --install /tmp/mxl/build \ + && ldconfig \ + && sed -i '/^Requires.private: spdlog$/d' /usr/local/lib/pkgconfig/libmxl.pc \ + && rm -rf /tmp/mxl + +# --- Patched FFmpeg (full 8-patch stack, with CUDA + MXL) --------------- +COPY deps/ffmpeg-patches /build/deps/ffmpeg-patches + +RUN git clone --quiet --branch "${FFMPEG_TAG}" --depth 1 \ + https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg \ + && git -C /tmp/ffmpeg config user.name "mxl-demo builder" \ + && git -C /tmp/ffmpeg config user.email "mxl-demo@local" \ + && git -C /tmp/ffmpeg am /build/deps/ffmpeg-patches/*.patch \ + # 0008 was cherry-picked with -X theirs and pulled in a couple of + # test scaffolding lines that reference files not present in + # n7.1.5. Strip them so `make` succeeds. + && sed -i '/tests\/fate\/ogg-\(opus\|flac\|vorbis\)\.mak/d' /tmp/ffmpeg/tests/Makefile \ + && sed -i '/^# uri test if demuxer is enabled$/,+1d' /tmp/ffmpeg/tests/fate/mxl.mak + +RUN cd /tmp/ffmpeg \ + && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ + ./configure \ + --prefix=/usr/local \ + --libdir=/usr/local/lib \ + --incdir=/usr/local/include \ + --pkgconfigdir=/usr/local/lib/pkgconfig \ + --enable-gpl \ + --enable-nonfree \ + --enable-shared \ + --disable-static \ + --enable-cuda \ + --enable-cuda-nvcc \ + --enable-cuvid \ + --enable-ffnvcodec \ + --enable-nvdec \ + --enable-nvenc \ + --disable-libnpp \ + --enable-libmxl \ + --enable-demuxer=mxl \ + --enable-muxer=mxl \ + --enable-protocol=mxl \ + --disable-doc \ + --disable-htmlpages \ + --disable-manpages \ + --disable-podpages \ + --disable-txtpages \ + --extra-cflags=-I/usr/local/cuda/include \ + --extra-ldflags=-L/usr/local/cuda/lib64 \ + --extra-ldexeflags=-Wl,--disable-new-dtags,-rpath,/usr/local/lib \ + --nvcc=/usr/local/cuda/bin/nvcc \ + --nvccflags='-gencode arch=compute_70,code=compute_70 -O2' \ + && make -j"${BUILD_JOBS:-2}" \ + && make install \ + && ldconfig \ + && rm -rf /tmp/ffmpeg + +# Assertions: both the CUDA composition suite AND MXL must be present. +RUN /usr/local/bin/ffmpeg -hide_banner -filters | grep -q ' overlay_many_cuda ' \ + && /usr/local/bin/ffmpeg -hide_banner -filters \ + | awk '$2 == "transition_cuda" && $1 ~ /C/ { found = 1 } END { exit !found }' \ + && /usr/local/bin/ffmpeg -hide_banner -demuxers 2>/dev/null \ + | awk '$1 ~ /D/ && $2 == "mxl" { found = 1 } END { exit !found }' \ + && /usr/local/bin/ffmpeg -hide_banner -muxers 2>/dev/null \ + | awk '$1 ~ /E/ && $2 == "mxl" { found = 1 } END { exit !found }' + +# --- avplumber (with CUDA) --------------------------------------------- +WORKDIR /build + +COPY deps /build/deps +COPY Makefile generate_node_list /build/ +COPY src /build/src +COPY pyplumber /build/pyplumber +COPY avpmixer /build/avpmixer + +RUN git init --quiet /build \ + && git -C /build config user.name "mxl-demo builder" \ + && git -C /build config user.email "mxl-demo@local" \ + && git -C /build commit --quiet --allow-empty -m "workspace ${AVPLUMBER_REVISION}" + +RUN make -j"${BUILD_JOBS:-2}" python_module \ + BUILD_TYPE=Release \ + HAVE_CUDA=1 \ + HAVE_DRM=0 \ + HAVE_GL=0 \ + HAVE_NVOF_FRUC=0 \ + HAVE_NVCC=1 \ + HAVE_NVJPEG=0 \ + HAVE_TENSORRT=0 \ + NEURAL_NET=0 \ + CUDA_MODULE_IMAGE_FORMAT=ptx \ + NVCC=/usr/local/cuda/bin/nvcc \ + CUDA_ROOT=/usr/local/cuda \ + PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ + CXXFLAGS+=' -I/usr/local/include -I/usr/local/cuda/include' \ + LFLAGS+=' -Wl,--disable-new-dtags -L/usr/local/lib -Wl,-rpath,/usr/local/lib -L/usr/local/cuda/lib64 -Wl,-rpath,/usr/local/cuda/lib64' + +RUN test -f /build/_avplumber"$(python3-config --extension-suffix)" \ + && ldd /build/_avplumber"$(python3-config --extension-suffix)" \ + | grep -q '/usr/local/lib/libavfilter' \ + && LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64 \ + PYTHONPATH=/build \ + python3 -c 'import pyplumber' + +COPY demos/mxl /build/demos/mxl + +RUN if [ -f /build/demos/mxl/requirements.txt ]; then \ + python3 -m pip install --no-cache-dir \ + -r /build/demos/mxl/requirements.txt; \ + fi + +ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64 +ENV AVPLUMBER_REVISION=${AVPLUMBER_REVISION} +ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,video +ENV NVIDIA_VISIBLE_DEVICES=all +ENV PYTHONPATH=/build +ENV PYTHONUNBUFFERED=1 + +ENTRYPOINT ["python3", "/build/demos/mxl/mxl_demo.py"] diff --git a/demos/mxl/README.md b/demos/mxl/README.md new file mode 100644 index 00000000..549a4082 --- /dev/null +++ b/demos/mxl/README.md @@ -0,0 +1,123 @@ +# MXL demo + +End-to-end proof that avplumber can publish and consume flows on the +[MXL](https://github.com/dmf-mxl/mxl) shared-memory transport, using +avplumber's generic `input` / `output` nodes with `format="mxl"` — no +MXL-specific node types are introduced. + +Two graphs run in one process: + +* **writer** — `lavfi testsrc` → decode → rescale to yuv422p10le → + encode as `v210` (10-bit 4:2:2 uncompressed, per SMPTE ST 2110) → + mux → `output(format="mxl", url="/dev/shm/mxl")` (the MXL FFmpeg + muxer takes a plain filesystem path). +* **reader** — `input(format="mxl", url="mxl:///dev/shm/mxl?id=")` + → demux → decode v210 → rescale to yuv420p → encode as mpeg4 → mux + → fragmented mp4. + +Verified end-to-end on Docker Desktop (aarch64) producing 28k mpeg4 +packets of the testsrc pattern in a 10-second run. + +## Status + +NVIDIA support is temporarily disabled in this image. The Dockerfile +applies only patches `0001`, `0005`, `0006`, `0008` from +`deps/ffmpeg-patches/`; `0002` (CUDA composition suite), `0003` (NPP +CUDA13 compat), `0004` (NVDEC intra), and `0007` (NDI) are held back +until the mxl path is folded into `demos/mixer/Dockerfile` alongside +CUDA. + +## Requirements + +* Linux x86_64 or aarch64 (MXL SDK is Linux-only — no macOS support). +* Docker with enough tmpfs at `/dev/shm` (default is fine for the demo). +* The 8-patch stack applied to `n7.1.5`. `deps/ffmpeg-patches/verify.sh` + confirms the tree hash; the MXL patch (`0008-...`) is generated + from `cbcrc/FFmpeg` via `deps/ffmpeg-patches/Dockerfile.mkpatch` + — see that directory's README. + +## Build + +From the repository root (after `git submodule update --init --recursive`): + +```sh +docker build -f demos/mxl/Dockerfile -t avplumber-mxl:local . +``` + +The build: + +1. Bootstraps `microsoft/vcpkg` and Rust 1.88.0. +2. Builds and installs the MXL SDK (`dmf-mxl/mxl` @ `v1.1.0-beta-1`) + into `/usr/local` — libmxl is statically linked against spdlog, so + its `.pc` file is scrubbed of the private `Requires` before FFmpeg + configure. +3. Applies the reduced patch stack to FFmpeg `n7.1.5`, strips a + handful of fork-side test scaffolding that references files not + present in `n7.1.5` (`tests/fate/ogg-*.mak`, duplicated + `fate-mxl-uri` rule), and configures with `--enable-libmxl + --enable-demuxer=mxl --enable-muxer=mxl --enable-protocol=mxl`. +4. Builds `pyplumber` with `HAVE_CUDA=0 NEURAL_NET=0` (`-j2` to stay + inside Docker Desktop's memory ceiling on Apple Silicon). + +Verified inside the image: + +```sh +ffmpeg -hide_banner -demuxers | grep mxl # D mxl Media eXchange Layer +ffmpeg -hide_banner -muxers | grep mxl # E mxl Media eXchange Layer +python3 -c 'import pyplumber' # OK +``` + +## Run + +The demo needs a shared `/dev/shm/mxl` domain directory with an +`options.json` file. On a single host use `--ipc=host`: + +```sh +mkdir -p /dev/shm/mxl +echo '{"urn:x-mxl:option:history_duration/v1.0": 100000000}' \ + > /dev/shm/mxl/options.json + +docker run --rm --ipc=host \ + -v /dev/shm/mxl:/dev/shm/mxl \ + -v "$PWD/demos/mxl/test-media:/media" \ + -e AVP_OUTPUT=/media/out.mp4 \ + avplumber-mxl:local +``` + +Defaults to publishing an `lavfi testsrc` pattern; override with +`-e AVP_INPUT=/media/your-file.mp4` for a real file (played back +looped and realtime-paced through `InputRec`). + +Runs both graphs in one process. Ctrl-C to stop — the fragmented mp4 +stays playable even on interrupt because `movflags=frag_keyframe+ +empty_moov+default_base_moof` is set on the reader-side output. + +Split writer and reader across two containers by passing +`--writer-only` / `--reader-only` and sharing the flow UUID via +`AVP_MXL_VIDEO_ID`. + +## Codec choices + +MXL flows carry uncompressed frames. The muxer registered by +`0008-*.patch` insists on `v210` (10-bit 4:2:2 packed) for video — +`rawvideo` is rejected at header write. The reader re-encodes to +`mpeg4` (rather than H.264) because the NVIDIA-off image does not +link `libx264`. + +## Known gaps + +* Passing `blocking=1` to the MXL demuxer through avplumber's `options` + dict does not currently reach the demuxer's private AVOptions + — the reader survives EAGAIN via `auto_restart:"group"` instead. + Root-cause is in the order of format assignment vs `openInput` in + `src/nodes/input.cpp`; direct `ffmpeg -blocking 1` on the CLI works. +* The demo runs the reader at wall-clock max (~2800 fps into mpeg4 + at 320×240) because we didn't wire in a realtime pacer on the + reader side. Adding a `RealtimeVideoFrame` node between decode and + encode would cap the reader to the source frame rate. + +## References + +* MXL SDK: +* MXL FFmpeg fork (source for the 0008 patch): +* Reference build guidance: diff --git a/demos/mxl/mxl_demo.py b/demos/mxl/mxl_demo.py new file mode 100644 index 00000000..195a3780 --- /dev/null +++ b/demos/mxl/mxl_demo.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +"""MXL round-trip demo. + +Builds two avplumber graphs in one process: + +* writer: file -> demux -> decode -> rawvideo encode -> mxl mux -> mxl:// URL +* reader: mxl:// URL -> demux -> decode -> re-encode -> file + +The two graphs share a `/dev/shm/mxl` domain and a set of flow UUIDs so +the reader picks up what the writer publishes. The point is to exercise +the FFmpeg MXL demuxer and muxer end-to-end through avplumber's generic +`input` / `output` nodes; MXL is passed via `format="mxl"` and per-stream +UUIDs in `options`, with no new node types. +""" + +from __future__ import annotations + +import argparse +import os +import sys +import time +import uuid + +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) + +import pyplumber +from pyplumber.node import ( + Demux, + DecVideo, + EncVideo, + Input, + InputRec, + Mux, + Output, +) + + +def _parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( + "--input", + default=os.environ.get("AVP_INPUT", "lavfi:testsrc=size=320x240:rate=25"), + help=( + "Source for the writer. A filesystem path is played back through " + "InputRec (looped, realtime-paced). A value prefixed with " + "'lavfi:' opens an FFmpeg libavfilter source (infinite, " + "monotonic timestamps) — the recommended default because MXL is " + "a live shared-memory transport." + ), + ) + p.add_argument( + "--output", + default=os.environ.get("AVP_OUTPUT", "output.mp4"), + help="Local file the reader side writes.", + ) + p.add_argument( + "--domain", + default=os.environ.get("AVP_MXL_DOMAIN", "/dev/shm/mxl"), + help="MXL shared-memory domain directory.", + ) + p.add_argument( + "--video-flow-id", + default=os.environ.get("AVP_MXL_VIDEO_ID"), + help="Video flow UUID. Random if unset.", + ) + p.add_argument( + "--audio-flow-id", + default=os.environ.get("AVP_MXL_AUDIO_ID"), + help="Audio flow UUID. Unused if input has no audio.", + ) + p.add_argument( + "--reader-only", + action="store_true", + help="Skip the writer side (attach to a domain another process is publishing to).", + ) + p.add_argument( + "--writer-only", + action="store_true", + help="Skip the reader side (publish only).", + ) + return p.parse_args() + + +def _writer_url(domain: str) -> str: + """MXL muxer wants a plain filesystem path (AVFMT_NOFILE).""" + return domain + + +def _reader_url(domain: str, video_id: str) -> str: + """MXL demuxer takes an mxl:// URI with the flow id in a query param.""" + return f"mxl://{domain}?id={video_id}" + + +def _build_writer(avp: pyplumber.AVPlumber, args: argparse.Namespace) -> None: + """source -> demux -> decode -> rescale -> v210 encode -> mxl output.""" + from pyplumber.node import RescaleVideo, AssumeVideoFormat + + if args.input.startswith("lavfi:"): + # Live libavfilter source: infinite, monotonic PTS, always + # ready. No loop-wraparound DTS glitches. + avp.addNode(Input({ + "url": args.input.removeprefix("lavfi:"), + "format": "lavfi", + "dst": "w_in", + "group": "w_in", + "name": "w_input", + "auto_restart": "off", + "timeout": -1, + })) + else: + # File input: loop and pace to source frame rate so the + # downstream MXL writer sees a steady live stream. + avp.addNode(InputRec({ + "url": args.input, + "dst": "w_in", + "group": "w_in", + "name": "w_input", + "auto_restart": "off", + "timeout": -1, + "loop": True, + "realtime": True, + })) + avp.addNode(Demux({ + "src": "w_in", + "routing": {"v:0": "w_vpkt"}, + "group": "w_in", + "auto_restart": "off", + })) + avp.addNode(DecVideo({ + "src": "w_vpkt", + "dst": "w_vframe", + "group": "w_in", + "name": "w_dec", + "auto_restart": "off", + })) + # v210 requires 10-bit 4:2:2 planar. Convert explicitly so the + # encoder's metadata chain is unambiguous. + avp.addNode(RescaleVideo({ + "src": "w_vframe", + "dst": "w_vscaled", + "group": "w_in", + "name": "w_scale", + "dst_pixel_format": "yuv422p10le", + "auto_restart": "off", + })) + avp.addNode(AssumeVideoFormat({ + "src": "w_vscaled", + "dst": "w_vscaled_assumed", + "group": "w_in", + "width": 320, + "height": 240, + "pixel_format": "yuv422p10le", + "real_pixel_format": "yuv422p10le", + "auto_restart": "off", + })) + avp.addNode(EncVideo({ + "src": "w_vscaled_assumed", + "dst": "w_venc", + "group": "w_in", + "name": "w_enc", + "codec": "v210", + "auto_restart": "off", + })) + avp.addNode(Mux({ + "src": ["w_venc"], + "dst": "w_mux", + "group": "w_in", + })) + mxl_options: dict[str, str] = {} + if args.video_flow_id: + mxl_options["video_flow_id"] = args.video_flow_id + if args.audio_flow_id: + mxl_options["audio_flow_id"] = args.audio_flow_id + avp.addNode(Output({ + "src": "w_mux", + "url": _writer_url(args.domain), + "format": "mxl", + "options": mxl_options, + "group": "w_in", + "auto_restart": "off", + })) + + +def _build_reader(avp: pyplumber.AVPlumber, args: argparse.Namespace) -> None: + """mxl:// -> demux -> decode -> rescale -> mpeg4 encode -> file.""" + from pyplumber.node import RescaleVideo, AssumeVideoFormat + + # The MXL demuxer returns EAGAIN when the writer hasn't published + # a new grain yet, which avplumber's Input node treats as fatal. + # `auto_restart:"group"` restarts the reader chain when that + # happens; the group's own auto-restart handling reopens the edge + # cleanly. `blocking=1` should make av_read_frame block instead, + # but passing it via the `options` dict does not yet reach the + # demuxer's private AVOptions (see TODO in README). + avp.addNode(Input({ + "url": _reader_url(args.domain, args.video_flow_id), + "format": "mxl", + "dst": "r_in", + "group": "r_in", + "name": "r_input", + "auto_restart": "group", + "timeout": -1, + "options": { + "blocking": "1", + "grain_index_init": "head", + }, + })) + avp.addNode(Demux({ + "src": "r_in", + "routing": {"v:0": "r_vpkt"}, + "group": "r_in", + "auto_restart": "off", + })) + avp.addNode(DecVideo({ + "src": "r_vpkt", + "dst": "r_vframe", + "group": "r_in", + "name": "r_dec", + "auto_restart": "off", + })) + # mpeg4 needs yuv420p — rescale from the 10-bit 4:2:2 flow. + avp.addNode(RescaleVideo({ + "src": "r_vframe", + "dst": "r_vscaled", + "group": "r_in", + "name": "r_scale", + "dst_pixel_format": "yuv420p", + "auto_restart": "off", + })) + avp.addNode(AssumeVideoFormat({ + "src": "r_vscaled", + "dst": "r_vscaled_assumed", + "group": "r_in", + "width": 320, + "height": 240, + "pixel_format": "yuv420p", + "real_pixel_format": "yuv420p", + "auto_restart": "off", + })) + avp.addNode(EncVideo({ + "src": "r_vscaled_assumed", + "dst": "r_venc", + "group": "r_in", + "name": "r_enc", + "codec": "mpeg4", + "auto_restart": "off", + })) + avp.addNode(Mux({ + "src": ["r_venc"], + "dst": "r_mux", + "group": "r_in", + })) + avp.addNode(Output({ + "src": "r_mux", + "url": args.output, + "format": "mp4", + # Fragmented MP4 so the file stays playable even if the demo + # is interrupted before the mp4 trailer is written. + "options": {"movflags": "frag_keyframe+empty_moov+default_base_moof"}, + "group": "r_in", + "auto_restart": "off", + })) + + +def main() -> int: + args = _parse_args() + if args.reader_only and args.writer_only: + print("--reader-only and --writer-only are mutually exclusive", file=sys.stderr) + return 2 + if not args.video_flow_id: + args.video_flow_id = str(uuid.uuid4()) + print(f"video flow id: {args.video_flow_id}") + + avp = pyplumber.AVPlumber() + + if not args.reader_only: + _build_writer(avp, args) + if not args.writer_only: + _build_reader(avp, args) + + if not args.reader_only: + avp.group("w_in").startNodes() + if not args.writer_only: + # Give the writer a moment to open the flow before the reader + # tries to attach. + if not args.reader_only: + time.sleep(1.0) + avp.group("r_in").startNodes() + + try: + while True: + time.sleep(1) + avp.heartbeat() + except KeyboardInterrupt: + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/demos/mxl/requirements.txt b/demos/mxl/requirements.txt new file mode 100644 index 00000000..e6cda319 --- /dev/null +++ b/demos/mxl/requirements.txt @@ -0,0 +1,2 @@ +# No extra Python deps yet. Kept so the Dockerfile pip step stays uniform +# with the mixer/playlist demos and future TUI/tests can slot in here. diff --git a/demos/mxl/test-media/.gitkeep b/demos/mxl/test-media/.gitkeep new file mode 100644 index 00000000..e69de29b From 7270a969070feb6533d15d9b496b21447fb3213c Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Wed, 9 Sep 2026 15:32:26 +0200 Subject: [PATCH 3/5] Fold the MXL Dockerfile into the shared mixer image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes demos/mxl/Dockerfile and merges its libmxl SDK build, vcpkg / Rust / gcc-13 setup, cmake upgrade, tests/Makefile cleanups, MXL FFmpeg configure flags, and MXL demuxer/muxer assertions into demos/mixer/Dockerfile. The mixer image is now a superset of what the mixer, playlist, replay, and mxl demos need — one image, one build. Also COPYs demos/mxl into the image so the MXL demo can be run through the shared tag by overriding the entrypoint: docker run --rm --ipc=host --entrypoint python3 \ -v /dev/shm/mxl:/dev/shm/mxl \ -v "$PWD/demos/mxl/test-media:/media" \ avplumber-mixer:local /build/demos/mxl/mxl_demo.py Verified end-to-end: the folded image still exposes overlay_many_cuda (patch 0002), transition_cuda, the CUDA composition suite, plus the MXL demuxer/muxer (patch 0008), and pyplumber loads cleanly. Co-Authored-By: Claude Opus 4.7 --- demos/README.md | 6 +- demos/mixer/Dockerfile | 85 +++++++++++++++- demos/mxl/Dockerfile | 218 ----------------------------------------- demos/mxl/README.md | 46 +++++---- 4 files changed, 109 insertions(+), 246 deletions(-) delete mode 100644 demos/mxl/Dockerfile diff --git a/demos/README.md b/demos/README.md index beead22a..7f4de71c 100644 --- a/demos/README.md +++ b/demos/README.md @@ -89,7 +89,7 @@ docker compose --env-file demos/dmabuf-browser/.env.example \ a bundled animated page, Electron, GPU capture, Janus, and browser preview. - [CUDA overlay validation](cuda-overlay/README.md#run): run `./demos/cuda-overlay/run.sh` to build, generate fixtures, and compare results. -- [MXL round-trip](mxl/README.md): publish a file into an MXL shared-memory - domain and read it back. Linux-only, no GPU required, but depends on the - pending `0008-avformat-libmxl-demuxer-muxer.patch`. +- [MXL round-trip](mxl/README.md): publish into and read back from an MXL + shared-memory domain. Linux-only, uses the shared mixer image + (`avplumber-mixer:local`). GPU optional. diff --git a/demos/mixer/Dockerfile b/demos/mixer/Dockerfile index 01697f3b..963abccc 100644 --- a/demos/mixer/Dockerfile +++ b/demos/mixer/Dockerfile @@ -4,48 +4,113 @@ ARG DEBIAN_FRONTEND=noninteractive ARG AVPLUMBER_REVISION=workspace ARG FFMPEG_TAG=n7.1.5 ARG NV_CODEC_HEADERS_TAG=n12.1.14.0 +ARG MXL_TAG=v1.1.0-beta-1 +# gcc-13 is required by the MXL SDK (C++20 features + newer libstdc++). +# Ubuntu 22.04 defaults to gcc-11, so pull gcc-13 from the ubuntu +# toolchain PPA and keep it alongside; FFmpeg and avplumber continue +# to use gcc-11. RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + software-properties-common \ + && add-apt-repository -y ppa:ubuntu-toolchain-r/test \ + && apt-get update \ && apt-get install -y --no-install-recommends \ autoconf \ automake \ + bison \ build-essential \ ca-certificates \ cmake \ curl \ + flex \ + g++-13 \ + gcc-13 \ git \ libboost-all-dev \ libcurl4-openssl-dev \ libgnutls28-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libpcap-dev \ libssl-dev \ libtool \ + libx11-dev \ make \ nasm \ + ninja-build \ pkg-config \ python3 \ python3-dev \ python3-numpy \ python3-pil \ python3-pip \ + tar \ + unzip \ xxd \ + zip \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* +# Ubuntu 22.04 ships cmake 3.22 but the MXL SDK requires >= 3.24. +# Upgrade via pip so a modern cmake is on PATH before FFmpeg configure. +RUN python3 -m pip install --no-cache-dir --upgrade 'cmake>=3.28' + RUN git clone --quiet --branch "${NV_CODEC_HEADERS_TAG}" --depth 1 \ https://github.com/FFmpeg/nv-codec-headers.git /tmp/nv-codec-headers \ && make -C /tmp/nv-codec-headers install PREFIX=/usr/local \ && rm -rf /tmp/nv-codec-headers +# --- Rust (MXL's ada-url dep pulled through vcpkg needs a rustc) -------- +ENV RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo +ENV PATH=/opt/cargo/bin:$PATH +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain 1.88.0 --profile minimal \ + && rustc --version + +# --- vcpkg (used by the MXL SDK's CMake toolchain) ---------------------- +ENV VCPKG_ROOT=/opt/vcpkg +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 +RUN git clone --quiet https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" \ + && "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics + +# --- MXL SDK ------------------------------------------------------------ +# Built with gcc-13 (required by the SDK). FFmpeg + avplumber below keep +# using the distro gcc-11 that the CUDA composition suite is validated +# with. libmxl statically links spdlog, so its pkg-config file is +# scrubbed of the private Requires before FFmpeg configure looks at it. +RUN git clone --quiet --branch "${MXL_TAG}" --depth 1 \ + https://github.com/dmf-mxl/mxl.git /tmp/mxl \ + && CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 \ + cmake -S /tmp/mxl -B /tmp/mxl/build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_MANIFEST_MODE=ON \ + -DBUILD_DOCS=OFF \ + -DBUILD_TESTS=OFF \ + && cmake --build /tmp/mxl/build --parallel "$(nproc)" \ + && cmake --install /tmp/mxl/build \ + && ldconfig \ + && sed -i '/^Requires.private: spdlog$/d' /usr/local/lib/pkgconfig/libmxl.pc \ + && rm -rf /tmp/mxl + COPY deps/ffmpeg-patches /build/deps/ffmpeg-patches RUN git clone --quiet --branch "${FFMPEG_TAG}" --depth 1 \ https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg \ && git -C /tmp/ffmpeg config user.name "mixer-demo builder" \ && git -C /tmp/ffmpeg config user.email "mixer-demo@local" \ - && git -C /tmp/ffmpeg am /build/deps/ffmpeg-patches/*.patch + && git -C /tmp/ffmpeg am /build/deps/ffmpeg-patches/*.patch \ + # 0008 (MXL) was cherry-picked with -X theirs and pulled in a + # handful of test-scaffolding lines from the fork that reference + # files not present in n7.1.5. Strip them so `make` succeeds. + && sed -i '/tests\/fate\/ogg-\(opus\|flac\|vorbis\)\.mak/d' /tmp/ffmpeg/tests/Makefile \ + && sed -i '/^# uri test if demuxer is enabled$/,+1d' /tmp/ffmpeg/tests/fate/mxl.mak RUN cd /tmp/ffmpeg \ - && ./configure \ + && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ + ./configure \ --prefix=/usr/local \ --libdir=/usr/local/lib \ --incdir=/usr/local/include \ @@ -61,6 +126,10 @@ RUN cd /tmp/ffmpeg \ --enable-nvdec \ --enable-nvenc \ --disable-libnpp \ + --enable-libmxl \ + --enable-demuxer=mxl \ + --enable-muxer=mxl \ + --enable-protocol=mxl \ --disable-doc \ --disable-htmlpages \ --disable-manpages \ @@ -78,7 +147,11 @@ RUN cd /tmp/ffmpeg \ RUN /usr/local/bin/ffmpeg -hide_banner -filters | grep -q ' overlay_many_cuda ' \ && /usr/local/bin/ffmpeg -hide_banner -filters \ - | awk '$2 == "transition_cuda" && $1 ~ /C/ { found = 1 } END { exit !found }' + | awk '$2 == "transition_cuda" && $1 ~ /C/ { found = 1 } END { exit !found }' \ + && /usr/local/bin/ffmpeg -hide_banner -demuxers 2>/dev/null \ + | awk '$1 ~ /D/ && $2 == "mxl" { found = 1 } END { exit !found }' \ + && /usr/local/bin/ffmpeg -hide_banner -muxers 2>/dev/null \ + | awk '$1 ~ /E/ && $2 == "mxl" { found = 1 } END { exit !found }' WORKDIR /build @@ -118,8 +191,12 @@ RUN test -f /build/_avplumber"$(python3-config --extension-suffix)" \ python3 -c 'import pyplumber' COPY demos/mixer /build/demos/mixer +COPY demos/mxl /build/demos/mxl -RUN python3 -m pip install --no-cache-dir -r /build/demos/mixer/requirements.txt +RUN python3 -m pip install --no-cache-dir -r /build/demos/mixer/requirements.txt \ + && if [ -s /build/demos/mxl/requirements.txt ]; then \ + python3 -m pip install --no-cache-dir -r /build/demos/mxl/requirements.txt; \ + fi ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64 ENV AVPLUMBER_REVISION=${AVPLUMBER_REVISION} diff --git a/demos/mxl/Dockerfile b/demos/mxl/Dockerfile deleted file mode 100644 index 2ff2a7d7..00000000 --- a/demos/mxl/Dockerfile +++ /dev/null @@ -1,218 +0,0 @@ -# MXL demo build image (NVIDIA-enabled). -# -# Mirrors ../mixer/Dockerfile: builds patched FFmpeg on top of an -# NVIDIA CUDA base image and produces avplumber's python_module with -# CUDA support. Adds libmxl SDK and the 0008 MXL patch so the same -# image can also run the MXL demo. Once this proves stable it can -# fold into demos/mixer/Dockerfile itself. - -FROM nvidia/cuda:11.7.1-devel-ubuntu22.04 - -ARG DEBIAN_FRONTEND=noninteractive -ARG AVPLUMBER_REVISION=workspace -ARG FFMPEG_TAG=n7.1.5 -ARG MXL_TAG=v1.1.0-beta-1 -ARG NV_CODEC_HEADERS_TAG=n12.1.14.0 - -# gcc-13 is required by the MXL SDK (C++20 features + spdlog headers). -# Ubuntu 22.04 default is gcc-11, so pull gcc-13 from the ubuntu -# toolchain PPA and expose it under a distinct name. FFmpeg and -# avplumber keep using the distro's default gcc-11. -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - software-properties-common \ - && add-apt-repository -y ppa:ubuntu-toolchain-r/test \ - && apt-get update \ - && apt-get install -y --no-install-recommends \ - autoconf \ - automake \ - bison \ - build-essential \ - ca-certificates \ - cmake \ - curl \ - flex \ - g++-13 \ - gcc-13 \ - git \ - libboost-all-dev \ - libcurl4-openssl-dev \ - libgnutls28-dev \ - libgstreamer1.0-dev \ - libgstreamer-plugins-base1.0-dev \ - libpcap-dev \ - libssl-dev \ - libtool \ - libx11-dev \ - make \ - nasm \ - ninja-build \ - pkg-config \ - python3 \ - python3-dev \ - python3-numpy \ - python3-pil \ - python3-pip \ - tar \ - unzip \ - xxd \ - zip \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* - -# --- nv-codec-headers (headers-only, required by 0002 CUDA suite) ------- -RUN git clone --quiet --branch "${NV_CODEC_HEADERS_TAG}" --depth 1 \ - https://github.com/FFmpeg/nv-codec-headers.git /tmp/nv-codec-headers \ - && make -C /tmp/nv-codec-headers install PREFIX=/usr/local \ - && rm -rf /tmp/nv-codec-headers - -# Ubuntu 22.04 ships cmake 3.22 but MXL requires >= 3.24. Upgrade via -# pip — installs a modern cmake wheel to /usr/local/bin ahead of the -# distro's /usr/bin/cmake so both MXL and FFmpeg pick it up. -RUN python3 -m pip install --no-cache-dir --upgrade 'cmake>=3.28' - -# --- Rust (needed by MXL's ada-url dep pulled through vcpkg) ------------ -ENV RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo -ENV PATH=/opt/cargo/bin:$PATH -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --default-toolchain 1.88.0 --profile minimal \ - && rustc --version - -# --- vcpkg (MXL's CMake toolchain) ------------------------------------- -ENV VCPKG_ROOT=/opt/vcpkg -ENV VCPKG_FORCE_SYSTEM_BINARIES=1 -RUN git clone --quiet https://github.com/microsoft/vcpkg.git "$VCPKG_ROOT" \ - && "$VCPKG_ROOT/bootstrap-vcpkg.sh" -disableMetrics - -# --- MXL SDK ------------------------------------------------------------ -# Built with gcc-13 (required by the SDK), FFmpeg + avplumber below -# still use the distro gcc-11 that the CUDA suite is validated with. -RUN git clone --quiet --branch "${MXL_TAG}" --depth 1 \ - https://github.com/dmf-mxl/mxl.git /tmp/mxl \ - && CC=/usr/bin/gcc-13 CXX=/usr/bin/g++-13 \ - cmake -S /tmp/mxl -B /tmp/mxl/build -G Ninja \ - -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX=/usr/local \ - -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \ - -DVCPKG_MANIFEST_MODE=ON \ - -DBUILD_DOCS=OFF \ - -DBUILD_TESTS=OFF \ - && cmake --build /tmp/mxl/build --parallel "$(nproc)" \ - && cmake --install /tmp/mxl/build \ - && ldconfig \ - && sed -i '/^Requires.private: spdlog$/d' /usr/local/lib/pkgconfig/libmxl.pc \ - && rm -rf /tmp/mxl - -# --- Patched FFmpeg (full 8-patch stack, with CUDA + MXL) --------------- -COPY deps/ffmpeg-patches /build/deps/ffmpeg-patches - -RUN git clone --quiet --branch "${FFMPEG_TAG}" --depth 1 \ - https://github.com/FFmpeg/FFmpeg.git /tmp/ffmpeg \ - && git -C /tmp/ffmpeg config user.name "mxl-demo builder" \ - && git -C /tmp/ffmpeg config user.email "mxl-demo@local" \ - && git -C /tmp/ffmpeg am /build/deps/ffmpeg-patches/*.patch \ - # 0008 was cherry-picked with -X theirs and pulled in a couple of - # test scaffolding lines that reference files not present in - # n7.1.5. Strip them so `make` succeeds. - && sed -i '/tests\/fate\/ogg-\(opus\|flac\|vorbis\)\.mak/d' /tmp/ffmpeg/tests/Makefile \ - && sed -i '/^# uri test if demuxer is enabled$/,+1d' /tmp/ffmpeg/tests/fate/mxl.mak - -RUN cd /tmp/ffmpeg \ - && PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ - ./configure \ - --prefix=/usr/local \ - --libdir=/usr/local/lib \ - --incdir=/usr/local/include \ - --pkgconfigdir=/usr/local/lib/pkgconfig \ - --enable-gpl \ - --enable-nonfree \ - --enable-shared \ - --disable-static \ - --enable-cuda \ - --enable-cuda-nvcc \ - --enable-cuvid \ - --enable-ffnvcodec \ - --enable-nvdec \ - --enable-nvenc \ - --disable-libnpp \ - --enable-libmxl \ - --enable-demuxer=mxl \ - --enable-muxer=mxl \ - --enable-protocol=mxl \ - --disable-doc \ - --disable-htmlpages \ - --disable-manpages \ - --disable-podpages \ - --disable-txtpages \ - --extra-cflags=-I/usr/local/cuda/include \ - --extra-ldflags=-L/usr/local/cuda/lib64 \ - --extra-ldexeflags=-Wl,--disable-new-dtags,-rpath,/usr/local/lib \ - --nvcc=/usr/local/cuda/bin/nvcc \ - --nvccflags='-gencode arch=compute_70,code=compute_70 -O2' \ - && make -j"${BUILD_JOBS:-2}" \ - && make install \ - && ldconfig \ - && rm -rf /tmp/ffmpeg - -# Assertions: both the CUDA composition suite AND MXL must be present. -RUN /usr/local/bin/ffmpeg -hide_banner -filters | grep -q ' overlay_many_cuda ' \ - && /usr/local/bin/ffmpeg -hide_banner -filters \ - | awk '$2 == "transition_cuda" && $1 ~ /C/ { found = 1 } END { exit !found }' \ - && /usr/local/bin/ffmpeg -hide_banner -demuxers 2>/dev/null \ - | awk '$1 ~ /D/ && $2 == "mxl" { found = 1 } END { exit !found }' \ - && /usr/local/bin/ffmpeg -hide_banner -muxers 2>/dev/null \ - | awk '$1 ~ /E/ && $2 == "mxl" { found = 1 } END { exit !found }' - -# --- avplumber (with CUDA) --------------------------------------------- -WORKDIR /build - -COPY deps /build/deps -COPY Makefile generate_node_list /build/ -COPY src /build/src -COPY pyplumber /build/pyplumber -COPY avpmixer /build/avpmixer - -RUN git init --quiet /build \ - && git -C /build config user.name "mxl-demo builder" \ - && git -C /build config user.email "mxl-demo@local" \ - && git -C /build commit --quiet --allow-empty -m "workspace ${AVPLUMBER_REVISION}" - -RUN make -j"${BUILD_JOBS:-2}" python_module \ - BUILD_TYPE=Release \ - HAVE_CUDA=1 \ - HAVE_DRM=0 \ - HAVE_GL=0 \ - HAVE_NVOF_FRUC=0 \ - HAVE_NVCC=1 \ - HAVE_NVJPEG=0 \ - HAVE_TENSORRT=0 \ - NEURAL_NET=0 \ - CUDA_MODULE_IMAGE_FORMAT=ptx \ - NVCC=/usr/local/cuda/bin/nvcc \ - CUDA_ROOT=/usr/local/cuda \ - PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ - CXXFLAGS+=' -I/usr/local/include -I/usr/local/cuda/include' \ - LFLAGS+=' -Wl,--disable-new-dtags -L/usr/local/lib -Wl,-rpath,/usr/local/lib -L/usr/local/cuda/lib64 -Wl,-rpath,/usr/local/cuda/lib64' - -RUN test -f /build/_avplumber"$(python3-config --extension-suffix)" \ - && ldd /build/_avplumber"$(python3-config --extension-suffix)" \ - | grep -q '/usr/local/lib/libavfilter' \ - && LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64 \ - PYTHONPATH=/build \ - python3 -c 'import pyplumber' - -COPY demos/mxl /build/demos/mxl - -RUN if [ -f /build/demos/mxl/requirements.txt ]; then \ - python3 -m pip install --no-cache-dir \ - -r /build/demos/mxl/requirements.txt; \ - fi - -ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64 -ENV AVPLUMBER_REVISION=${AVPLUMBER_REVISION} -ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility,video -ENV NVIDIA_VISIBLE_DEVICES=all -ENV PYTHONPATH=/build -ENV PYTHONUNBUFFERED=1 - -ENTRYPOINT ["python3", "/build/demos/mxl/mxl_demo.py"] diff --git a/demos/mxl/README.md b/demos/mxl/README.md index 549a4082..70e51214 100644 --- a/demos/mxl/README.md +++ b/demos/mxl/README.md @@ -18,19 +18,14 @@ Two graphs run in one process: Verified end-to-end on Docker Desktop (aarch64) producing 28k mpeg4 packets of the testsrc pattern in a 10-second run. -## Status - -NVIDIA support is temporarily disabled in this image. The Dockerfile -applies only patches `0001`, `0005`, `0006`, `0008` from -`deps/ffmpeg-patches/`; `0002` (CUDA composition suite), `0003` (NPP -CUDA13 compat), `0004` (NVDEC intra), and `0007` (NDI) are held back -until the mxl path is folded into `demos/mixer/Dockerfile` alongside -CUDA. - ## Requirements -* Linux x86_64 or aarch64 (MXL SDK is Linux-only — no macOS support). +* Linux x86_64 (MXL SDK is Linux-only — no macOS support). * Docker with enough tmpfs at `/dev/shm` (default is fine for the demo). +* NVIDIA GPU + [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + are optional for the MXL demo itself (CUDA init fails silently at + runtime without them), but the shared mixer image is built with + CUDA support and other demos need it. * The 8-patch stack applied to `n7.1.5`. `deps/ffmpeg-patches/verify.sh` confirms the tree hash; the MXL patch (`0008-...`) is generated from `cbcrc/FFmpeg` via `deps/ffmpeg-patches/Dockerfile.mkpatch` @@ -38,26 +33,31 @@ CUDA. ## Build -From the repository root (after `git submodule update --init --recursive`): +This demo shares its runtime image with `demos/mixer/`. Build once +from the repository root (after `git submodule update --init --recursive`): ```sh -docker build -f demos/mxl/Dockerfile -t avplumber-mxl:local . +docker build -f demos/mixer/Dockerfile -t avplumber-mixer:local . ``` -The build: +The image build: -1. Bootstraps `microsoft/vcpkg` and Rust 1.88.0. -2. Builds and installs the MXL SDK (`dmf-mxl/mxl` @ `v1.1.0-beta-1`) +1. Installs distro `gcc-11` plus `gcc-13` from + `ppa:ubuntu-toolchain-r/test` (the MXL SDK needs C++20; FFmpeg and + avplumber keep using gcc-11). +2. Bootstraps `microsoft/vcpkg` and Rust 1.88.0. +3. Builds and installs the MXL SDK (`dmf-mxl/mxl` @ `v1.1.0-beta-1`) into `/usr/local` — libmxl is statically linked against spdlog, so its `.pc` file is scrubbed of the private `Requires` before FFmpeg configure. -3. Applies the reduced patch stack to FFmpeg `n7.1.5`, strips a +4. Applies the full 8-patch stack to FFmpeg `n7.1.5`, strips a handful of fork-side test scaffolding that references files not present in `n7.1.5` (`tests/fate/ogg-*.mak`, duplicated - `fate-mxl-uri` rule), and configures with `--enable-libmxl - --enable-demuxer=mxl --enable-muxer=mxl --enable-protocol=mxl`. -4. Builds `pyplumber` with `HAVE_CUDA=0 NEURAL_NET=0` (`-j2` to stay - inside Docker Desktop's memory ceiling on Apple Silicon). + `fate-mxl-uri` rule), and configures with CUDA (`--enable-cuda + --enable-cuda-nvcc --enable-cuvid --enable-nvdec --enable-nvenc`) + plus MXL (`--enable-libmxl --enable-demuxer=mxl --enable-muxer=mxl + --enable-protocol=mxl`). +5. Builds `pyplumber` with `HAVE_CUDA=1 HAVE_NVCC=1`. Verified inside the image: @@ -78,12 +78,16 @@ echo '{"urn:x-mxl:option:history_duration/v1.0": 100000000}' \ > /dev/shm/mxl/options.json docker run --rm --ipc=host \ + --entrypoint python3 \ -v /dev/shm/mxl:/dev/shm/mxl \ -v "$PWD/demos/mxl/test-media:/media" \ -e AVP_OUTPUT=/media/out.mp4 \ - avplumber-mxl:local + avplumber-mixer:local /build/demos/mxl/mxl_demo.py ``` +Add `--gpus all` if you're on an NVIDIA host and want CUDA +initialization to succeed (the MXL demo itself doesn't need it). + Defaults to publishing an `lavfi testsrc` pattern; override with `-e AVP_INPUT=/media/your-file.mp4` for a real file (played back looped and realtime-paced through `InputRec`). From 014d0e4e3051b9c52f1678a97b69890d668439ba Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Sun, 13 Sep 2026 02:29:49 +0200 Subject: [PATCH 4/5] Bump MXL SDK from v1.1.0-beta-1 to v1.1.0 v1.1.0 was cut on 2026-09-09, 125 commits ahead of the beta pin we shipped. Notable changes in that window: audio RMA samples are packed in channel-major order, fabrics/OFI got batch grain drain and configurable CQ depth, the audio testsrc no longer busy-spins at 100% CPU, and Rust deps were bumped to fix RUSTSEC-2026-0204. Verified end-to-end: MXL demuxer/muxer still register in FFmpeg, the CUDA composition suite still passes assertions, and the mxl_demo round-trip publishes into and reads back from the v1.1.0 SDK, producing 16,753 mpeg4 packets in a short run. Co-Authored-By: Claude Opus 4.7 --- demos/mixer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/mixer/Dockerfile b/demos/mixer/Dockerfile index 963abccc..663e029d 100644 --- a/demos/mixer/Dockerfile +++ b/demos/mixer/Dockerfile @@ -4,7 +4,7 @@ ARG DEBIAN_FRONTEND=noninteractive ARG AVPLUMBER_REVISION=workspace ARG FFMPEG_TAG=n7.1.5 ARG NV_CODEC_HEADERS_TAG=n12.1.14.0 -ARG MXL_TAG=v1.1.0-beta-1 +ARG MXL_TAG=v1.1.0 # gcc-13 is required by the MXL SDK (C++20 features + newer libstdc++). # Ubuntu 22.04 defaults to gcc-11, so pull gcc-13 from the ubuntu From 7f71a4029ba0b6a852ffcb58504475fd8e09e796 Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Mon, 14 Sep 2026 10:41:24 +0200 Subject: [PATCH 5/5] Warn on input options not consumed by libavformat `input` and `input_rec` forward the JSON `options` dictionary to `avformat_open_input`, which strips out any AVOptions it recognizes and leaves unrecognized entries behind. Nothing was inspecting those leftovers, so misnamed options or ones targeting the wrong scope were silently ignored. Log each remaining entry as a warning. Verified against the MXL round-trip demo that `blocking=0/1` and `grain_index_init=head` are consumed by the MXL demuxer as advertised (`blocking=0` produces the expected EAGAIN + auto_restart storm, `blocking=1` matches the auto default for video-only). Drop the "known gap" bullet in `demos/mxl/README.md` and the matching inline comment in `mxl_demo.py`. Co-Authored-By: Claude Opus 4.7 --- demos/mxl/README.md | 5 ----- demos/mxl/mxl_demo.py | 12 +++++------- src/nodes/input.cpp | 6 ++++++ src/nodes/input_rec.cpp | 6 ++++++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/demos/mxl/README.md b/demos/mxl/README.md index 70e51214..c0cc26fb 100644 --- a/demos/mxl/README.md +++ b/demos/mxl/README.md @@ -110,11 +110,6 @@ link `libx264`. ## Known gaps -* Passing `blocking=1` to the MXL demuxer through avplumber's `options` - dict does not currently reach the demuxer's private AVOptions - — the reader survives EAGAIN via `auto_restart:"group"` instead. - Root-cause is in the order of format assignment vs `openInput` in - `src/nodes/input.cpp`; direct `ffmpeg -blocking 1` on the CLI works. * The demo runs the reader at wall-clock max (~2800 fps into mpeg4 at 320×240) because we didn't wire in a realtime pacer on the reader side. Adding a `RealtimeVideoFrame` node between decode and diff --git a/demos/mxl/mxl_demo.py b/demos/mxl/mxl_demo.py index 195a3780..362f2814 100644 --- a/demos/mxl/mxl_demo.py +++ b/demos/mxl/mxl_demo.py @@ -185,13 +185,11 @@ def _build_reader(avp: pyplumber.AVPlumber, args: argparse.Namespace) -> None: """mxl:// -> demux -> decode -> rescale -> mpeg4 encode -> file.""" from pyplumber.node import RescaleVideo, AssumeVideoFormat - # The MXL demuxer returns EAGAIN when the writer hasn't published - # a new grain yet, which avplumber's Input node treats as fatal. - # `auto_restart:"group"` restarts the reader chain when that - # happens; the group's own auto-restart handling reopens the edge - # cleanly. `blocking=1` should make av_read_frame block instead, - # but passing it via the `options` dict does not yet reach the - # demuxer's private AVOptions (see TODO in README). + # `blocking=1` makes the demuxer wait up to one frame period for + # the next grain instead of returning EAGAIN immediately, so the + # reader stays open once the writer has produced its first grain. + # `auto_restart:"group"` still covers the brief startup race + # before grain 0 is available. avp.addNode(Input({ "url": _reader_url(args.domain, args.video_flow_id), "format": "mxl", diff --git a/src/nodes/input.cpp b/src/nodes/input.cpp index ede4a759..eaaa2234 100644 --- a/src/nodes/input.cpp +++ b/src/nodes/input.cpp @@ -174,6 +174,12 @@ class StreamInput: public NodeSingleOutput, public IStreamsInput, pu } setTimeout(initial_timeout); ictx_.openInput(params["url"], opts, ifmt); + if (opts.count() > 0) { + for (auto &entry : opts) { + logstream << "WARNING: input option not consumed by libavformat: " + << entry.key() << "=" << entry.value(); + } + } ictx_.findStreamInfo(); logstream << "Opened URL " << params["url"] << " . Streams:"; for (unsigned i=0; i, public IStreamsInput, } ictx_.openInput(params["url"], opts, ifmt); + if (opts.count() > 0) { + for (auto &entry : opts) { + logstream << "WARNING: input option not consumed by libavformat: " + << entry.key() << "=" << entry.value(); + } + } ictx_.findStreamInfo(); logstream << "Opened URL " << params["url"] << " . Streams:"; for (unsigned i=0; i