UNPKG

videojs-contrib-eme

Version:

Supports Encrypted Media Extensions for playback of encrypted content in Video.js

1,654 lines 53.8 kB
/*! @name videojs-contrib-eme @version 5.5.0 @license Apache-2.0 */
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var videojs = require('video.js');
var window = require('global/window');
var _extends = require('@babel/runtime/helpers/extends');
var document = require('global/document');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
var document__default = /*#__PURE__*/_interopDefaultLegacy(document);

const stringToUint16Array = string => {
  // 2 bytes for each char
  const buffer = new ArrayBuffer(string.length * 2);
  const array = new Uint16Array(buffer);

  for (let i = 0; i < string.length; i++) {
    array[i] = string.charCodeAt(i);
  }

  return array;
};
const uint8ArrayToString = array => {
  return String.fromCharCode.apply(null, new Uint8Array(array.buffer || array));
};
const uint16ArrayToString = array => {
  return String.fromCharCode.apply(null, new Uint16Array(array.buffer || array));
};
const getHostnameFromUri = uri => {
  const link = document__default["default"].createElement('a');
  link.href = uri;
  return link.hostname;
};
const arrayBuffersEqual = (arrayBuffer1, arrayBuffer2) => {
  if (arrayBuffer1 === arrayBuffer2) {
    return true;
  }

  if (arrayBuffer1.byteLength !== arrayBuffer2.byteLength) {
    return false;
  }

  const dataView1 = new DataView(arrayBuffer1);
  const dataView2 = new DataView(arrayBuffer2);

  for (let i = 0; i < dataView1.byteLength; i++) {
    if (dataView1.getUint8(i) !== dataView2.getUint8(i)) {
      return false;
    }
  }

  return true;
};
const arrayBufferFrom = bufferOrTypedArray => {
  if (bufferOrTypedArray instanceof Uint8Array || bufferOrTypedArray instanceof Uint16Array) {
    return bufferOrTypedArray.buffer;
  }

  return bufferOrTypedArray;
}; // Normalize between Video.js 6/7 (videojs.mergeOptions) and 8 (videojs.obj.merge).

const merge = (...args) => {
  const context = videojs__default["default"].obj || videojs__default["default"];
  const fn = context.merge || context.mergeOptions;
  return fn.apply(context, args);
};
const mergeAndRemoveNull = (...args) => {
  const result = merge(...args); // Any header whose value is `null` will be removed.

  Object.keys(result).forEach(k => {
    if (result[k] === null) {
      delete result[k];
    }
  });
  return result;
};
/**
 * Transforms the keySystems object into a MediaKeySystemConfiguration Object array.
 *
 * @param {Object} keySystems object from the options.
 * @return {Array} of MediaKeySystemConfiguration objects.
 */

const getMediaKeySystemConfigurations = keySystems => {
  const config = [];
  Object.keys(keySystems).forEach(keySystem => {
    const mediaKeySystemConfig = getSupportedConfigurations(keySystem, keySystems[keySystem])[0];
    config.push(mediaKeySystemConfig);
  });
  return config;
};

let httpResponseHandler = videojs__default["default"].xhr.httpHandler; // to make sure this doesn't break with older versions of Video.js,
// do a super simple wrapper instead

if (!httpResponseHandler) {
  httpResponseHandler = (callback, decodeResponseBody) => (err, response, responseBody) => {
    if (err) {
      callback(err);
      return;
    } // if the HTTP status code is 4xx or 5xx, the request also failed


    if (response.statusCode >= 400 && response.statusCode <= 599) {
      let cause = responseBody;

      if (decodeResponseBody) {
        cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
      }

      callback({
        cause
      });
      return;
    } // otherwise, request succeeded


    callback(null, responseBody);
  };
}

/**
 * Parses the EME key message XML to extract HTTP headers and the Challenge element to use
 * in the PlayReady license request.
 *
 * @param {ArrayBuffer} message key message from EME
 * @return {Object} an object containing headers and the message body to use in the
 * license request
 */

const getMessageContents = message => {
  // TODO do we want to support UTF-8?
  const xmlString = String.fromCharCode.apply(null, new Uint16Array(message));
  const xml = new window__default["default"].DOMParser().parseFromString(xmlString, 'application/xml');
  const headersElement = xml.getElementsByTagName('HttpHeaders')[0];
  let headers = {};

  if (headersElement) {
    const headerNames = headersElement.getElementsByTagName('name');
    const headerValues = headersElement.getElementsByTagName('value');

    for (let i = 0; i < headerNames.length; i++) {
      headers[headerNames[i].childNodes[0].nodeValue] = headerValues[i].childNodes[0].nodeValue;
    }
  }

  const challengeElement = xml.getElementsByTagName('Challenge')[0];
  let challenge;

  if (challengeElement) {
    challenge = window__default["default"].atob(challengeElement.childNodes[0].nodeValue);
  } // If we failed to parse the xml the soap message might be encoded already.
  // set the message data as the challenge and add generic SOAP headers.


  if (xml.querySelector('parsererror')) {
    headers = {
      'Content-Type': 'text/xml; charset=utf-8',
      'SOAPAction': '"http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"'
    };
    challenge = message;
  }

  return {
    headers,
    message: challenge
  };
};
const requestPlayreadyLicense = (keySystem, keySystemOptions, messageBuffer, emeOptions, callback) => {
  const messageContents = getMessageContents(messageBuffer);
  const message = messageContents.message;
  const headers = mergeAndRemoveNull(messageContents.headers, emeOptions.emeHeaders, keySystemOptions.licenseHeaders);
  videojs__default["default"].xhr({
    uri: keySystemOptions.url,
    method: 'post',
    headers,
    body: message,
    responseType: 'arraybuffer',
    requestType: 'license',
    metadata: {
      keySystem
    }
  }, httpResponseHandler(callback, true));
};

const Error$1 = {
  EMEFailedToRequestMediaKeySystemAccess: 'eme-failed-request-media-key-system-access',
  EMEFailedToCreateMediaKeys: 'eme-failed-create-media-keys',
  EMEFailedToAttachMediaKeysToVideoElement: 'eme-failed-attach-media-keys-to-video',
  EMEFailedToCreateMediaKeySession: 'eme-failed-create-media-key-session',
  EMEFailedToSetServerCertificate: 'eme-failed-set-server-certificate',
  EMEFailedToGenerateLicenseRequest: 'eme-failed-generate-license-request',
  EMEFailedToUpdateSessionWithReceivedLicenseKeys: 'eme-failed-update-session',
  EMEFailedToCloseSession: 'eme-failed-close-session',
  EMEFailedToRemoveKeysFromSession: 'eme-failed-remove-keys',
  EMEFailedToLoadSessionBySessionId: 'eme-failed-load-session'
};

/**
 * The W3C Working Draft of 22 October 2013 seems to be the best match for
 * the ms-prefixed API. However, it should only be used as a guide; it is
 * doubtful the spec is 100% implemented as described.
 *
 * @see https://www.w3.org/TR/2013/WD-encrypted-media-20131022
 */
const LEGACY_FAIRPLAY_KEY_SYSTEM = 'com.apple.fps.1_0';

const concatInitDataIdAndCertificate = ({
  initData,
  id,
  cert
}) => {
  if (typeof id === 'string') {
    id = stringToUint16Array(id);
  } // layout:
  //   [initData]
  //   [4 byte: idLength]
  //   [idLength byte: id]
  //   [4 byte:certLength]
  //   [certLength byte: cert]


  let offset = 0;
  const buffer = new ArrayBuffer(initData.byteLength + 4 + id.byteLength + 4 + cert.byteLength);
  const dataView = new DataView(buffer);
  const initDataArray = new Uint8Array(buffer, offset, initData.byteLength);
  initDataArray.set(initData);
  offset += initData.byteLength;
  dataView.setUint32(offset, id.byteLength, true);
  offset += 4;
  const idArray = new Uint16Array(buffer, offset, id.length);
  idArray.set(id);
  offset += idArray.byteLength;
  dataView.setUint32(offset, cert.byteLength, true);
  offset += 4;
  const certArray = new Uint8Array(buffer, offset, cert.byteLength);
  certArray.set(cert);
  return new Uint8Array(buffer, 0, buffer.byteLength);
};

const addKey = ({
  video,
  contentId,
  initData,
  cert,
  options,
  getLicense,
  eventBus,
  emeError
}) => {
  return new Promise((resolve, reject) => {
    if (!video.webkitKeys) {
      try {
        video.webkitSetMediaKeys(new window__default["default"].WebKitMediaKeys(LEGACY_FAIRPLAY_KEY_SYSTEM));
      } catch (error) {
        const metadata = {
          errorType: Error$1.EMEFailedToCreateMediaKeys,
          keySystem: LEGACY_FAIRPLAY_KEY_SYSTEM
        };
        emeError(error, metadata);
        reject('Could not create MediaKeys');
        return;
      }
    }

    let keySession;

    try {
      keySession = video.webkitKeys.createSession('video/mp4', concatInitDataIdAndCertificate({
        id: contentId,
        initData,
        cert
      }));
    } catch (error) {
      const metadata = {
        errorType: Error$1.EMEFailedToCreateMediaKeySession,
        keySystem: LEGACY_FAIRPLAY_KEY_SYSTEM
      };
      emeError(error, metadata);
      reject('Could not create key session');
      return;
    }

    safeTriggerOnEventBus(eventBus, {
      type: 'keysessioncreated',
      keySession
    });
    keySession.contentId = contentId;
    keySession.addEventListener('webkitkeymessage', event => {
      safeTriggerOnEventBus(eventBus, {
        type: 'keymessage',
        messageEvent: event
      });
      getLicense(options, contentId, event.message, (err, license) => {
        if (eventBus) {
          safeTriggerOnEventBus(eventBus, {
            type: 'licenserequestattempted'
          });
        }

        if (err) {
          const metadata = {
            errortype: Error$1.EMEFailedToGenerateLicenseRequest,
            keySystem: LEGACY_FAIRPLAY_KEY_SYSTEM
          };
          emeError(err, metadata);
          reject(err);
          return;
        }

        keySession.update(new Uint8Array(license));
        safeTriggerOnEventBus(eventBus, {
          type: 'keysessionupdated',
          keySession
        });
      });
    });
    keySession.addEventListener('webkitkeyadded', () => {
      resolve();
    }); // for testing purposes, adding webkitkeyerror must be the last item in this method

    keySession.addEventListener('webkitkeyerror', () => {
      const error = keySession.error;
      const metadata = {
        errorType: Error$1.EMEFailedToUpdateSessionWithReceivedLicenseKeys,
        keySystem: LEGACY_FAIRPLAY_KEY_SYSTEM
      };
      emeError(error, metadata);
      reject(`KeySession error: code ${error.code}, systemCode ${error.systemCode}`);
    });
  });
};

const defaultGetCertificate = (keySystem, fairplayOptions) => {
  return (emeOptions, callback) => {
    const headers = mergeAndRemoveNull(emeOptions.emeHeaders, fairplayOptions.certificateHeaders);
    videojs__default["default"].xhr({
      uri: fairplayOptions.certificateUri,
      responseType: 'arraybuffer',
      requestType: 'license',
      metadata: {
        keySystem
      },
      headers
    }, httpResponseHandler((err, license) => {
      if (err) {
        callback(err);
        return;
      } // in this case, license is still the raw ArrayBuffer,
      // (we don't want httpResponseHandler to decode it)
      // convert it into Uint8Array as expected


      callback(null, new Uint8Array(license));
    }));
  };
};
const defaultGetContentId = (emeOptions, initDataString) => {
  return getHostnameFromUri(initDataString);
};
const defaultGetLicense$1 = (keySystem, fairplayOptions) => {
  return (emeOptions, contentId, keyMessage, callback) => {
    const headers = mergeAndRemoveNull({
      'Content-type': 'application/octet-stream'
    }, emeOptions.emeHeaders, fairplayOptions.licenseHeaders);
    videojs__default["default"].xhr({
      uri: fairplayOptions.licenseUri || fairplayOptions.url,
      method: 'POST',
      responseType: 'arraybuffer',
      requestType: 'license',
      metadata: {
        keySystem,
        contentId
      },
      body: keyMessage,
      headers
    }, httpResponseHandler(callback, true));
  };
};

const fairplay = ({
  video,
  initData,
  options,
  eventBus,
  emeError
}) => {
  const fairplayOptions = options.keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM];
  const getCertificate = fairplayOptions.getCertificate || defaultGetCertificate(LEGACY_FAIRPLAY_KEY_SYSTEM, fairplayOptions);
  const getContentId = fairplayOptions.getContentId || defaultGetContentId;
  const getLicense = fairplayOptions.getLicense || defaultGetLicense$1(LEGACY_FAIRPLAY_KEY_SYSTEM, fairplayOptions);
  return new Promise((resolve, reject) => {
    getCertificate(options, (err, cert) => {
      if (err) {
        const metadata = {
          errorType: Error$1.EMEFailedToSetServerCertificate,
          keySystem: LEGACY_FAIRPLAY_KEY_SYSTEM
        };
        emeError(err, metadata);
        reject(err);
        return;
      }

      resolve(cert);
    });
  }).then(cert => {
    return addKey({
      video,
      cert,
      initData,
      getLicense,
      options,
      contentId: getContentId(options, uint16ArrayToString(initData)),
      eventBus,
      emeError
    });
  });
};

const isFairplayKeySystem = str => str.startsWith('com.apple.fps');
/**
 * Trigger an event on the event bus component safely.
 *
 * This is used because there are cases where we can see race conditions
 * between asynchronous operations (like closing a key session) and the
 * availability of the event bus's DOM element.
 *
 * @param  {Component} eventBus
 * @param  {...} args
 */


const safeTriggerOnEventBus = (eventBus, args) => {
  if (eventBus.isDisposed()) {
    return;
  }

  eventBus.trigger(_extends__default["default"]({}, args));
};
/**
 * Returns an array of MediaKeySystemConfigurationObjects provided in the keySystem
 * options.
 *
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemconfiguration|MediaKeySystemConfigurationObject}
 *
 * @param {Object} keySystemOptions
 *        Options passed into videojs-contrib-eme for a specific keySystem
 * @return {Object[]}
 *         Array of MediaKeySystemConfigurationObjects
 */

const getSupportedConfigurations = (keySystem, keySystemOptions) => {
  if (keySystemOptions.supportedConfigurations) {
    return keySystemOptions.supportedConfigurations;
  }

  const isFairplay = isFairplayKeySystem(keySystem);
  const supportedConfiguration = {};
  const initDataTypes = keySystemOptions.initDataTypes || ( // fairplay requires an explicit initDataTypes
  isFairplay ? ['sinf'] : null);
  const audioContentType = keySystemOptions.audioContentType;
  const audioRobustness = keySystemOptions.audioRobustness;
  const videoContentType = keySystemOptions.videoContentType || ( // fairplay requires an explicit videoCapabilities/videoContentType
  isFairplay ? 'video/mp4' : null);
  const videoRobustness = keySystemOptions.videoRobustness;
  const persistentState = keySystemOptions.persistentState;

  if (audioContentType || audioRobustness) {
    supportedConfiguration.audioCapabilities = [_extends__default["default"]({}, audioContentType ? {
      contentType: audioContentType
    } : {}, audioRobustness ? {
      robustness: audioRobustness
    } : {})];
  }

  if (videoContentType || videoRobustness) {
    supportedConfiguration.videoCapabilities = [_extends__default["default"]({}, videoContentType ? {
      contentType: videoContentType
    } : {}, videoRobustness ? {
      robustness: videoRobustness
    } : {})];
  }

  if (persistentState) {
    supportedConfiguration.persistentState = persistentState;
  }

  if (initDataTypes) {
    supportedConfiguration.initDataTypes = initDataTypes;
  }

  return [supportedConfiguration];
};
const getSupportedKeySystem = keySystems => {
  // As this happens after the src is set on the video, we rely only on the set src (we
  // do not change src based on capabilities of the browser in this plugin).
  let promise;
  Object.keys(keySystems).forEach(keySystem => {
    const supportedConfigurations = getSupportedConfigurations(keySystem, keySystems[keySystem]);

    if (!promise) {
      promise = window__default["default"].navigator.requestMediaKeySystemAccess(keySystem, supportedConfigurations);
    } else {
      promise = promise.catch(e => window__default["default"].navigator.requestMediaKeySystemAccess(keySystem, supportedConfigurations));
    }
  });
  return promise;
};
const makeNewRequest = (player, requestOptions) => {
  const {
    mediaKeys,
    initDataType,
    initData,
    options,
    getLicense,
    removeSession,
    eventBus,
    contentId,
    emeError,
    keySystem
  } = requestOptions;

  try {
    const keySession = mediaKeys.createSession();
    safeTriggerOnEventBus(eventBus, {
      type: 'keysessioncreated',
      keySession
    });
    player.on('dispose', () => {
      keySession.close().then(() => {
        safeTriggerOnEventBus(eventBus, {
          type: 'keysessionclosed',
          keySession
        });
      }).catch(error => {
        const metadata = {
          errorType: Error$1.EMEFailedToCloseSession,
          keySystem
        };
        emeError(error, metadata);
      });
    });
    return new Promise((resolve, reject) => {
      keySession.addEventListener('message', event => {
        safeTriggerOnEventBus(eventBus, {
          type: 'keymessage',
          messageEvent: event
        }); // all other types will be handled by keystatuseschange

        if (event.messageType !== 'license-request' && event.messageType !== 'license-renewal') {
          return;
        }

        getLicense(options, event.message, contentId).then(license => {
          resolve(keySession.update(license).then(() => {
            safeTriggerOnEventBus(eventBus, {
              type: 'keysessionupdated',
              keySession
            });
          }).catch(error => {
            const metadata = {
              errorType: Error$1.EMEFailedToUpdateSessionWithReceivedLicenseKeys,
              keySystem
            };
            emeError(error, metadata);
          }));
        }).catch(err => {
          reject(err);
        });
      }, false);
      const KEY_STATUSES_CHANGE = 'keystatuseschange';
      keySession.addEventListener(KEY_STATUSES_CHANGE, event => {
        let expired = false; // Protect from race conditions causing the player to be disposed.

        if (eventBus.isDisposed()) {
          return;
        } // Re-emit the keystatuseschange event with the entire keyStatusesMap


        safeTriggerOnEventBus(eventBus, {
          type: KEY_STATUSES_CHANGE,
          keyStatuses: keySession.keyStatuses
        }); // Keep 'keystatuschange' for backward compatibility.
        // based on https://www.w3.org/TR/encrypted-media/#example-using-all-events

        keySession.keyStatuses.forEach((status, keyId) => {
          // Trigger an event so that outside listeners can take action if appropriate.
          // For instance, the `output-restricted` status should result in an
          // error being thrown.
          safeTriggerOnEventBus(eventBus, {
            keyId,
            status,
            target: keySession,
            type: 'keystatuschange'
          });

          switch (status) {
            case 'expired':
              // If one key is expired in a session, all keys are expired. From
              // https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-expired, "All other
              // keys in the session must have this status."
              expired = true;
              break;

            case 'internal-error':
              const message = 'Key status reported as "internal-error." Leaving the session open since we ' + 'don\'t have enough details to know if this error is fatal.'; // "This value is not actionable by the application."
              // https://www.w3.org/TR/encrypted-media/#dom-mediakeystatus-internal-error

              videojs__default["default"].log.warn(message, event);
              break;
          }
        });

        if (expired) {
          // Close session and remove it from the session list to ensure that a new
          // session can be created.
          videojs__default["default"].log.debug('Session expired, closing the session.');
          keySession.close().then(() => {
            // Because close() is async, this promise could resolve after the
            // player has been disposed.
            if (eventBus.isDisposed()) {
              return;
            }

            safeTriggerOnEventBus(eventBus, {
              type: 'keysessionclosed',
              keySession
            });
            removeSession(initData);
            makeNewRequest(player, requestOptions);
          }).catch(error => {
            const metadata = {
              errorType: Error$1.EMEFailedToCloseSession,
              keySystem
            };
            emeError(error, metadata);
          });
        }
      }, false);
      keySession.generateRequest(initDataType, initData).catch(error => {
        const metadata = {
          errorType: Error$1.EMEFailedToGenerateLicenseRequest,
          keySystem
        };
        emeError(error, metadata);
        reject('Unable to create or initialize key session');
      });
    });
  } catch (error) {
    const metadata = {
      errorType: Error$1.EMEFailedToCreateMediaKeySession,
      keySystem
    };
    emeError(error, metadata);
  }
};
/*
 * Creates a new media key session if media keys are available, otherwise queues the
 * session creation for when the media keys are available.
 *
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeysession|MediaKeySession}
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeys|MediaKeys}
 *
 * @function addSession
 * @param {Object} video
 *        Target video element
 * @param {string} initDataType
 *        The type of init data provided
 * @param {Uint8Array} initData
 *        The media's init data
 * @param {Object} options
 *        Options provided to the plugin for this key system
 * @param {function()} [getLicense]
 *        User provided function to retrieve a license
 * @param {function()} removeSession
 *        Function to remove the persisted session on key expiration so that a new session
 *        may be created
 * @param {Object} eventBus
 *        Event bus for any events pertinent to users
 * @return {Promise}
 *         A resolved promise if session is waiting for media keys, or a promise for the
 *         session creation if media keys are available
 */

const addSession = ({
  player,
  video,
  initDataType,
  initData,
  options,
  getLicense,
  contentId,
  removeSession,
  eventBus,
  emeError
}) => {
  const sessionData = {
    initDataType,
    initData,
    options,
    getLicense,
    removeSession,
    eventBus,
    contentId,
    emeError,
    keySystem: video.keySystem
  };

  if (video.mediaKeysObject) {
    sessionData.mediaKeys = video.mediaKeysObject;
    return makeNewRequest(player, sessionData);
  }

  video.pendingSessionData.push(sessionData);
  return Promise.resolve();
};
/*
 * Given media keys created from a key system access object, check for any session data
 * that was queued and create new sessions for each.
 *
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeysystemaccess|MediaKeySystemAccess}
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeysession|MediaKeySession}
 * @see {@link https://www.w3.org/TR/encrypted-media/#dom-mediakeys|MediaKeys}
 *
 * @function addPendingSessions
 * @param {Object} video
 *        Target video element
 * @param {string} [certificate]
 *        The server certificate (if used)
 * @param {Object} createdMediaKeys
 *        Media keys to use for session creation
 * @return {Promise}
 *         A promise containing new session creations and setting of media keys on the
 *         video object
 */

const addPendingSessions = ({
  player,
  video,
  certificate,
  createdMediaKeys,
  emeError
}) => {
  // save media keys on the video element to act as a reference for other functions so
  // that they don't recreate the keys
  video.mediaKeysObject = createdMediaKeys;
  const promises = [];

  if (certificate) {
    promises.push(createdMediaKeys.setServerCertificate(certificate).catch(error => {
      const metadata = {
        errorType: Error$1.EMEFailedToSetServerCertificate,
        keySystem: video.keySystem
      };
      emeError(error, metadata);
    }));
  }

  for (let i = 0; i < video.pendingSessionData.length; i++) {
    const data = video.pendingSessionData[i];
    promises.push(makeNewRequest(player, {
      mediaKeys: video.mediaKeysObject,
      initDataType: data.initDataType,
      initData: data.initData,
      options: data.options,
      getLicense: data.getLicense,
      removeSession: data.removeSession,
      eventBus: data.eventBus,
      contentId: data.contentId,
      emeError: data.emeError,
      keySystem: video.keySystem
    }));
  }

  video.pendingSessionData = [];
  promises.push(video.setMediaKeys(createdMediaKeys).catch(error => {
    const metadata = {
      errorType: Error$1.EMEFailedToAttachMediaKeysToVideoElement,
      keySystem: video.keySystem
    };
    emeError(error, metadata);
  }));
  return Promise.all(promises);
};

const defaultPlayreadyGetLicense = (keySystem, keySystemOptions) => (emeOptions, keyMessage, callback) => {
  requestPlayreadyLicense(keySystem, keySystemOptions, keyMessage, emeOptions, callback);
};

const defaultGetLicense = (keySystem, keySystemOptions) => (emeOptions, keyMessage, callback) => {
  const headers = mergeAndRemoveNull({
    'Content-type': 'application/octet-stream'
  }, emeOptions.emeHeaders, keySystemOptions.licenseHeaders);
  videojs__default["default"].xhr({
    uri: keySystemOptions.url,
    method: 'POST',
    responseType: 'arraybuffer',
    requestType: 'license',
    metadata: {
      keySystem
    },
    body: keyMessage,
    headers
  }, httpResponseHandler(callback, true));
};

const promisifyGetLicense = (keySystem, getLicenseFn, eventBus) => {
  return (emeOptions, keyMessage, contentId) => {
    return new Promise((resolve, reject) => {
      const callback = function (err, license) {
        if (eventBus) {
          safeTriggerOnEventBus(eventBus, {
            type: 'licenserequestattempted'
          });
        }

        if (err) {
          reject(err);
          return;
        }

        resolve(license);
      };

      if (isFairplayKeySystem(keySystem)) {
        getLicenseFn(emeOptions, contentId, new Uint8Array(keyMessage), callback);
      } else {
        getLicenseFn(emeOptions, keyMessage, callback);
      }
    });
  };
};

const standardizeKeySystemOptions = (keySystem, keySystemOptions) => {
  if (typeof keySystemOptions === 'string') {
    keySystemOptions = {
      url: keySystemOptions
    };
  }

  if (!keySystemOptions.url && keySystemOptions.licenseUri) {
    keySystemOptions.url = keySystemOptions.licenseUri;
  }

  if (!keySystemOptions.url && !keySystemOptions.getLicense) {
    throw new Error(`Missing url/licenseUri or getLicense in ${keySystem} keySystem configuration.`);
  }

  const isFairplay = isFairplayKeySystem(keySystem);

  if (isFairplay && keySystemOptions.certificateUri && !keySystemOptions.getCertificate) {
    keySystemOptions.getCertificate = defaultGetCertificate(keySystem, keySystemOptions);
  }

  if (isFairplay && !keySystemOptions.getCertificate) {
    throw new Error(`Missing getCertificate or certificateUri in ${keySystem} keySystem configuration.`);
  }

  if (isFairplay && !keySystemOptions.getContentId) {
    keySystemOptions.getContentId = defaultGetContentId;
  }

  if (keySystemOptions.url && !keySystemOptions.getLicense) {
    if (keySystem === 'com.microsoft.playready') {
      keySystemOptions.getLicense = defaultPlayreadyGetLicense(keySystem, keySystemOptions);
    } else if (isFairplay) {
      keySystemOptions.getLicense = defaultGetLicense$1(keySystem, keySystemOptions);
    } else {
      keySystemOptions.getLicense = defaultGetLicense(keySystem, keySystemOptions);
    }
  }

  return keySystemOptions;
};

const standard5July2016 = ({
  player,
  video,
  initDataType,
  initData,
  keySystemAccess,
  options,
  removeSession,
  eventBus,
  emeError
}) => {
  let keySystemPromise = Promise.resolve();
  const keySystem = keySystemAccess.keySystem;
  let keySystemOptions; // try catch so that we return a promise rejection

  try {
    keySystemOptions = standardizeKeySystemOptions(keySystem, options.keySystems[keySystem]);
  } catch (e) {
    return Promise.reject(e);
  }

  const contentId = keySystemOptions.getContentId ? keySystemOptions.getContentId(options, uint8ArrayToString(initData)) : null;

  if (typeof video.mediaKeysObject === 'undefined') {
    // Prevent entering this path again.
    video.mediaKeysObject = null; // Will store all initData until the MediaKeys is ready.

    video.pendingSessionData = [];
    let certificate;
    keySystemPromise = new Promise((resolve, reject) => {
      // save key system for adding sessions
      video.keySystem = keySystem;

      if (!keySystemOptions.getCertificate) {
        resolve(keySystemAccess);
        return;
      }

      keySystemOptions.getCertificate(options, (err, cert) => {
        if (err) {
          reject(err);
          return;
        }

        certificate = cert;
        resolve();
      });
    }).then(() => {
      return keySystemAccess.createMediaKeys();
    }).then(createdMediaKeys => {
      safeTriggerOnEventBus(eventBus, {
        type: 'keysystemaccesscomplete',
        mediaKeys: createdMediaKeys
      });
      return addPendingSessions({
        player,
        video,
        certificate,
        createdMediaKeys,
        emeError
      });
    }).catch(err => {
      const metadata = {
        errorType: Error$1.EMEFailedToCreateMediaKeys,
        keySystem
      };
      emeError(err, metadata); // if we have a specific error message, use it, otherwise show a more
      // generic one

      if (err) {
        return Promise.reject(err);
      }

      return Promise.reject('Failed to create and initialize a MediaKeys object');
    });
  }

  return keySystemPromise.then(() => {
    // if key system has not been determined then addSession doesn't need getLicense
    const getLicense = video.keySystem ? promisifyGetLicense(keySystem, keySystemOptions.getLicense, eventBus) : null;
    return addSession({
      player,
      video,
      initDataType,
      initData,
      options,
      getLicense,
      contentId,
      removeSession,
      eventBus,
      emeError
    });
  });
};

/**
 * The W3C Working Draft of 22 October 2013 seems to be the best match for
 * the ms-prefixed API. However, it should only be used as a guide; it is
 * doubtful the spec is 100% implemented as described.
 *
 * @see https://www.w3.org/TR/2013/WD-encrypted-media-20131022
 * @see https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/mt598601(v=vs.85)
 */
const PLAYREADY_KEY_SYSTEM = 'com.microsoft.playready';
const addKeyToSession = (options, session, event, eventBus, emeError) => {
  let playreadyOptions = options.keySystems[PLAYREADY_KEY_SYSTEM];

  if (typeof playreadyOptions.getKey === 'function') {
    playreadyOptions.getKey(options, event.destinationURL, event.message.buffer, (err, key) => {
      if (err) {
        const metadata = {
          errorType: Error$1.EMEFailedToRequestMediaKeySystemAccess,
          config: getMediaKeySystemConfigurations(options.keySystems)
        };
        emeError(err, metadata);
        safeTriggerOnEventBus(eventBus, {
          message: 'Unable to get key: ' + err,
          target: session,
          type: 'mskeyerror'
        });
        return;
      }

      session.update(key);
      safeTriggerOnEventBus(eventBus, {
        type: 'keysessionupdated',
        keySession: session
      });
    });
    return;
  }

  if (typeof playreadyOptions === 'string') {
    playreadyOptions = {
      url: playreadyOptions
    };
  } else if (typeof playreadyOptions === 'boolean') {
    playreadyOptions = {};
  }

  if (!playreadyOptions.url) {
    playreadyOptions.url = event.destinationURL;
  }

  const callback = (err, responseBody) => {
    if (eventBus) {
      safeTriggerOnEventBus(eventBus, {
        type: 'licenserequestattempted'
      });
    }

    if (err) {
      const metadata = {
        errorType: Error$1.EMEFailedToGenerateLicenseRequest,
        keySystem: PLAYREADY_KEY_SYSTEM
      };
      emeError(err, metadata);
      safeTriggerOnEventBus(eventBus, {
        message: 'Unable to request key from url: ' + playreadyOptions.url,
        target: session,
        type: 'mskeyerror'
      });
      return;
    }

    session.update(new Uint8Array(responseBody));
  };

  if (playreadyOptions.getLicense) {
    playreadyOptions.getLicense(options, event.message.buffer, callback);
  } else {
    requestPlayreadyLicense(PLAYREADY_KEY_SYSTEM, playreadyOptions, event.message.buffer, options, callback);
  }
};
const createSession = (video, initData, options, eventBus, emeError) => {
  // Note: invalid mime type passed here throws a NotSupportedError
  const session = video.msKeys.createSession('video/mp4', initData);

  if (!session) {
    const error = new Error('Could not create key session.');
    const metadata = {
      errorType: Error$1.EMEFailedToCreateMediaKeySession,
      keySystem: PLAYREADY_KEY_SYSTEM
    };
    emeError(error, metadata);
    throw error;
  }

  safeTriggerOnEventBus(eventBus, {
    type: 'keysessioncreated',
    keySession: session
  }); // Note that mskeymessage may not always be called for PlayReady:
  //
  // "If initData contains a PlayReady object that contains an OnDemand header, only a
  // keyAdded event is returned (as opposed to a keyMessage event as described in the
  // Encrypted Media Extension draft). Similarly, if initData contains a PlayReady object
  // that contains a key identifier in the hashed data storage (HDS), only a keyAdded
  // event is returned."
  // eslint-disable-next-line max-len
  // @see [PlayReady License Acquisition]{@link https://msdn.microsoft.com/en-us/library/dn468979.aspx}

  session.addEventListener('mskeymessage', event => {
    safeTriggerOnEventBus(eventBus, {
      type: 'keymessage',
      messageEvent: event
    });
    addKeyToSession(options, session, event, eventBus, emeError);
  });
  session.addEventListener('mskeyerror', event => {
    const metadata = {
      errorType: Error$1.EMEFailedToCreateMediaKeySession,
      keySystem: PLAYREADY_KEY_SYSTEM
    };
    emeError(session.error, metadata);
    safeTriggerOnEventBus(eventBus, {
      message: 'Unexpected key error from key session with ' + `code: ${session.error.code} and systemCode: ${session.error.systemCode}`,
      target: session,
      type: 'mskeyerror'
    });
  });
  session.addEventListener('mskeyadded', () => {
    safeTriggerOnEventBus(eventBus, {
      target: session,
      type: 'mskeyadded'
    });
  });
};
var msPrefixed = (({
  video,
  initData,
  options,
  eventBus,
  emeError
}) => {
  // Although by the standard examples the presence of video.msKeys is checked first to
  // verify that we aren't trying to create a new session when one already exists, here
  // sessions are managed earlier (on the player.eme object), meaning that at this point
  // any existing keys should be cleaned up.
  // TODO: Will this break rotation? Is it safe?
  if (video.msKeys) {
    delete video.msKeys;
  }

  try {
    video.msSetMediaKeys(new window__default["default"].MSMediaKeys(PLAYREADY_KEY_SYSTEM));
  } catch (e) {
    const metadata = {
      errorType: Error$1.EMEFailedToCreateMediaKeys,
      keySystem: PLAYREADY_KEY_SYSTEM
    };
    emeError(e, metadata);
    throw new Error('Unable to create media keys for PlayReady key system. ' + 'Error: ' + e.message);
  }

  createSession(video, initData, options, eventBus, emeError);
});

const genericConfig = [{
  initDataTypes: ['cenc'],
  audioCapabilities: [{
    contentType: 'audio/mp4;codecs="mp4a.40.2"'
  }],
  videoCapabilities: [{
    contentType: 'video/mp4;codecs="avc1.42E01E"'
  }]
}];
const keySystems = [// Fairplay
// Requires different config than other CDMs
{
  keySystem: 'com.apple.fps',
  supportedConfig: [{
    initDataTypes: ['sinf'],
    videoCapabilities: [{
      contentType: 'video/mp4'
    }]
  }]
}, // Playready
{
  keySystem: 'com.microsoft.playready.recommendation',
  supportedConfig: genericConfig
}, // Widevine
{
  keySystem: 'com.widevine.alpha',
  supportedConfig: genericConfig
}, // Clear
{
  keySystem: 'org.w3.clearkey',
  supportedConfig: genericConfig
}]; // Asynchronously detect the list of supported CDMs by requesting key system access
// when possible, otherwise rely on browser-specific EME API feature detection.

const detectSupportedCDMs = () => {
  const Promise = window__default["default"].Promise;
  const results = {
    fairplay: Boolean(window__default["default"].WebKitMediaKeys),
    playready: false,
    widevine: false,
    clearkey: false
  };

  if (!window__default["default"].MediaKeys || !window__default["default"].navigator.requestMediaKeySystemAccess) {
    return Promise.resolve(results);
  }

  return Promise.all(keySystems.map(({
    keySystem,
    supportedConfig
  }) => {
    return window__default["default"].navigator.requestMediaKeySystemAccess(keySystem, supportedConfig).catch(() => {});
  })).then(([fairplay, playready, widevine, clearkey]) => {
    results.fairplay = Boolean(fairplay);
    results.playready = Boolean(playready);
    results.widevine = Boolean(widevine);
    results.clearkey = Boolean(clearkey);
    return results;
  });
};

var version = "5.5.0";

const hasSession = (sessions, initData) => {
  for (let i = 0; i < sessions.length; i++) {
    // Other types of sessions may be in the sessions array that don't store the initData
    // (for instance, PlayReady sessions on IE11).
    if (!sessions[i].initData) {
      continue;
    } // initData should be an ArrayBuffer by the spec:
    // eslint-disable-next-line max-len
    // @see [Media Encrypted Event initData Spec]{@link https://www.w3.org/TR/encrypted-media/#mediaencryptedeventinit}
    //
    // However, on some browsers it may come back with a typed array view of the buffer.
    // This is the case for IE11, however, since IE11 sessions are handled differently
    // (following the msneedkey PlayReady path), this coversion may not be important. It
    // is safe though, and might be a good idea to retain in the short term (until we have
    // catalogued the full range of browsers and their implementations).


    const sessionBuffer = arrayBufferFrom(sessions[i].initData);
    const initDataBuffer = arrayBufferFrom(initData);

    if (arrayBuffersEqual(sessionBuffer, initDataBuffer)) {
      return true;
    }
  }

  return false;
};
const removeSession = (sessions, initData) => {
  for (let i = 0; i < sessions.length; i++) {
    if (sessions[i].initData === initData) {
      sessions.splice(i, 1);
      return;
    }
  }
};
const handleEncryptedEvent = (player, event, options, sessions, eventBus, emeError) => {
  if (!options || !options.keySystems) {
    // return silently since it may be handled by a different system
    return Promise.resolve();
  } // Legacy fairplay is the keysystem 'com.apple.fps.1_0'.
  // If we are using this keysystem we want to use WebkitMediaKeys.
  // This can be initialized manually with initLegacyFairplay().


  if (options.keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM]) {
    videojs__default["default"].log.debug('eme', `Ignoring \'encrypted\' event, using legacy fairplay keySystem ${LEGACY_FAIRPLAY_KEY_SYSTEM}`);
    return Promise.resolve();
  }

  let initData = event.initData;
  return getSupportedKeySystem(options.keySystems).then(keySystemAccess => {
    const keySystem = keySystemAccess.keySystem; // Use existing init data from options if provided

    if (options.keySystems[keySystem] && options.keySystems[keySystem].pssh) {
      initData = options.keySystems[keySystem].pssh;
    } // "Initialization Data must be a fixed value for a given set of stream(s) or media
    // data. It must only contain information related to the keys required to play a given
    // set of stream(s) or media data."
    // eslint-disable-next-line max-len
    // @see [Initialization Data Spec]{@link https://www.w3.org/TR/encrypted-media/#initialization-data}


    if (hasSession(sessions, initData) || !initData) {
      // TODO convert to videojs.log.debug and add back in
      // https://github.com/videojs/video.js/pull/4780
      // videojs.log('eme',
      //             'Already have a configured session for init data, ignoring event.');
      return Promise.resolve();
    }

    sessions.push({
      initData
    });
    return standard5July2016({
      player,
      video: event.target,
      initDataType: event.initDataType,
      initData,
      keySystemAccess,
      options,
      removeSession: removeSession.bind(null, sessions),
      eventBus,
      emeError
    });
  }).catch(error => {
    const metadata = {
      errorType: Error$1.EMEFailedToRequestMediaKeySystemAccess,
      config: getMediaKeySystemConfigurations(options.keySystems)
    };
    emeError(error, metadata);
  });
};
const handleWebKitNeedKeyEvent = (event, options, eventBus, emeError) => {
  if (!options.keySystems || !options.keySystems[LEGACY_FAIRPLAY_KEY_SYSTEM] || !event.initData) {
    // return silently since it may be handled by a different system
    return Promise.resolve();
  } // From Apple's example Safari FairPlay integration code, webkitneedkey is not repeated
  // for the same content. Unless documentation is found to present the opposite, handle
  // all webkitneedkey events the same (even if they are repeated).


  return fairplay({
    video: event.target,
    initData: event.initData,
    options,
    eventBus,
    emeError
  });
};
const handleMsNeedKeyEvent = (event, options, sessions, eventBus, emeError) => {
  if (!options.keySystems || !options.keySystems[PLAYREADY_KEY_SYSTEM]) {
    // return silently since it may be handled by a different system
    return;
  } // "With PlayReady content protection, your Web app must handle the first needKey event,
  // but it must then ignore any other needKey event that occurs."
  // eslint-disable-next-line max-len
  // @see [PlayReady License Acquisition]{@link https://msdn.microsoft.com/en-us/library/dn468979.aspx}
  //
  // Usually (and as per the example in the link above) this is determined by checking for
  // the existence of video.msKeys. However, since the video element may be reused, it's
  // easier to directly manage the session.


  if (sessions.reduce((acc, session) => acc || session.playready, false)) {
    // TODO convert to videojs.log.debug and add back in
    // https://github.com/videojs/video.js/pull/4780
    // videojs.log('eme',
    //             'An \'msneedkey\' event was receieved earlier, ignoring event.');
    return;
  }

  let initData = event.initData; // Use existing init data from options if provided

  if (options.keySystems[PLAYREADY_KEY_SYSTEM] && options.keySystems[PLAYREADY_KEY_SYSTEM].pssh) {
    initData = options.keySystems[PLAYREADY_KEY_SYSTEM].pssh;
  }

  if (!initData) {
    return;
  }

  sessions.push({
    playready: true,
    initData
  });
  msPrefixed({
    video: event.target,
    initData,
    options,
    eventBus,
    emeError
  });
};
const getOptions = player => {
  return merge(player.currentSource(), player.eme.options);
};
/**
 * Configure a persistent sessions array and activeSrc property to ensure we properly
 * handle each independent source's events. Should be run on any encrypted or needkey
 * style event to ensure that the sessions reflect the active source.
 *
 * @function setupSessions
 * @param    {Player} player
 */

const setupSessions = player => {
  const src = player.src();

  if (src !== player.eme.activeSrc) {
    player.eme.activeSrc = src;
    player.eme.sessions = [];
  }
};
/**
 * Construct a simple function that can be used to dispatch EME errors on the
 * player directly, such as providing it to a `.catch()`.
 *
 * @function emeErrorHandler
 * @param    {Player} player
 * @return   {Function}
 */

const emeErrorHandler = player => {
  return (objOrErr, metadata) => {
    const error = {
      // MEDIA_ERR_ENCRYPTED is code 5
      code: 5
    };

    if (typeof objOrErr === 'string') {
      error.message = objOrErr;
    } else if (objOrErr) {
      if (objOrErr.message) {
        error.message = objOrErr.message;
      }

      if (objOrErr.cause && (objOrErr.cause.length || objOrErr.cause.byteLength)) {
        error.cause = objOrErr.cause;
      }

      if (objOrErr.keySystem) {
        error.keySystem = objOrErr.keySystem;
      } // pass along original error object.


      error.originalError = objOrErr;
    }

    if (metadata) {
      error.metadata = metadata;
    }

    player.error(error);
  };
};
/**
 * Function to invoke when the player is ready.
 *
 * This is a great place for your plugin to initialize itself. When this
 * function is called, the player will have its DOM and child components
 * in place.
 *
 * @function onPlayerReady
 * @param    {Player} player
 * @param    {Function} emeError
 */

const onPlayerReady = (player, emeError) => {
  if (player.$('.vjs-tech').tagName.toLowerCase() !== 'video') {
    return;
  }

  setupSessions(player);

  if (window__default["default"].MediaKeys) {
    // Support EME 05 July 2016
    // Chrome 42+, Firefox 47+, Edge, Safari 12.1+ on macOS 10.14+
    player.tech_.el_.addEventListener('encrypted', event => {
      videojs__default["default"].log.debug('eme', 'Received an \'encrypted\' event');
      setupSessions(player);
      handleEncryptedEvent(player, event, getOptions(player), player.eme.sessions, player.tech_, emeError);
    });
  } else if (window__default["default"].WebKitMediaKeys) {
    player.eme.initLegacyFairplay();
  } else if (window__default["default"].MSMediaKeys) {
    // IE11 Windows 8.1+
    // Since IE11 doesn't support promises, we have to use a combination of
    // try/catch blocks and event handling to simulate promise rejection.
    // Functionally speaking, there should be no discernible difference between
    // the behavior of IE11 and those of other browsers.
    player.tech_.el_.addEventListener('msneedkey', event => {
      videojs__default["default"].log.debug('eme', 'Received an \'msneedkey\' event');
      setupSessions(player);

      try {
        handleMsNeedKeyEvent(event, getOptions(player), player.eme.sessions, player.tech_, emeError);
      } catch (error) {
        emeError(error);
      }
    });

    const msKeyErrorCallback = error => {
      emeError(error);
    };

    player.tech_.on('mskeyerror', msKeyErrorCallback); // TODO: refactor this plugin so it can use a plugin dispose

    player.on('dispose', () => {
      player.tech_.off('mskeyerror', msKeyErrorCallback);
    });
  }
};
/**
 * A video.js plugin.
 *
 * In the plugin function, the value of `this` is a video.js `Player`
 * instance. You cannot rely on the player being in a "ready" state here,
 * depending on how the plugin is invoked. This may or may not be important
 * to you; if not, remove the wait for "ready"!
 *
 * @function eme
 * @param    {Object} [options={}]
 *           An object of options left to the plugin author to define.
 */


const eme = function (options = {}) {
  const player = this;
  const emeError = emeErrorHandler(player);
  player.ready(() => onPlayerReady(player, emeError)); // Plugin API

  player.eme = {
    /**
     * For manual setup for eme listeners (for example: after player.reset call)
     * basically for any cases when player.tech.el is changed
     */
    setupEmeListeners() {
      onPlayerReady(player, emeError);
    },

    /**
    * Sets up MediaKeys on demand
    * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449
    *
    * @function initializeMediaKeys
    * @param    {Object} [emeOptions={}]
    *           An object of eme plugin options.
    * @param    {Function} [callback=function(){}]
    * @param    {boolean} [suppressErrorIfPossible=false]
    */
    initializeMediaKeys(emeOptions = {}, callback = function () {}, suppressErrorIfPossible = false) {
      // TODO: this should be refactored and renamed to be less tied
      // to encrypted events
      const mergedEmeOptions = merge(player.currentSource(), options, emeOptions); // fake an encrypted event for handleEncryptedEvent

      const mockEncryptedEvent = {
        initDataType: 'cenc',
        initData: null,
        target: player.tech_.el_
      };
      setupSessions(player);

      if (window__default["default"].MediaKeys) {
        handleEncryptedEvent(player, mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_, emeError).then(() => callback()).catch(error => {
          callback(error);

          if (!suppressErrorIfPossible) {
            emeError(error);
          }
        });
      } else if (window__default["default"].MSMediaKeys) {
        const msKeyHandler = event => {
          player.tech_.off('mskeyadded', msKeyHandler);
          player.tech_.off('mskeyerror', msKeyHandler);

          if (event.type === 'mskeyerror') {
            callback(event.target.error);

            if (!suppressErrorIfPossible) {
              emeError(event.message);
            }
          } else {
            callback();
          }
        };

        player.tech_.one('mskeyadded', msKeyHandler);
        player.tech_.one('mskeyerror', msKeyHandler);

        try {
          handleMsNeedKeyEvent(mockEncryptedEvent, mergedEmeOptions, player.eme.sessions, player.tech_, emeError);
        } catch (error) {
          player.tech_.off('mskeyadded', msKeyHandler);
          player.tech_.off('mskeyerror', msKeyHandler);
          callback(error);

          if (!suppressErrorIfPossible) {
            emeError(error);
          }
        }
      }
    },

    initLegacyFairplay() {
      const handleFn = event => {
        videojs__default["default"].log.debug('eme', 'Received a \'webkitneedkey\' event'); // TODO it's possible that the video state must be cleared if reusing the same video
        // element between sources

        setupSessions(player);
        handleWebKitNeedKeyEvent(event, getOptions(player), player.tech_, emeError).catch(error => {
          emeError(error);
        });
      };

      const webkitNeedKeyEventHandler = event => {
        const firstWebkitneedkeyTimeout = getOptions(player).firstWebkitneedkeyTimeout || 1000;
        const src = player.src(); // on source change or first startup reset webkitneedkey options.

        player.eme.webkitneedkey_ = player.eme.webkitneedkey_ || {}; // if the source changed we need to handle the first event again.
        // track source changes internally.

        if (player.eme.webkitneedkey_.src !== src) {
          player.eme.webkitneedkey_ = {
            handledFirstEvent: false,
            src
          };
        } // It's possible that at the start of playback a rendition switch
        // on a small player in safari's HLS implementation will cause
        // two webkitneedkey events to occur. We want to make sure to cancel
        // our first existing request if we get another within 1 second. This
        // prevents a non-fatal player error from showing up due to a
        // request failure.


        if (!player.eme.webkitneedkey_.handledFirstEvent) {
          // clear the old timeout so that a new one can be created
          // with the new rendition's event data
          player.clearTimeout(player.eme.webkitneedkey_.timeout);
          player.eme.webkitneedkey_.timeout = player.setTimeout(() => {
            player.eme.webkitneedkey_.handledFirstEvent = true;
            player.eme.webkitneedkey_.timeout = null;
            handleFn(event);
          }, firstWebkitneedkeyTimeout); // after we have a verified first request, we will request on
          // every other event like normal.
        } else {
          handleFn(event);
        }
      };

      let videoElement = player.tech_.el_; // Support Safari EME with FairPlay
      // (also used in early Chrome or Chrome with EME disabled flag)

      videoElement.addEventListener('webkitneedkey', webkitNeedKeyEventHandler);

      const cleanupWebkitNeedKeyHandler = () => {
        // no need in auto-cleanup if manual clean is called
        player.off('dispose', cleanupWebkitNeedKeyHandler); // check for null, if manual cleanup is called multiple times for any reason

        if (videoElement !== null) {
          videoElement.removeEventListener('webkitneedkey', webkitNeedKeyEventHandler);
        }

        videoElement = null;
      }; // auto-cleanup:


      player.on('dispose', cleanupWebkitNeedKeyHandler); // returning for manual cleanup

      return cleanupWebkitNeedKeyHandler;
    },

    detectSupportedCDMs,
    options
  };
}; // Register the plugin with video.js.


videojs__default["default"].registerPlugin('eme', eme); // contrib-eme specific error const

eme.Error = Error$1; // Include the version number.

eme.VERSION = version;

exports["default"] = eme;
exports.emeErrorHandler = emeErrorHandler;
exports.getOptions = getOptions;
exports.handleEncryptedEvent = handleEncryptedEvent;
exports.handleMsNeedKeyEvent = handleMsNeedKeyEvent;
exports.handleWebKitNeedKeyEvent = handleWebKitNeedKeyEvent;
exports.hasSession = hasSession;
exports.removeSession = removeSession;
exports.setupSessions = setupSessions;