/*! @license
* Shaka Player
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
// cspell:ignore subk subv
goog.provide('shaka.dash.JsonUtils');
/**
* @summary JSON processing utility functions.
*/
shaka.dash.JsonUtils = class {
/**
* Converts a DASH-JSON manifest (dash-json-schema) into an MPD XML string.
*
* @param {!Object} json - Root JSON object following dash-json-schema.
* @return {string}
*/
static jsonToMpd(json) {
const MPD_NS = 'urn:mpeg:dash:schema:mpd:2011';
/**
* Escape XML special chars.
* @param {*} s
* @return {string}
*/
function esc(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
/**
* Emit a tag with attributes and content.
*
* @param {string} tagName
* @param {!Object<string, ?>} attrs
* @param {string} inner
* @return {string}
*/
function emit(tagName, attrs, inner) {
let out = '<' + tagName;
for (const [k, v] of Object.entries(attrs)) {
out += ` ${k}="${esc(v)}"`;
}
if (inner === '') {
return out + '/>';
}
return out + '>' + inner + `</${tagName}>`;
}
/**
* Recursively build XML for a JSON node.
*
* @param {string} key Element name, WITHOUT prefix
* @param {*} node Value
* @param {!Object} nsMap Map prefix->URI
* @return {string}
*/
function buildNode(key, node, nsMap) {
if (node == null) {
return emit(key, {}, '');
}
// Primitive → <key>value</key>
if (typeof node !== 'object') {
return emit(key, {}, esc(String(node)));
}
// Object case
const attrs = {};
let textContent = '';
const children = [];
// Handle prefix groups (namespaced stuff)
const obj = /** @type {!Object<string,?>} */ (node);
for (const [k, v] of Object.entries(obj)) {
if (k === '$value' || k === '$ns') {
continue;
}
const isPrefixGroup = nsMap[k] !== undefined;
if (isPrefixGroup && typeof v === 'object' && v != null) {
const prefix = k;
const groupObj = v;
for (const [subk, subv] of Object.entries(groupObj)) {
if (Array.isArray(subv)) {
// Namespaced repeating child elements <prefix:subk>
for (const item of subv) {
const tag = `${prefix}:${subk}`;
children.push(buildNode(tag, item, nsMap));
}
} else if (typeof subv === 'object') {
// Single namespaced child element
const tag = `${prefix}:${subk}`;
children.push(buildNode(tag, subv, nsMap));
} else {
// Namespaced attribute prefix:attr
attrs[`${prefix}:${subk}`] = subv;
}
}
continue;
}
// Regular attribute / child element
const val = v;
const isObj = typeof val === 'object' && val != null;
if (!isObj) {
// scalar → attribute
attrs[k] = val;
} else {
// object or array → child
if (Array.isArray(val)) {
for (const item of val) {
children.push(buildNode(k, item, nsMap));
}
} else {
children.push(buildNode(k, val, nsMap));
}
}
}
if (node['$value'] !== undefined && node['$value'] !== null) {
textContent = esc(node['$value']);
}
return emit(key, attrs, textContent + children.join(''));
}
// ------------------------------------------------------------------
// Build namespace map prefix→URI from $ns
// ------------------------------------------------------------------
const nsMap = {};
if (json['$ns'] && typeof json['$ns'] === 'object') {
for (const [uri, info] of Object.entries(json['$ns'])) {
const prefix = info.prefix;
nsMap[prefix] = uri;
}
}
// ------------------------------------------------------------------
// Build root attributes: xmlns + xmlns:pref + scalar attrs at root
// ------------------------------------------------------------------
const rootAttrs = {
'xmlns': MPD_NS,
};
// nsMap: prefix→uri
for (const [prefix, uri] of Object.entries(nsMap)) {
rootAttrs[`xmlns:${prefix}`] = uri;
}
// Non-object root attributes
for (const [k, v] of Object.entries(json)) {
if (k === '$ns') {
continue;
}
if (typeof v !== 'object' || v === null) {
rootAttrs[k] = v;
}
}
// ------------------------------------------------------------------
// Build children of <MPD>
// ------------------------------------------------------------------
let mpdInner = '';
for (const [k, v] of Object.entries(json)) {
if (k === '$ns') {
continue;
}
if (k in nsMap) {
// Namespace group
const groupObj = /** @type {!Object<string,?>} */ (v);
for (const [subk, subv] of Object.entries(groupObj)) {
if (Array.isArray(subv)) {
for (const item of subv) {
const tag = `${k}:${subk}`;
mpdInner += buildNode(tag, item, nsMap);
}
} else if (typeof subv === 'object') {
const tag = `${k}:${subk}`;
mpdInner += buildNode(tag, subv, nsMap);
} else {
// namespaced attribute on root? (rare)
rootAttrs[`${k}:${subk}`] = subv;
}
}
continue;
}
if (typeof v === 'object' && v !== null) {
if (Array.isArray(v)) {
for (const item of v) {
mpdInner += buildNode(k, item, nsMap);
}
} else {
mpdInner += buildNode(k, v, nsMap);
}
}
}
return emit('MPD', rootAttrs, mpdInner);
}
};