onKeyStatusesChange_(event) {
    const session = /** @type {!MediaKeySession} */(event.target);
    shaka.log.v2('Key status changed for session', session.sessionId);

    const found = this.activeSessions_.get(session);
    const keyStatusMap = session.keyStatuses;
    let hasExpiredKeys = false;

    keyStatusMap.forEach((status, keyId) => {
      // The spec has changed a few times on the exact order of arguments here.
      // As of 2016-06-30, Edge has the order reversed compared to the current
      // EME spec.  Given the back and forth in the spec, it may not be the only
      // one.  Try to detect this and compensate:
      if (typeof keyId == 'string') {
        const tmp = keyId;
        keyId = /** @type {!ArrayBuffer} */(status);
        status = /** @type {string} */(tmp);
      }

      // Microsoft's implementation in Edge seems to present key IDs as
      // little-endian UUIDs, rather than big-endian or just plain array of
      // bytes.
      // standard: 6e 5a 1d 26 - 27 57 - 47 d7 - 80 46 ea a5 d1 d3 4b 5a
      // on Edge:  26 1d 5a 6e - 57 27 - d7 47 - 80 46 ea a5 d1 d3 4b 5a
      // Bug filed: https://bit.ly/2thuzXu

      // NOTE that we skip this if byteLength != 16.  This is used for Edge
      // which uses single-byte dummy key IDs.
      // However, unlike Edge and Chromecast, Tizen doesn't have this problem.
      const device = shaka.device.DeviceFactory.getDevice();
      if (shaka.drm.DrmUtils.isPlayReadyKeySystem(
          this.currentDrmInfo_.keySystem) &&
          keyId.byteLength == 16 &&
          device.returnLittleEndianUsingPlayReady()) {
        // Read out some fields in little-endian:
        const dataView = shaka.util.BufferUtils.toDataView(keyId);
        const part0 = dataView.getUint32(0, /* LE= */ true);
        const part1 = dataView.getUint16(4, /* LE= */ true);
        const part2 = dataView.getUint16(6, /* LE= */ true);
        // Write it back in big-endian:
        dataView.setUint32(0, part0, /* BE= */ false);
        dataView.setUint16(4, part1, /* BE= */ false);
        dataView.setUint16(6, part2, /* BE= */ false);
      }

      if (status != 'status-pending') {
        found.loaded = true;
      }

      if (!found) {
        // We can get a key status changed for a closed session after it has
        // been removed from |activeSessions_|.  If it is closed, none of its
        // keys should be usable.
        goog.asserts.assert(
            status != 'usable', 'Usable keys found in closed session');
      }

      if (status == 'expired') {
        hasExpiredKeys = true;
      }

      const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId).slice(0, 32);

      this.keyStatusByKeyId_.set(keyIdHex, status);
    });

    // If the session has expired, close it.
    // Some CDMs do not have sub-second time resolution, so the key status may
    // fire with hundreds of milliseconds left until the stated expiration time.
    const msUntilExpiration = session.expiration - Date.now();
    if (msUntilExpiration < 0 || (hasExpiredKeys && msUntilExpiration < 1000)) {
      // If this is part of a remove(), we don't want to close the session until
      // the update is complete.  Otherwise, we will orphan the session.
      if (found && !found.updatePromise) {
        shaka.log.debug('Session has expired', session.sessionId);
        this.activeSessions_.delete(session);
        this.closeSession_(session);
      }
    }

    if (!this.areAllSessionsLoaded_()) {
      // Don't announce key statuses or resolve the "all loaded" promise until
      // everything is loaded.
      return;
    }

    this.allSessionsLoaded_.resolve();

    // Batch up key status changes before checking them or notifying Player.
    // This handles cases where the statuses of multiple sessions are set
    // simultaneously by the browser before dispatching key status changes for
    // each of them.  By batching these up, we only send one status change event
    // and at most one EXPIRED error on expiration.
    this.keyStatusTimer_.tickAfter(
        /* seconds= */ shaka.drm.DrmEngine.KEY_STATUS_BATCH_TIME);
  }
@param {!Event} event
@private
@suppress {invalidCasts} to swap keyId and status