From 74935c3f277faf83501dc499ebf640e84f8fb72b Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:18:17 -0300 Subject: [PATCH 01/10] create Dockerfile, add pugixml as dependency when building --- Dockerfile | 56 +++++++++++++++++++++++++++++++++++ Makefile | 2 +- config | 2 ++ ngx_morpheus_internal.cpp | 2 +- stream_with_scte35_events.mpd | 28 ++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 Dockerfile create mode 100644 stream_with_scte35_events.mpd diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1bdd8b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# Stage 1: Build the dynamic module +FROM nginx:1.24.0 AS builder + +RUN apt-get update && apt-get install -y \ + build-essential \ + libpcre3-dev \ + zlib1g-dev \ + libssl-dev \ + wget \ + && rm -rf /var/lib/apt/lists/* + +# Download nginx source matching the base image version +RUN wget http://nginx.org/download/nginx-1.24.0.tar.gz \ + && tar -xzf nginx-1.24.0.tar.gz \ + && rm nginx-1.24.0.tar.gz + +# Remove -Werror from nginx build system — it breaks C++ compilation +RUN sed -i 's/ -Werror//' /nginx-1.24.0/auto/cc/gcc + +COPY . /morpheus/ + +# Download pugixml v1.15 source from the official repository. +# Runs after COPY so it works regardless of whether local pugixml/ files are present. +RUN mkdir -p /morpheus/pugixml \ + && wget -qO /morpheus/pugixml/pugixml.cpp https://raw.githubusercontent.com/zeux/pugixml/v1.15/src/pugixml.cpp \ + && wget -qO /morpheus/pugixml/pugixml.hpp https://raw.githubusercontent.com/zeux/pugixml/v1.15/src/pugixml.hpp \ + && wget -qO /morpheus/pugixml/pugiconfig.hpp https://raw.githubusercontent.com/zeux/pugixml/v1.15/src/pugiconfig.hpp + +WORKDIR /nginx-1.24.0 +RUN ./configure \ + --with-ld-opt="-lstdc++" \ + --with-cc-opt="-Wno-write-strings" \ + --with-compat \ + --add-dynamic-module=/morpheus \ + && make modules + + +# Stage 2: Production image +FROM nginx:1.24.0 + +# Copy the compiled module +COPY --from=builder /nginx-1.24.0/objs/ngx_http_morpheus_module.so /etc/nginx/modules/ + +# Copy nginx configuration and MIME types +COPY nginx_morpheus.conf /etc/nginx/nginx.conf +COPY mime.types /etc/nginx/mime.types + +# nginx_morpheus.conf includes /usr/share/nginx/modules/*.conf which doesn't +# exist in the Docker image (unlike Ubuntu package installs); create it empty +RUN mkdir -p /usr/share/nginx/modules/ + +EXPOSE 80 + +# /dev/shm is a tmpfs mounted at container runtime, so its subdirectories +# cannot be created at build time — create them before starting nginx +CMD ["/bin/sh", "-c", "mkdir -p /dev/shm/nginx/client_temp && exec nginx -g 'daemon off;'"] diff --git a/Makefile b/Makefile index 46b5592..9de2fab 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC = g++ CFLAGS = -Wall -W -g -SRCS = ngx_morpheus_internal.cpp morpheus_main.cpp +SRCS = ngx_morpheus_internal.cpp morpheus_main.cpp pugixml.cpp OBJS = $(SRCS:.cpp=.o) all: morphdriver diff --git a/config b/config index 15270d1..95d34f7 100644 --- a/config +++ b/config @@ -2,11 +2,13 @@ ngx_addon_name=ngx_http_morpheus_module MORPHEUS_SRCS=" \ $ngx_addon_dir/ngx_http_morpheus_module.cpp \ $ngx_addon_dir/ngx_morpheus_internal.cpp \ + $ngx_addon_dir/pugixml/pugixml.cpp \ " if test -n "$ngx_module_link"; then ngx_module_type=HTTP ngx_module_name=ngx_http_morpheus_module + ngx_module_incs="$ngx_addon_dir $ngx_addon_dir/pugixml" ngx_module_srcs="$MORPHEUS_SRCS" . auto/module else diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 4f07c38..943259d 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -11,7 +11,7 @@ #include const std::unordered_map MANIFEST_URLS = { - {1, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"}, + {1, "http://localhost:3000/api/list-mpd?vasturl=http://localhost:3000/samples/dash-alt-mpd/vast-sample.xm"}, {2, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"}, {3, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"} }; diff --git a/stream_with_scte35_events.mpd b/stream_with_scte35_events.mpd new file mode 100644 index 0000000..a83bce8 --- /dev/null +++ b/stream_with_scte35_events.mpd @@ -0,0 +1,28 @@ + + + https://dash.akamaized.net/akamai/bbb_30fps/ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ff0cb483b8a34d0b5ea601c7f6b4b580c7213bfb Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Fri, 13 Mar 2026 11:13:08 -0300 Subject: [PATCH 02/10] add missing 'l' at the end of url --- ngx_morpheus_internal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 943259d..280380d 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -11,7 +11,7 @@ #include const std::unordered_map MANIFEST_URLS = { - {1, "http://localhost:3000/api/list-mpd?vasturl=http://localhost:3000/samples/dash-alt-mpd/vast-sample.xm"}, + {1, "http://localhost:3000/api/list-mpd?vasturl=http://localhost:3000/samples/dash-alt-mpd/vast-sample.xml"}, {2, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"}, {3, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"} }; From 1eaf4bb00a8861ad19fb46eaef2bbb1e7394f829 Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:11:11 -0300 Subject: [PATCH 03/10] support parsing scte35:SegmentationDescriptor into DASH OverlayEvent, unify scte-to-overlay and scte-to-alternative into a single flow --- Makefile | 8 +- morpheus_main.cpp | 9 +- ngx_http_morpheus_module.cpp | 18 +-- ngx_morpheus_internal.cpp | 273 ++++++++++++++++++++++++++++------- 4 files changed, 233 insertions(+), 75 deletions(-) diff --git a/Makefile b/Makefile index 9de2fab..9aa003c 100644 --- a/Makefile +++ b/Makefile @@ -2,14 +2,14 @@ CC = g++ CFLAGS = -Wall -W -g -SRCS = ngx_morpheus_internal.cpp morpheus_main.cpp pugixml.cpp +SRCS = ngx_morpheus_internal.cpp morpheus_main.cpp OBJS = $(SRCS:.cpp=.o) all: morphdriver -morphdriver: $(OBJS) cxxopts.hpp - $(CC) $(CFLAGS) $(SRCS) -o $@ +morphdriver: $(OBJS) pugixml.o cxxopts.hpp + $(CC) $(CFLAGS) $(OBJS) pugixml.o -o $@ clean: - rm -f *.o morphdriver + rm -f ngx_morpheus_internal.o morpheus_main.o morphdriver diff --git a/morpheus_main.cpp b/morpheus_main.cpp index eaa6a58..dfd65a3 100644 --- a/morpheus_main.cpp +++ b/morpheus_main.cpp @@ -7,12 +7,11 @@ extern "C" { using namespace std; -void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd, const bool alternativeconf); +void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd); int main (int argc, char *argv[]) { string inmpd_o, drmconf_o, iframesmpd_o; - bool alternativeconf_o = false; try { @@ -22,7 +21,6 @@ int main (int argc, char *argv[]) { ("n", "encoder mpd file", cxxopts::value()) ("i", "iframes track mpd file", cxxopts::value()) ("d", "ckm encrypt context response xml file", cxxopts::value()) - ("a", "alterantive insertion") ("h,help", "Print this help") ; @@ -43,8 +41,6 @@ int main (int argc, char *argv[]) { if (result.count("d")) drmconf_o = result["d"].as().c_str(); - if (result.count("a")) - alternativeconf_o = true; } catch (const cxxopts::OptionException& e) { @@ -55,9 +51,8 @@ int main (int argc, char *argv[]) { const char* inmpd = inmpd_o.length() ? inmpd_o.c_str() : NULL; const char* drmconf = drmconf_o.length() ? drmconf_o.c_str() : NULL; const char* iframesmpd = iframesmpd_o.length() ? iframesmpd_o.c_str() : NULL; - const bool alternativeconf = alternativeconf_o; - morph_process(inmpd, drmconf, iframesmpd, alternativeconf); + morph_process(inmpd, drmconf, iframesmpd); return EXIT_SUCCESS; } diff --git a/ngx_http_morpheus_module.cpp b/ngx_http_morpheus_module.cpp index 7bb6c6a..7fac46f 100644 --- a/ngx_http_morpheus_module.cpp +++ b/ngx_http_morpheus_module.cpp @@ -72,7 +72,7 @@ static char *ngx_http_morpheus_merge_loc_conf(ngx_conf_t *cf, static ngx_int_t ngx_http_morpheus_init(ngx_conf_t *cf); static char *ngx_http_morpheus(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); -void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd, const bool alternativeconf); +void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd); static ngx_conf_bitmask_t ngx_http_morpheus_methods_mask[] = { { ngx_string("off"), NGX_HTTP_DAV_OFF }, @@ -248,12 +248,11 @@ ngx_http_morpheus_put_handler(ngx_http_request_t *r) { size_t root; time_t date; - ngx_str_t *temp, path, mode_value; + ngx_str_t *temp, path; ngx_uint_t status; ngx_file_info_t fi; ngx_ext_rename_file_t ext; ngx_http_morpheus_loc_conf_t *dlcf; - bool scte_to_alternative = false; if (r->request_body == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, @@ -290,18 +289,7 @@ ngx_http_morpheus_put_handler(ngx_http_request_t *r) temp = &r->request_body->temp_file->file.name; - #define SCTE_TO_ALT_MODE "scte-to-alternative" - if (r->args.len > 0) { - if (ngx_http_arg(r, (u_char *) "mode", 4, &mode_value) == NGX_OK) { - if (mode_value.len == (sizeof(SCTE_TO_ALT_MODE) - 1) && - ngx_strncmp(mode_value.data, SCTE_TO_ALT_MODE, sizeof(SCTE_TO_ALT_MODE) - 1) == 0) - { - scte_to_alternative = true; - } - } - } - - morph_process((const char*)temp->data, NULL, NULL, scte_to_alternative); + morph_process((const char*)temp->data, NULL, NULL); if (ngx_file_info(path.data, &fi) == NGX_FILE_ERROR) { status = NGX_HTTP_CREATED; diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 280380d..5ee3140 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include "pugixml.hpp" #include @@ -16,6 +19,175 @@ const std::unordered_map MANIFEST_URLS = { {3, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"} }; +#define BANNER_AD_URL "http://localhost:3001/image/html?template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" +#define SKYSCRAPER_AD_URL "http://localhost:3001/image/html?template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" +#define LSHAPE_RIGHT_AD_URL "http://localhost:3001/image/html?template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" +#define LSHAPE_LEFT_AD_URL "http://localhost:3001/image/html?template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" + +static std::string fmt_double(double v) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%g", v); + return buf; +} + +struct vec2 { double x, y; }; +struct squeeze_cfg { bool active; double pct; const char* origin; }; +struct shape_cfg { const char* url; int z; vec2 viewport, size, top_left; squeeze_cfg squeeze; }; + +static const shape_cfg SHAPE_CONFIGS[] = { + // URL Z VIEWPORT SIZE TOPLEFT SQUEEZE + { BANNER_AD_URL, 1, {1920, 1080}, {1920, 324}, {0, 756}, {false, 0.0, ""} }, + { SKYSCRAPER_AD_URL, 1, {1920, 1080}, {480, 1080}, {0, 0}, {false, 0.0, ""} }, + { LSHAPE_RIGHT_AD_URL, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top left"} }, + { LSHAPE_LEFT_AD_URL, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top right"} }, +}; + +static int parse_shape(const char* upid_text) { + char buf[256] = {}; + std::strncpy(buf, upid_text, sizeof(buf) - 1); + + // trim leading whitespace + char* p = buf; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p; + + // trim trailing whitespace and '&' + char* end = p + std::strlen(p) - 1; + while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r' || *end == '&')) + *end-- = '\0'; + + // normalize to lowercase + for (char* c = p; *c; ++c) + *c = (char)std::tolower((unsigned char)*c); + + if (std::strncmp(p, "shape=", 6) != 0) return -1; + const char* val = p + 6; + + if (std::strcmp(val, "banner") == 0) return 0; + if (std::strcmp(val, "skyscraper") == 0) return 1; + if (std::strcmp(val, "lshape-right") == 0) return 2; + if (std::strcmp(val, "lshape-left") == 0) return 3; + return -1; +} + +struct overlay_ev { + bool is_start; + uint32_t seg_id; + uint64_t ptime; + uint64_t dur; + int shape_idx; +}; + +void morph_overlay(pugi::xml_document& mpddoc) { + pugi::xml_node mpd = mpddoc.child("MPD"); + + for (pugi::xml_node period : mpd.children("Period")) { + pugi::xml_node scte_stream; + for (pugi::xml_node es : period.children("EventStream")) { + std::string scheme = es.attribute("schemeIdUri").value(); + if (scheme.find("scte35") != std::string::npos) { + scte_stream = es; + break; + } + } + if (!scte_stream) continue; + + uint64_t timescale = scte_stream.attribute("timescale").as_ullong(1000); + std::vector events; + + for (pugi::xml_node event : scte_stream.children("Event")) { + uint64_t ptime = event.attribute("presentationTime").as_ullong(0); + uint64_t dur = event.attribute("duration").as_ullong(0); + + pugi::xml_node section = event.child("scte35:SpliceInfoSection"); + if (!section) continue; + pugi::xml_node descriptor = section.child("scte35:SegmentationDescriptor"); + if (!descriptor) continue; + + int type_id = descriptor.attribute("segmentationTypeId").as_int(0); + + const char* seg_id_str = descriptor.attribute("segmentationEventId").value(); + uint32_t seg_id = seg_id_str[0] + ? (uint32_t)std::strtoul(seg_id_str, nullptr, 0) + : event.attribute("id").as_uint(0); + + if (type_id == 56) { + const char* upid_text = nullptr; + for (pugi::xml_node upid : descriptor.children("scte35:SegmentationUpid")) { + if (upid.attribute("segmentationUpidType").as_int(0) == 14) { + upid_text = upid.child_value(); + break; + } + } + if (!upid_text || !upid_text[0]) { + std::cerr << "morpheus: overlay start event id=" << seg_id << " missing UPID type 14, skipping\n"; + continue; + } + int shape_idx = parse_shape(upid_text); + if (shape_idx < 0) { + std::cerr << "morpheus: overlay start event id=" << seg_id << " unknown shape, skipping\n"; + continue; + } + events.push_back({true, seg_id, ptime, dur, shape_idx}); + + } else if (type_id == 57) { + events.push_back({false, seg_id, ptime, 0, -1}); + } + } + + pugi::xml_node new_stream = period.append_child("EventStream"); + new_stream.append_attribute("schemeIdUri").set_value("urn:scte:dash:scte214-events"); + new_stream.append_attribute("timescale").set_value((unsigned long long)timescale); + + for (const overlay_ev& oe : events) { + pugi::xml_node ev = new_stream.append_child("Event"); + ev.append_attribute("presentationTime").set_value((unsigned long long)oe.ptime); + + if (oe.is_start) { + if (oe.dur > 0) + ev.append_attribute("duration").set_value((unsigned long long)oe.dur); + ev.append_attribute("id").set_value((unsigned long)oe.seg_id); + + const shape_cfg& cfg = SHAPE_CONFIGS[oe.shape_idx]; + + pugi::xml_node overlay = ev.append_child("OverlayEvent"); + overlay.append_attribute("uri").set_value(cfg.url); + overlay.append_attribute("mimeType").set_value("text/html"); + overlay.append_attribute("earliestResolutionTime").set_value("0"); + overlay.append_attribute("loop").set_value("false"); + overlay.append_attribute("mode").set_value("start"); + overlay.append_attribute("z").set_value(cfg.z); + + overlay.append_child("Viewport") + .append_attribute("x").set_value(fmt_double(cfg.viewport.x).c_str()); + overlay.child("Viewport") + .append_attribute("y").set_value(fmt_double(cfg.viewport.y).c_str()); + + overlay.append_child("Size") + .append_attribute("x").set_value(fmt_double(cfg.size.x).c_str()); + overlay.child("Size") + .append_attribute("y").set_value(fmt_double(cfg.size.y).c_str()); + + overlay.append_child("TopLeft") + .append_attribute("x").set_value(fmt_double(cfg.top_left.x).c_str()); + overlay.child("TopLeft") + .append_attribute("y").set_value(fmt_double(cfg.top_left.y).c_str()); + + if (cfg.squeeze.active) { + pugi::xml_node sq = overlay.append_child("SqueezeCurrent"); + sq.append_attribute("percentage").set_value(fmt_double(cfg.squeeze.pct).c_str()); + sq.append_attribute("origin").set_value(cfg.squeeze.origin); + } + } else { + pugi::xml_node overlay = ev.append_child("OverlayEvent"); + overlay.append_attribute("mode").set_value("stop"); + overlay.append_attribute("refId").set_value((unsigned long)oe.seg_id); + } + } + + period.remove_child(scte_stream); + } +} + #ifdef __cplusplus extern "C" { #endif @@ -88,52 +260,59 @@ void morph_drm(pugi::xml_document& mpddoc, const char* drmconf) { void morph_alternative(pugi::xml_document& mpddoc) { pugi::xml_node mpd = mpddoc.child("MPD"); - + for (pugi::xml_node period : mpd.children("Period")) { for (pugi::xml_node event_stream : period.children("EventStream")) { - + std::string scheme_id = event_stream.attribute("schemeIdUri").value(); - if (scheme_id == "urn:scte:scte35:2013:xml") { - - event_stream.attribute("schemeIdUri").set_value("urn:mpeg:dash:event:alternativeMPD:replace:2025"); - - if (!event_stream.attribute("xmlns")) { - event_stream.append_attribute("xmlns").set_value(""); - } - - for (pugi::xml_node event : event_stream.children("Event")) { - - uint64_t presentationTime = event.attribute("presentationTime").as_ullong(0); - uint64_t duration = event.attribute("duration").as_ullong(0); - - pugi::xml_node scte_section = event.child("scte35:SpliceInfoSection"); - if (scte_section) { - pugi::xml_node splice_insert = scte_section.child("scte35:SpliceInsert"); - - if (splice_insert) { - int splice_event_id = splice_insert.attribute("spliceEventId").as_int(1); - - pugi::xml_node break_duration = splice_insert.child("scte35:BreakDuration"); - - uint64_t scte_duration = break_duration.attribute("duration").as_ullong(0); - - event.remove_child(scte_section); - - pugi::xml_node replace_presentation = event.append_child("ReplacePresentation"); - - auto it = MANIFEST_URLS.find(splice_event_id); - if (it == MANIFEST_URLS.end()) { - throw std::runtime_error("Manifest URL not found for spliceEventId: " + std::to_string(splice_event_id)); - } - std::string manifest_url = it->second; - - replace_presentation.append_attribute("url").set_value(manifest_url.c_str()); - replace_presentation.append_attribute("earliestResolutionTimeOffset").set_value(std::to_string(presentationTime).c_str()); - replace_presentation.append_attribute("returnOffset").set_value(std::to_string(duration).c_str()); - replace_presentation.append_attribute("maxDuration").set_value(std::to_string(scte_duration).c_str()); - replace_presentation.append_attribute("clip").set_value("false"); - replace_presentation.append_attribute("startAtPlayhead").set_value("false"); + if (scheme_id != "urn:scte:scte35:2013:xml") continue; + + // Only claim streams that actually contain SpliceInsert events + bool has_splice_insert = false; + for (pugi::xml_node ev : event_stream.children("Event")) { + pugi::xml_node sec = ev.child("scte35:SpliceInfoSection"); + if (sec && sec.child("scte35:SpliceInsert")) { has_splice_insert = true; break; } + } + if (!has_splice_insert) continue; + + event_stream.attribute("schemeIdUri").set_value("urn:mpeg:dash:event:alternativeMPD:replace:2025"); + + if (!event_stream.attribute("xmlns")) { + event_stream.append_attribute("xmlns").set_value(""); + } + + for (pugi::xml_node event : event_stream.children("Event")) { + + uint64_t presentationTime = event.attribute("presentationTime").as_ullong(0); + uint64_t duration = event.attribute("duration").as_ullong(0); + + pugi::xml_node scte_section = event.child("scte35:SpliceInfoSection"); + if (scte_section) { + pugi::xml_node splice_insert = scte_section.child("scte35:SpliceInsert"); + + if (splice_insert) { + int splice_event_id = splice_insert.attribute("spliceEventId").as_int(1); + + pugi::xml_node break_duration = splice_insert.child("scte35:BreakDuration"); + + uint64_t scte_duration = break_duration.attribute("duration").as_ullong(0); + + event.remove_child(scte_section); + + pugi::xml_node replace_presentation = event.append_child("ReplacePresentation"); + + auto it = MANIFEST_URLS.find(splice_event_id); + if (it == MANIFEST_URLS.end()) { + throw std::runtime_error("Manifest URL not found for spliceEventId: " + std::to_string(splice_event_id)); } + std::string manifest_url = it->second; + + replace_presentation.append_attribute("url").set_value(manifest_url.c_str()); + replace_presentation.append_attribute("earliestResolutionTimeOffset").set_value(std::to_string(presentationTime).c_str()); + replace_presentation.append_attribute("returnOffset").set_value(std::to_string(duration).c_str()); + replace_presentation.append_attribute("maxDuration").set_value(std::to_string(scte_duration).c_str()); + replace_presentation.append_attribute("clip").set_value("false"); + replace_presentation.append_attribute("startAtPlayhead").set_value("false"); } } } @@ -141,11 +320,7 @@ void morph_alternative(pugi::xml_document& mpddoc) { } } -void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd, const bool alternativeconf) { - /* if iframes track mpd exists add its' AdaptationSet first - * then if drmconf exists, add drm pieces - * then do the other modifications to the mpd - */ +void morph_process(const char* encmpd, const char* drmconf, const char* iframesmpd) { pugi::xml_document doc; doc.load_file((const char*)encmpd); @@ -155,8 +330,8 @@ void morph_process(const char* encmpd, const char* drmconf, const char* iframesm if (drmconf) morph_drm(doc, drmconf); - if(alternativeconf) - morph_alternative(doc); + morph_alternative(doc); + morph_overlay(doc); pugi::xml_node mpd = doc.child("MPD"); From 01dc50543b059cf775c8aaa8c11afb33351e6f71 Mon Sep 17 00:00:00 2001 From: Nicolas Levy Date: Mon, 1 Jun 2026 15:55:58 -0300 Subject: [PATCH 04/10] fix overlay ad URLs to ad-gen published port (:8888, not :3001) The four SHAPE_CONFIGS overlay ad URLs pointed at localhost:3001, but the real-time-ad-gen API serving /image/html is published on host port 8888 (8888:8000). The player resolving the OverlayEvent uri got nothing on :3001, so overlays never loaded. Point all four at localhost:8888. Co-Authored-By: Claude Opus 4.8 (1M context) --- ngx_morpheus_internal.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 5ee3140..ed6223f 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -19,10 +19,10 @@ const std::unordered_map MANIFEST_URLS = { {3, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"} }; -#define BANNER_AD_URL "http://localhost:3001/image/html?template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" -#define SKYSCRAPER_AD_URL "http://localhost:3001/image/html?template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" -#define LSHAPE_RIGHT_AD_URL "http://localhost:3001/image/html?template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" -#define LSHAPE_LEFT_AD_URL "http://localhost:3001/image/html?template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" +#define BANNER_AD_URL "http://localhost:8888/image/html?template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" +#define SKYSCRAPER_AD_URL "http://localhost:8888/image/html?template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" +#define LSHAPE_RIGHT_AD_URL "http://localhost:8888/image/html?template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" +#define LSHAPE_LEFT_AD_URL "http://localhost:8888/image/html?template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" static std::string fmt_double(double v) { char buf[32]; From 27d6f0292bebbdd5dbffb4c295c841ba4d991ca0 Mon Sep 17 00:00:00 2001 From: Nicolas Levy Date: Mon, 1 Jun 2026 16:34:31 -0300 Subject: [PATCH 05/10] make overlay ad URLs configurable via env vars (fallback to defaults) Replace the four hardcoded *_AD_URL #defines with runtime resolution: the overlay uri is built as ${AD_GEN_BASE_URL}/image/html?${MORPHEUS__QUERY}. Each value is read with getenv() and falls back to the compiled default (localhost:8888 + the original per-shape query strings) when the env var is unset or empty, so the demo still runs with no configuration. Adds the required `env` directives in nginx_morpheus.conf so the vars reach workers. Co-Authored-By: Claude Opus 4.8 (1M context) --- nginx_morpheus.conf | 10 +++++++++ ngx_morpheus_internal.cpp | 45 +++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/nginx_morpheus.conf b/nginx_morpheus.conf index e2af0df..4416173 100644 --- a/nginx_morpheus.conf +++ b/nginx_morpheus.conf @@ -8,6 +8,16 @@ pid /run/nginx.pid; include /usr/share/nginx/modules/*.conf; load_module modules/ngx_http_morpheus_module.so; +# Pass overlay ad URL config from the worker environment through to the +# morpheus module (nginx sanitizes worker env by default, so each var must +# be declared here for getenv() to see it). All optional — empty/unset values +# fall back to the module's compiled defaults. +env AD_GEN_BASE_URL; +env MORPHEUS_BANNER_QUERY; +env MORPHEUS_SKYSCRAPER_QUERY; +env MORPHEUS_LSHAPE_RIGHT_QUERY; +env MORPHEUS_LSHAPE_LEFT_QUERY; + events { worker_connections 1024; } diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index ed6223f..107a44f 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include "pugixml.hpp" @@ -19,10 +20,18 @@ const std::unordered_map MANIFEST_URLS = { {3, "https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd"} }; -#define BANNER_AD_URL "http://localhost:8888/image/html?template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" -#define SKYSCRAPER_AD_URL "http://localhost:8888/image/html?template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" -#define LSHAPE_RIGHT_AD_URL "http://localhost:8888/image/html?template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" -#define LSHAPE_LEFT_AD_URL "http://localhost:8888/image/html?template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" +// Overlay ad URLs are configurable at runtime via environment variables, with +// safe fallback to the compiled defaults below if the env var is unset/empty. +// final url = ${AD_GEN_BASE_URL}/image/html?${MORPHEUS__QUERY} +// The base default is intentionally neutral/portable (localhost, not a LAN IP). +#define DEFAULT_AD_GEN_BASE_URL "http://localhost:8888" + +// Per-shape fallback query strings (everything after "image/html?"), using the +// repo's original template_ids and targeting params. +#define DEFAULT_BANNER_QUERY "template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" +#define DEFAULT_SKYSCRAPER_QUERY "template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" +#define DEFAULT_LSHAPE_RIGHT_QUERY "template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" +#define DEFAULT_LSHAPE_LEFT_QUERY "template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" static std::string fmt_double(double v) { char buf[32]; @@ -32,16 +41,29 @@ static std::string fmt_double(double v) { struct vec2 { double x, y; }; struct squeeze_cfg { bool active; double pct; const char* origin; }; -struct shape_cfg { const char* url; int z; vec2 viewport, size, top_left; squeeze_cfg squeeze; }; +struct shape_cfg { const char* query_env; const char* query_default; int z; vec2 viewport, size, top_left; squeeze_cfg squeeze; }; static const shape_cfg SHAPE_CONFIGS[] = { - // URL Z VIEWPORT SIZE TOPLEFT SQUEEZE - { BANNER_AD_URL, 1, {1920, 1080}, {1920, 324}, {0, 756}, {false, 0.0, ""} }, - { SKYSCRAPER_AD_URL, 1, {1920, 1080}, {480, 1080}, {0, 0}, {false, 0.0, ""} }, - { LSHAPE_RIGHT_AD_URL, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top left"} }, - { LSHAPE_LEFT_AD_URL, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top right"} }, + // QUERY_ENV QUERY_DEFAULT Z VIEWPORT SIZE TOPLEFT SQUEEZE + { "MORPHEUS_BANNER_QUERY", DEFAULT_BANNER_QUERY, 1, {1920, 1080}, {1920, 324}, {0, 756}, {false, 0.0, ""} }, + { "MORPHEUS_SKYSCRAPER_QUERY", DEFAULT_SKYSCRAPER_QUERY, 1, {1920, 1080}, {480, 1080}, {0, 0}, {false, 0.0, ""} }, + { "MORPHEUS_LSHAPE_RIGHT_QUERY", DEFAULT_LSHAPE_RIGHT_QUERY, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top left"} }, + { "MORPHEUS_LSHAPE_LEFT_QUERY", DEFAULT_LSHAPE_LEFT_QUERY, -1, {1.0,1.0}, {1.0,1.0}, {0.0,0.0}, {true, 0.6, "top right"} }, }; +// Build the overlay ad URL for a shape from env vars, falling back to the +// compiled defaults when an env var is unset or empty. +static std::string build_ad_url(const shape_cfg& cfg) { + const char* base = std::getenv("AD_GEN_BASE_URL"); + if (!base || !base[0]) base = DEFAULT_AD_GEN_BASE_URL; + const char* query = std::getenv(cfg.query_env); + if (!query || !query[0]) query = cfg.query_default; + std::string b(base); + // avoid double slash if base ends with '/' + if (!b.empty() && b.back() == '/') b.pop_back(); + return b + "/image/html?" + query; +} + static int parse_shape(const char* upid_text) { char buf[256] = {}; std::strncpy(buf, upid_text, sizeof(buf) - 1); @@ -150,7 +172,8 @@ void morph_overlay(pugi::xml_document& mpddoc) { const shape_cfg& cfg = SHAPE_CONFIGS[oe.shape_idx]; pugi::xml_node overlay = ev.append_child("OverlayEvent"); - overlay.append_attribute("uri").set_value(cfg.url); + std::string ad_url = build_ad_url(cfg); + overlay.append_attribute("uri").set_value(ad_url.c_str()); overlay.append_attribute("mimeType").set_value("text/html"); overlay.append_attribute("earliestResolutionTime").set_value("0"); overlay.append_attribute("loop").set_value("false"); From b4f4d58144edf95b165d2c9404fe3ceef78d0d04 Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Tue, 26 May 2026 13:41:25 -0300 Subject: [PATCH 06/10] modify overlay urls for client side parameters support --- ngx_morpheus_internal.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 107a44f..172f61e 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -102,6 +102,16 @@ struct overlay_ev { void morph_overlay(pugi::xml_document& mpddoc) { pugi::xml_node mpd = mpddoc.child("MPD"); + if (!mpd.find_child_by_attribute("SupplementalProperty", "schemeIdUri", "urn:mpeg:dash:urlparam:2016")) { + pugi::xml_node sp = mpd.prepend_child("SupplementalProperty"); + sp.append_attribute("schemeIdUri").set_value("urn:mpeg:dash:urlparam:2016"); + pugi::xml_node eqi = sp.append_child("up:ExtUrlQueryInfo"); + eqi.append_attribute("xmlns:up").set_value("urn:mpeg:dash:schema:urlparam:2016"); + eqi.append_attribute("useMPDUrlQuery").set_value("true"); + eqi.append_attribute("queryTemplate").set_value("$querypart$"); + eqi.append_attribute("includeInRequests").set_value("urn:scte:dash:scte214-events"); + } + for (pugi::xml_node period : mpd.children("Period")) { pugi::xml_node scte_stream; for (pugi::xml_node es : period.children("EventStream")) { @@ -175,7 +185,7 @@ void morph_overlay(pugi::xml_document& mpddoc) { std::string ad_url = build_ad_url(cfg); overlay.append_attribute("uri").set_value(ad_url.c_str()); overlay.append_attribute("mimeType").set_value("text/html"); - overlay.append_attribute("earliestResolutionTime").set_value("0"); + overlay.append_attribute("earliestResolutionTime").set_value("35000"); overlay.append_attribute("loop").set_value("false"); overlay.append_attribute("mode").set_value("start"); overlay.append_attribute("z").set_value(cfg.z); From a37cb3bd682c6123b0324d33faffd3661de40986 Mon Sep 17 00:00:00 2001 From: Nicolas Levy Date: Mon, 1 Jun 2026 17:19:32 -0300 Subject: [PATCH 07/10] trim compiled default overlay queries to template_id only Personalization params (city, gender, hobbies, ...) no longer belong in the compiled fallback: client-side params now flow in from the MPD URL query via ExtUrlQueryInfo. Keep only the template_id per shape as the default; the rest is supplied per-request by the player. Co-Authored-By: Claude Opus 4.8 (1M context) --- ngx_morpheus_internal.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 172f61e..2fa8153 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -27,11 +27,12 @@ const std::unordered_map MANIFEST_URLS = { #define DEFAULT_AD_GEN_BASE_URL "http://localhost:8888" // Per-shape fallback query strings (everything after "image/html?"), using the -// repo's original template_ids and targeting params. -#define DEFAULT_BANNER_QUERY "template_id=17306275-3762-45f9-9a86-c262b8925963&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic&price=UYU200&favourite_colors=red%2Cblue%2Cblack" -#define DEFAULT_SKYSCRAPER_QUERY "template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203&city=montevideo&gender=male&hobbies=football%2Cmusic&restrictions=diabetic%2Cceliac&favourite_colors=red%2Cblue%2Cblack&favourite_food=chivito" -#define DEFAULT_LSHAPE_RIGHT_QUERY "template_id=7822830e-10ff-449f-a5e6-f92f7899f442&gender=male&country=uruguay&restrictions=celiac&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink" -#define DEFAULT_LSHAPE_LEFT_QUERY "template_id=1ac069f1-0437-4a9d-861b-e29ad552c842&gender=male&country=uruguay&hobbies=gaming%2Cfootball%2Ctravelling&colors=black%2Cpink&age=27&social_media=%40qualabs" +// repo's original template_ids. Personalization params are intentionally not +// hardcoded: they flow in client-side from the MPD URL query (ExtUrlQueryInfo). +#define DEFAULT_BANNER_QUERY "template_id=17306275-3762-45f9-9a86-c262b8925963" +#define DEFAULT_SKYSCRAPER_QUERY "template_id=cda83e2d-0cd9-44f6-b1d5-d9c0c62cc203" +#define DEFAULT_LSHAPE_RIGHT_QUERY "template_id=7822830e-10ff-449f-a5e6-f92f7899f442" +#define DEFAULT_LSHAPE_LEFT_QUERY "template_id=1ac069f1-0437-4a9d-861b-e29ad552c842" static std::string fmt_double(double v) { char buf[32]; From 4be98805ca20fca81b2c247c489938117d65c155 Mon Sep 17 00:00:00 2001 From: Nicolas Levy Date: Mon, 1 Jun 2026 18:03:14 -0300 Subject: [PATCH 08/10] set overlay earliestResolutionTime to 5s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve (prefetch) the overlay ad ~5s before display instead of 35s, so the prefetch→load→show sequence is observable in a short window and the lead stays close to the real fetch/present timing measured in the WG. Co-Authored-By: Claude Opus 4.8 (1M context) --- ngx_morpheus_internal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 2fa8153..8a276d8 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -186,7 +186,7 @@ void morph_overlay(pugi::xml_document& mpddoc) { std::string ad_url = build_ad_url(cfg); overlay.append_attribute("uri").set_value(ad_url.c_str()); overlay.append_attribute("mimeType").set_value("text/html"); - overlay.append_attribute("earliestResolutionTime").set_value("35000"); + overlay.append_attribute("earliestResolutionTime").set_value("5000"); overlay.append_attribute("loop").set_value("false"); overlay.append_attribute("mode").set_value("start"); overlay.append_attribute("z").set_value(cfg.z); From 54de65d461ee83c3d5a542dc10c37737e7c8dbbd Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:15:28 -0300 Subject: [PATCH 09/10] refactor overlay event handling: move urlparam SP to EventStream scope - Remove top-level MPD SupplementalProperty; move urlparam SP onto each overlay EventStream instead so context params are scoped to overlay requests only - If stream-lens already injected a urlparam SP (carrying the fused KV context string), augment it with useMPDUrlQuery=true rather than creating a duplicate SP - Filter EventStreams to only those carrying segmentation type 56/57 - Convert SCTE-35 EventStream to overlay EventStream in-place instead of appending a new stream and removing the original Co-Authored-By: Claude Sonnet 4.6 --- ngx_morpheus_internal.cpp | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 8a276d8..6cbbb31 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -103,16 +103,6 @@ struct overlay_ev { void morph_overlay(pugi::xml_document& mpddoc) { pugi::xml_node mpd = mpddoc.child("MPD"); - if (!mpd.find_child_by_attribute("SupplementalProperty", "schemeIdUri", "urn:mpeg:dash:urlparam:2016")) { - pugi::xml_node sp = mpd.prepend_child("SupplementalProperty"); - sp.append_attribute("schemeIdUri").set_value("urn:mpeg:dash:urlparam:2016"); - pugi::xml_node eqi = sp.append_child("up:ExtUrlQueryInfo"); - eqi.append_attribute("xmlns:up").set_value("urn:mpeg:dash:schema:urlparam:2016"); - eqi.append_attribute("useMPDUrlQuery").set_value("true"); - eqi.append_attribute("queryTemplate").set_value("$querypart$"); - eqi.append_attribute("includeInRequests").set_value("urn:scte:dash:scte214-events"); - } - for (pugi::xml_node period : mpd.children("Period")) { pugi::xml_node scte_stream; for (pugi::xml_node es : period.children("EventStream")) { @@ -124,6 +114,18 @@ void morph_overlay(pugi::xml_document& mpddoc) { } if (!scte_stream) continue; + // Only process EventStreams that carry SCTE-35 segmentation overlay events + bool has_segmentation = false; + for (pugi::xml_node ev : scte_stream.children("Event")) { + pugi::xml_node sec = ev.child("scte35:SpliceInfoSection"); + if (!sec) continue; + pugi::xml_node desc = sec.child("scte35:SegmentationDescriptor"); + if (!desc) continue; + int type_id = desc.attribute("segmentationTypeId").as_int(0); + if (type_id == 56 || type_id == 57) { has_segmentation = true; break; } + } + if (!has_segmentation) continue; + uint64_t timescale = scte_stream.attribute("timescale").as_ullong(1000); std::vector events; @@ -167,12 +169,20 @@ void morph_overlay(pugi::xml_document& mpddoc) { } } - pugi::xml_node new_stream = period.append_child("EventStream"); - new_stream.append_attribute("schemeIdUri").set_value("urn:scte:dash:scte214-events"); - new_stream.append_attribute("timescale").set_value((unsigned long long)timescale); + // Remove SCTE-35 Event children, preserving any SupplementalProperty children + std::vector evts_to_remove; + for (pugi::xml_node ev : scte_stream.children("Event")) { + evts_to_remove.push_back(ev); + } + for (auto& ev : evts_to_remove) { + scte_stream.remove_child(ev); + } + + // Convert to overlay EventStream in place + scte_stream.attribute("schemeIdUri").set_value("urn:scte:dash:scte214-events"); for (const overlay_ev& oe : events) { - pugi::xml_node ev = new_stream.append_child("Event"); + pugi::xml_node ev = scte_stream.append_child("Event"); ev.append_attribute("presentationTime").set_value((unsigned long long)oe.ptime); if (oe.is_start) { @@ -218,7 +228,30 @@ void morph_overlay(pugi::xml_document& mpddoc) { } } - period.remove_child(scte_stream); + // If stream-lens injected a urlparam SP, augment it with useMPDUrlQuery. + // Otherwise create morpheus's own SP. + pugi::xml_node existing_sp; + for (pugi::xml_node sp : scte_stream.children("SupplementalProperty")) { + if (std::strcmp(sp.attribute("schemeIdUri").value(), "urn:mpeg:dash:urlparam:2016") == 0) { + existing_sp = sp; + break; + } + } + + if (existing_sp) { + pugi::xml_node eqi = existing_sp.child("up:ExtUrlQueryInfo"); + if (eqi) { + eqi.append_attribute("useMPDUrlQuery").set_value("true"); + } + } else { + pugi::xml_node sp = scte_stream.append_child("SupplementalProperty"); + sp.append_attribute("schemeIdUri").set_value("urn:mpeg:dash:urlparam:2016"); + pugi::xml_node eqi = sp.append_child("up:ExtUrlQueryInfo"); + eqi.append_attribute("xmlns:up").set_value("urn:mpeg:dash:schema:urlparam:2016"); + eqi.append_attribute("useMPDUrlQuery").set_value("true"); + eqi.append_attribute("queryTemplate").set_value("$querypart$"); + eqi.append_attribute("includeInRequests").set_value("urn:scte:dash:scte214-events"); + } } } From 3b71cca754ea56560629c5a340e22cf1633a8443 Mon Sep 17 00:00:00 2001 From: Luca Benvenuto <20568758+lucabenvenuto@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:39:27 -0300 Subject: [PATCH 10/10] increase overlay earliestResolutionTime to 35s --- ngx_morpheus_internal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_morpheus_internal.cpp b/ngx_morpheus_internal.cpp index 6cbbb31..7d84fdb 100644 --- a/ngx_morpheus_internal.cpp +++ b/ngx_morpheus_internal.cpp @@ -196,7 +196,7 @@ void morph_overlay(pugi::xml_document& mpddoc) { std::string ad_url = build_ad_url(cfg); overlay.append_attribute("uri").set_value(ad_url.c_str()); overlay.append_attribute("mimeType").set_value("text/html"); - overlay.append_attribute("earliestResolutionTime").set_value("5000"); + overlay.append_attribute("earliestResolutionTime").set_value("35000"); overlay.append_attribute("loop").set_value("false"); overlay.append_attribute("mode").set_value("start"); overlay.append_attribute("z").set_value(cfg.z);