From 9e144c8745e8387f909523cc06266a8767f7a8a7 Mon Sep 17 00:00:00 2001 From: Mateusz Starzak Date: Wed, 9 Sep 2026 15:06:37 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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