static mergeConfigObjects(destination, source, template, overrides, path) {
    goog.asserts.assert(destination, 'Destination config must not be null!');

    // If true, override the template.
    const overrideTemplate = path in overrides;

    // If true, treat the source as a generic object to be copied without
    // descending more deeply.
    let genericObject = false;
    if (overrideTemplate) {
      genericObject = template.constructor == Object &&
        Object.keys(overrides).length == 0;
    } else {
      genericObject = template.constructor == Object &&
        Object.keys(template).length == 0;
    }

    // If true, don't validate the keys in the next level.
    const ignoreKeys = overrideTemplate || genericObject;

    let isValid = true;

    for (const k in source) {
      const subPath = path + '.' + k;
      const subTemplate = overrideTemplate ? overrides[path] : template[k];

      // The order of these checks is important.
      if (!ignoreKeys && !(k in template)) {
        shaka.log.alwaysError('Invalid config, unrecognized key ' + subPath);
        isValid = false;
      } else if (source[k] === undefined) {
        // An explicit 'undefined' value causes the key to be deleted from the
        // destination config and replaced with a default from the template if
        // possible.
        if (subTemplate === undefined || ignoreKeys) {
          // There is nothing in the template, so delete.
          delete destination[k];
        } else {
          // There is something in the template, so go back to that.
          destination[k] = shaka.util.ObjectUtils.cloneObject(subTemplate);
        }
      } else if (genericObject) {
        // Copy the fields of a generic object directly without a template and
        // without descending any deeper.
        destination[k] = source[k];
      } else if (subTemplate.constructor == Object &&
                 source[k] &&
                 source[k].constructor == Object) {
        // These are plain Objects with no other constructor.

        if (!destination[k]) {
          // Initialize the destination with the template so that normal
          // merging and type-checking can happen.
          destination[k] = shaka.util.ObjectUtils.cloneObject(subTemplate);
        }

        const subMergeValid = shaka.util.ConfigUtils.mergeConfigObjects(
            destination[k], source[k], subTemplate, overrides, subPath);
        isValid = isValid && subMergeValid;
      } else if (typeof source[k] != typeof subTemplate ||
                 source[k] == null ||
                 // Function constructors are not informative, and differ
                 // between sync and async functions.  So don't look at
                 // constructor for function types.
                 (typeof source[k] != 'function' &&
                  source[k].constructor != subTemplate.constructor)) {
        // The source is the wrong type.  This check allows objects to be
        // nulled, but does not allow null for any non-object fields.
        shaka.log.alwaysError('Invalid config, wrong type for ' + subPath);
        isValid = false;
      } else if (typeof template[k] == 'function' &&
                 template[k].length != source[k].length) {
        shaka.log.alwaysWarn(
            'Unexpected number of arguments for ' + subPath);
        destination[k] = source[k];
      } else if (Array.isArray(destination[k])) {
        // Make a copy of the input array, so that changes to the source object
        // don't immediately affect the running config.
        // Since everything here is very loosely-typed, use a cast to convince
        // the compiler we're not calling slice() on a potential ArrayBuffer,
        // which would break Tizen.
        destination[k] = /** @type {Array} */(source[k]).slice();
      } else {
        destination[k] = source[k];
      }
    }

    return isValid;
  }
@param {!Object} destination
@param {!Object} source
@param {!Object} template supplies default values
@param {!Object} overrides
  Supplies override type checking.  When the current path matches
  the key in this object, each sub-value must match the type in this
  object. If this contains an Object, it is used as the template.
@param {string} path to this part of the config
@return {boolean}
@export