trackers_PillarboxMonitoring.js

import pillarbox from '../pillarbox.js';

/** @import Player from 'video.js/dist/types/player' */

/* eslint max-statements: ["error", 25]*/

/**
 * The PillarboxMonitoring class retrieves data about media playback.
 *
 * This data can be used to :
 *  - help investigate playback problems
 *  - measure the quality of our service
 *
 * The sending of this data tries to respect as much as possible the
 * specification described in the link below.
 *
 * However, some platforms may have certain limitations.
 * In this case, only the data available will be sent.
 *
 * @see https://github.com/SRGSSR/pillarbox-documentation/blob/main/Specifications/monitoring.md
 */
class PillarboxMonitoring {
  /**
   * Creates an instance of PillarboxMonitoring.
   *
   * @constructor
   * @param {Player} player The player instance to be monitored
   * @param {PillarboxMonitoringOptions} [options={}] Configuration options for the monitoring
   * @param {string} [options.playerName='none'] The name of the player
   * @param {string} [options.playerVersion='none'] The version of the player
   * @param {string} [options.platform='Web'] The platform on which the player is running
   * @param {number} [options.schemaVersion=1] The version of the schema used for monitoring
   * @param {number} [options.heartbeatInterval=30000] The interval in milliseconds for sending heartbeat signals
   * @param {string} [options.beaconUrl='https://monitoring.pillarbox.ch/api/events'] The URL for the monitoring beacon
   */
  constructor(player, {
    playerName = 'none',
    playerVersion = 'none',
    platform = 'Web',
    schemaVersion = 1,
    heartbeatInterval = 30_000,
    beaconUrl = 'https://monitoring.pillarbox.ch/api/events'
  } = {}) {
    /**
     * @type {import('video.js/dist/types/player').default}
     */
    this.player = player;
    /**
     * @type {string}
     */
    this.playerName = playerName;
    /**
     * @type {string}
     */
    this.playerVersion = playerVersion;
    /**
     * @type {string}
     */
    this.platform = platform;
    /**
     * @type {string}
     */
    this.schemaVersion = schemaVersion;
    /**
     * @type {Number}
     */
    this.heartbeatInterval = heartbeatInterval;
    /**
     * @type {string}
     */
    this.beaconUrl = beaconUrl;
    /**
     * @type {string}
     */
    this.currentSessionId = undefined;
    /**
     * @type {Number}
     */
    this.lastPlaybackDuration = 0;
    /**
     * @type {Number}
     */
    this.lastPlaybackStartTimestamp = 0;
    /**
     * @type {Number}
     */
    this.lastStallCount = 0;
    /**
     * @type {Number}
     */
    this.lastStallDuration = 0;
    /**
     * @type {Number}
     */
    this.loadStartTimestamp = undefined;
    /**
     * @type {Number}
     */
    this.metadataRequestTime = 0;
    /**
     * @type {string}
     */
    this.mediaAssetUrl = undefined;
    /**
     * @type {string}
     */
    this.mediaId = undefined;
    /**
     * @type {string}
     */
    this.mediaMetadataUrl = undefined;
    /**
     * @type {string}
     */
    this.mediaOrigin = undefined;
    /**
     * @type {Number}
     */
    this.tokenRequestTime = 0;

    this.addListeners();
  }

  /**
   * Adds event listeners to the player and the window.
   */
  addListeners() {
    this.bindCallBacks();

    this.player.on('loadstart', this.loadStart);
    this.player.on('loadeddata', this.loadedData);
    this.player.on('playing', this.playbackStart);
    this.player.on('pause', this.playbackStop);
    this.player.on('error', this.error);
    this.player.on(['playerreset', 'dispose', 'ended'], this.sessionStop);
    this.player.on(['waiting', 'stalled'], this.stalled);

    window.addEventListener('beforeunload', this.sessionStop);
  }

  /**
   * The current bandwidth of the last segment download.
   *
   * @returns {number|undefined} The current bandwidth in bits per second,
   *                             undefined otherwise.
   */
  bandwidth() {
    const playerStats = this.player
      .tech(true).vhs ? this.player.tech(true).vhs.stats : undefined;

    return playerStats ? playerStats.bandwidth : undefined;
  }

  /**
   * Binds the callback functions to the current instance.
   */
  bindCallBacks() {
    this.error = this.error.bind(this);
    this.loadedData = this.loadedData.bind(this);
    this.loadStart = this.loadStart.bind(this);
    this.playbackStart = this.playbackStart.bind(this);
    this.playbackStop = this.playbackStop.bind(this);
    this.stalled = this.stalled.bind(this);
    this.sessionStop = this.sessionStop.bind(this);
  }

  /**
   * Get the buffer duration in milliseconds.
   *
   * @returns {Number} The buffer duration
   */
  bufferDuration() {
    const buffered = this.player.buffered();
    let bufferDuration = 0;

    for (let i = 0; i < buffered.length; i++) {
      const start = buffered.start(i);
      const end = buffered.end(i);

      bufferDuration += end - start;
    }

    return PillarboxMonitoring.secondsToMilliseconds(bufferDuration);
  }

  /**
   * Get the current representation when playing a Dash or Hls media.
   *
   * @typedef {Object} Representation
   * @property {number|undefined} bandwidth The bandwidth of the current
   *                                        representation
   * @property {number|undefined} programDateTime The program date time of the
   *                                              current representation
   * @property {string|undefined} uri The URL of the current representation
   *
   * @returns {Representation|undefined} The current representation object
   *                                     undefined otherwise
   */
  currentRepresentation() {
    const {
      activeCues: { cues_: [cue] } = { cues_: [] }
    } = Array.from(this.player.textTracks())
      .find(({ label, kind }) => kind === 'metadata' && label === 'segment-metadata') || {};

    return cue ? cue.value : undefined;
  }

  /**
   * Get the current resource information including bitrate and URL when available.
   *
   * @typedef {Object} Resource
   * @property {number|undefined} bitrate The bitrate of the current resource
   * @property {string|undefined} url The URL of the current resource
   *
   * @returns {Resource} The current resource information.
   */
  currentResource() {
    let { bandwidth: bitrate, uri: url } = this.currentRepresentation() || {};

    if (pillarbox.browser.IS_ANY_SAFARI) {
      const { configuration } = Array
        .from(this.player.videoTracks()).find(track => track.selected) || {};

      bitrate = configuration ? configuration.bitrate : undefined;
      url = this.player.currentSource().src;
    }

    return {
      bitrate,
      url
    };
  }

  /**
   * The media data of the current source.
   *
   * @returns {Object} The media data of the current source, or an empty object
   *                   if no media data is available.
   */
  currentSourceMediaData() {
    if (!this.player.currentSource().mediaData) return {};

    return this.player.currentSource().mediaData;
  }

  /**
   * Handles player errors by sending an `ERROR` event, then resets the session.
   */
  error() {
    const error = this.player.error();
    const playbackPosition = this.playbackPosition();
    const representation = this.currentRepresentation();
    const url = representation ?
      representation.uri : this.player.currentSource().src;

    if (!this.player.hasStarted()) {
      this.sendEvent('START', this.startEventData());
    }

    this.sendEvent('ERROR', {
      log: JSON
        .stringify(error.metadata || pillarbox.log.history().slice(-15)),
      message: error.message,
      name: PillarboxMonitoring.errorKeyCode(error.code),
      ...playbackPosition,
      url
    });

    this.reset();
  }

  /**
   * Get the DRM license request duration from performance API.
   *
   * @returns {number|undefined} The request duration
   */
  getDrmRequestDuration() {
    const keySystems = Object
      .values(this.player.currentSource().keySystems || {})
      .map(keySystem => keySystem.url);

    if (!keySystems.length) return;

    const resource = performance
      .getEntriesByType('resource')
      .filter(({ initiatorType, name }) =>
        initiatorType === 'xmlhttprequest' && keySystems.includes(name))
      .pop();

    return resource && resource.duration;
  }

  /**
   * Get metadata information from the performance API for a given id.
   *
   * @typedef {Object} MetadataInfo
   * @property {string} name The URL of the resource
   * @property {number} duration The duration of the resource fetch in milliseconds
   *
   * @param {string} id The id to search for in the resource entries
   *
   * @returns {MetadataInfo|undefined} An object containing metadata
   * information, or undefined otherwise
   */
  getMetadataInfo(id) {
    const resource = performance
      .getEntriesByType('resource')
      .filter(({ initiatorType, name }) =>
        initiatorType === 'fetch' && name.includes(id))
      .pop();

    if (!resource) return {};

    return {
      name: resource.name,
      duration: resource.duration
    };
  }

  /**
   * Get the Akamai token request duration from performance API.
   *
   * @returns {number|undefined} The request duration
   */
  getTokenRequestDuration(tokenType) {
    if (!tokenType) return;

    const resource = performance
      .getEntriesByType('resource')
      .filter(({ initiatorType, name }) =>
        initiatorType === 'fetch' && name.includes('/akahd/token'))
      .pop();

    return resource && resource.duration;
  }

  /**
   * Send an 'HEARTBEAT' event with the date of the current playback state at
   * regular intervals.
   */
  heartbeat() {
    this.heartbeatIntervalId = setInterval(() => {
      this.sendEvent('HEARTBEAT', this.statusEventData());
    }, this.heartbeatInterval);
  }

  /**
   * Check if the tracker is disabled.
   *
   * @returns {Boolean} __true__ if disabled __false__ otherwise.
   */
  isTrackerDisabled() {
    const currentSource = this.player.currentSource();

    if (!Array.isArray(currentSource.disableTrackers)) {
      return Boolean(currentSource.disableTrackers);
    }

    return Boolean(
      currentSource.disableTrackers.find(
        (tracker) => tracker.toLowerCase() === PillarboxMonitoring
          .name.toLowerCase()
      )
    );
  }

  /**
   * Handles the session start by sending a `START` event immediately followed
   * by a `HEARTBEAT` when the `loadeddata` event is triggered.
   */
  loadedData() {
    this.sendEvent('START', this.startEventData());
    this.sendEvent('HEARTBEAT', this.statusEventData());
    // starts the heartbeat interval
    this.heartbeat();
  }

  /**
   * Handles `loadstart` event and captures the current timestamp. Will be used
   * to calculate the media loading time.
   */
  loadStart() {
    // if the content is a plain old URL
    if (
      !Object.keys(this.currentSourceMediaData()).length &&
      this.currentSessionId
    ) {
      this.sessionStop();
      // Reference timestamp used to calculate the different time metrics.
      this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
    }

    this.loadStartTimestamp = PillarboxMonitoring.timestamp();
  }

  /**
   * The media information.
   *
   * @typedef {Object} MediaInfo
   * @property {string} asset_url  The URL of the media
   * @property {string} id  The ID of the media
   * @property {string} metadata_url  The URL of the media metadata
   * @property {string} origin  The origin of the media
   *
   * @returns {MediaInfo} An object container the media information
   */
  mediaInfo() {
    return {
      asset_url: this.mediaAssetUrl,
      id: this.mediaId,
      metadata_url: this.mediaMetadataUrl,
      origin: this.mediaOrigin,
    };
  }

  /**
   * The total playback duration for the current session.
   *
   * @returns {number} The total playback duration in milliseconds.
   */
  playbackDuration() {
    if (!this.lastPlaybackStartTimestamp) {
      return this.lastPlaybackDuration;
    }

    return (
      PillarboxMonitoring.timestamp() +
      this.lastPlaybackDuration -
      this.lastPlaybackStartTimestamp
    );
  }

  /**
   * The current playback position and position timestamp.
   *
   * @typedef {Object} PlaybackPosition
   * @property {number} position The current playback position in milliseconds
   * @property {number|undefined} position_timestamp The timestamp of the
   *                    current playback position, or undefined if not available
   *
   * @returns {PlaybackPosition} The playback position object.
   */
  playbackPosition() {
    const currentRepresentation = this.currentRepresentation();
    const position = PillarboxMonitoring
      .secondsToMilliseconds(this.player.currentTime());
    let position_timestamp;

    // Get the position timestamp from the program date time when VHS is used
    // or undefined if there is no value
    if (currentRepresentation) {
      position_timestamp = currentRepresentation.programDateTime;
    }

    // Calculate the position timestamp from the start date on Safari
    if (pillarbox.browser.IS_ANY_SAFARI) {
      const startDate = Date.parse(this.player.$('video').getStartDate());

      position_timestamp = !isNaN(startDate) ?
        (startDate + position) : undefined;
    }

    return {
      position,
      position_timestamp
    };
  }

  /**
   * Assign the timestamp each time the playback starts.
   */
  playbackStart() {
    this.lastPlaybackStartTimestamp = PillarboxMonitoring.timestamp();
  }

  /**
   * Calculates and accumulates the duration of the playback session each time
   * the playback stops for the current media.
   */
  playbackStop() {
    this.lastPlaybackDuration +=
      PillarboxMonitoring.timestamp() - this.lastPlaybackStartTimestamp;

    this.lastPlaybackStartTimestamp = 0;
  }

  /**
   * The current dimensions of the player.
   *
   * @typedef {Object} PlayerCurrentDimensions
   * @property {number} width The current width of the player
   * @property {number} height The current height of the player
   *
   * @returns {PlayerCurrentDimensions} The current dimensions of the player object.
   */
  playerCurrentDimensions() {
    return this.player.currentDimensions();
  }

  /**
   * Information about the player.
   *
   * @typedef {Object} PlayerInfo
   * @property {string} name The name of the player
   * @property {string} version The version of the player
   * @property {string} platform The platform on which the player is running
   *
   * @returns {PlayerInfo} An object containing player information.
   */
  playerInfo() {
    return {
      name: this.playerName,
      version: this.playerVersion,
      platform: this.platform
    };
  }

  /**
   * Generates the QoE timings object.
   *
   * @typedef {Object} QoeTimings
   * @property {number} metadata The time taken to load metadata
   * @property {number} asset The time taken to load the asset
   * @property {number} total The total time taken from session start to data load
   *
   * @param {number} timeToLoadedData The time taken to load the data
   * @param {number} timestamp The current timestamp
   *
   * @returns {QoeTimings} The QoE timings
   */
  qoeTimings(timeToLoadedData, timestamp) {
    return {
      metadata: this.metadataRequestTime,
      asset: timeToLoadedData,
      total: timestamp - this.sessionStartTimestamp
    };
  }

  /**
   * Generates the QoS timings object.
   *
   * @typedef {Object} QosTimings
   * @property {number} asset The time taken to load the asset
   * @property {number} drm The time taken for DRM processing
   * @property {number} metadata The time taken to load metadata
   * @property {number} token The time taken to request the token
   *
   * @param {number} timeToLoadedData The time taken to load the data
   *
   * @returns {QosTimings} The QoS timings
   */
  qosTimings(timeToLoadedData) {
    return {
      asset: timeToLoadedData,
      drm: this.getDrmRequestDuration(),
      metadata: this.metadataRequestTime,
      token: this.tokenRequestTime,
    };
  }

  /**
   * Removes all event listeners from the player and the window.
   */
  removeListeners() {
    this.player.off('loadstart', this.loadStart);
    this.player.off('loadeddata', this.loadedData);
    this.player.off('playing', this.playbackStart);
    this.player.off('pause', this.playbackStop);
    this.player.off('error', this.error);
    this.player.off(['playerreset', 'dispose', 'ended'], this.sessionStop);
    this.player.off(['waiting', 'stalled'], this.stalled);

    window.removeEventListener('beforeunload', this.sessionStop);
  }

  /**
   * Remove the token from the asset URL.
   *
   * @param {string} assetUrl The URL of the asset
   *
   * @returns {string|undefined} The URL without the token, or undefined if the
   *                             input URL is invalid
   */
  removeTokenFromAssetUrl(assetUrl) {
    if (!assetUrl) return;

    try {
      const url = new URL(assetUrl);

      url.searchParams.delete('hdnts');

      return url.href;
    } catch (e) {
      return;
    }
  }

  /**
   * Resets the playback session and clears relevant properties.
   *
   * @param {Event} event The event that triggered the reset. If the event type
   *                 is not 'ended' or 'playerreset', listeners will be removed.
   */
  reset(event) {
    this.currentSessionId = undefined;
    this.lastPlaybackDuration = 0;
    this.lastPlaybackStartTimestamp = 0;
    this.lastStallCount = 0;
    this.lastStallDuration = 0;
    this.loadStartTimestamp = 0;
    this.metadataRequestTime = 0;
    this.mediaAssetUrl = undefined;
    this.mediaId = undefined;
    this.mediaMetadataUrl = undefined;
    this.mediaOrigin = undefined;
    this.sessionStartTimestamp = undefined;
    this.tokenRequestTime = 0;

    clearInterval(this.heartbeatIntervalId);

    if (event && !['ended', 'playerreset'].includes(event.type)) {
      this.removeListeners();
    }
  }

  /**
   * Sends an event to the server using the Beacon API.
   *
   * @param {string} eventName Either START, STOP, ERROR, HEARTBEAT
   * @param {Object} [data={}] The payload object to be sent. Defaults to an
   *                           empty object if not provided
   */
  sendEvent(eventName, data = {}) {
    // If the tracker is disabled for the current session, and there has been no
    // previous session, no event is sent. However, if a session was already
    // active, we still want to send the STOP event so that it is properly
    // stopped.
    if (
      (this.isTrackerDisabled() && !this.currentSessionId) ||
      !this.currentSessionId
    ) return;

    const payload = JSON.stringify({
      event_name: eventName,
      session_id: this.currentSessionId,
      timestamp: PillarboxMonitoring.timestamp(),
      version: this.schemaVersion,
      data
    });

    navigator.sendBeacon(
      this.beaconUrl,
      payload
    );
  }

  /**
   * Starts a new session by first stopping the previous session, then resetting
   * the session start timestamp and media ID to their new values.
   */
  sessionStart() {
    if (this.sessionStartTimestamp) {
      this.sessionStop();
    }

    // Reference timestamp used to calculate the different time metrics.
    this.sessionStartTimestamp = PillarboxMonitoring.timestamp();
    // At this stage currentSource().src is the media identifier
    // and not the playable source.
    this.mediaId = this.player.currentSource().src || undefined;
  }

  /**
   * Stops the current session by sending a `STOP` event and resetting the
   * session.
   *
   * @param {Event} [event] The event that triggered the stop. This is passed
   *                        to the reset function.
   */
  sessionStop(event) {
    this.sendEvent('STOP', this.statusEventData());
    this.reset(event);
  }

  /**
   * Handles the stalled state of the player. Sets the stalled state and listens
   * for the event that indicates the player is no longer stalled.
   */
  stalled() {
    if (
      !this.player.hasStarted() ||
      this.player.seeking() ||
      this.isStalled
    ) return;

    this.isStalled = true;

    const stallStart = PillarboxMonitoring.timestamp();
    const unstalled = () => {
      const stallEnd = PillarboxMonitoring.timestamp();

      this.isStalled = false;
      this.lastStallCount += 1;
      this.lastStallDuration += (stallEnd - stallStart);
    };

    // As Safari is not consistent with its playing event, it is better to use
    // the timeupdate event.
    if (pillarbox.browser.IS_ANY_SAFARI) {
      this.player.one('timeupdate', unstalled);
    } else {
      // As Chromium-based browsers are not consistent with their timeupdate
      // event, it is better to use the playing event.
      //
      // Firefox is consistent with its playing event.
      this.player.one('playing', unstalled);
    }
  }

  /**
   * Information about the player's stall events.
   *
   * @typedef {Object} StallInfo
   * @property {number} count The number of stall events
   * @property {number} duration The total duration of stall events in
   *                             milliseconds
   *
   * @returns {StallInfo} An object containing the stall information
   */
  stallInfo() {
    return {
      count: this.lastStallCount,
      duration: this.lastStallDuration,
    };
  }

  /**
   * Get data on the current playback state. Will be used when sending `HEARTBEAT` or `STOP` events.
   *
   * @typedef {Object} StatusEventData
   * @property {number} bandwidth The current bandwidth
   * @property {number|undefined} bitrate The bitrate of the current resource
   * @property {number} buffered_duration The duration of the buffered content
   * @property {number} frame_drops The number of dropped frames
   * @property {number} playback_duration The duration of the playback
   * @property {number} position The current playback position
   * @property {number} position_timestamp The timestamp of the current playback position
   * @property {Object} stall Information about any stalls
   * @property {string} stream_type The type of stream, either 'on-demand' or 'live'
   * @property {string|undefined} url The URL of the current resource
   *
   * @returns {StatusEventData} The current event data
   */
  statusEventData() {
    const bandwidth = this.bandwidth();
    const buffered_duration = this.bufferDuration();
    const { bitrate, url } = this.currentResource();
    const {
      droppedVideoFrames: frame_drops
    } = this.player.getVideoPlaybackQuality();
    const playback_duration = this.playbackDuration();
    const { position, position_timestamp } = this.playbackPosition();
    const stream_type = isFinite(this.player.duration()) ? 'On-demand' : 'Live';
    const stall = this.stallInfo();

    const data = {
      bandwidth,
      bitrate,
      buffered_duration,
      frame_drops,
      playback_duration,
      position,
      position_timestamp,
      stall,
      stream_type,
      url,
    };

    return data;
  }

  /**
   * Generates the data for the start event.
   *
   * @typedef {Object} Device
   * @property {string} id The device ID.
   *
   * @typedef {Object} StartEventData
   * @property {string} browser The user agent string of the browser.
   * @property {Device} device Information about the device.
   * @property {MediaInfo} media Information about the media.
   * @property {PlayerInfo} player Information about the player.
   * @property {QoeTimings} qoe_timings Quality of Experience timings.
   * @property {QosTimings} qos_timings Quality of Service timings.
   * @property {PlayerCurrentDimensions} screen The current dimensions of the
   *                                            player.
   *
   * @returns {StartEventData} An object containing the start event data.
   */
  startEventData() {
    const timestamp = PillarboxMonitoring.timestamp();
    // This avoids false subtraction results when loadStartTimestamp is not
    // initialized.
    // loadStartTimestamp will be 0 if loadstart is not triggered.
    // This is the case when a STARTDATE error occurs.
    const timeToLoadedData = this
      .loadStartTimestamp ? timestamp - this.loadStartTimestamp : 0;

    if (!this.isTrackerDisabled()) {
      this.currentSessionId = PillarboxMonitoring.sessionId();
    }

    this.mediaAssetUrl = this
      .removeTokenFromAssetUrl(this.player.currentSource().src);
    this.mediaMetadataUrl = this.getMetadataInfo(this.mediaId).name;
    this.metadataRequestTime = this.getMetadataInfo(this.mediaId).duration;
    this.mediaOrigin = window.location.href;
    this.tokenRequestTime = this.getTokenRequestDuration(
      this.currentSourceMediaData().tokenType
    );

    return {
      browser: PillarboxMonitoring.userAgent(),
      device: { id: PillarboxMonitoring.deviceId() },
      media: this.mediaInfo(),
      player: this.playerInfo(),
      qoe_timings: this.qoeTimings(timeToLoadedData, timestamp),
      qos_timings: this.qosTimings(timeToLoadedData),
      screen: this.playerCurrentDimensions()
    };
  }

  /**
   * Returns a string representing the error key combined with its error code.
   *
   * If the error code doesn't exist the error key undefined.
   *
   * @param {number} code The error code
   *
   * @returns {string} The error key combined with its error code
   */
  static errorKeyCode(code) {
    const error = [
      'MEDIA_ERR_CUSTOM',
      ...Object.keys(window.MediaError),
      'MEDIA_ERR_ENCRYPTED'
    ];

    return `${error[code]}: ${code}`;
  }

  /**
   * Generates a new session ID.
   *
   * @returns {string} random UUID
   */
  static sessionId() {
    return PillarboxMonitoring.randomUUID();
  }

  /**
   * Retrieve or generate a unique device ID and stores it in localStorage.
   *
   * @returns {string|undefined} The device ID if localStorage is available,
   *                             otherwise `undefined`
   */
  static deviceId() {
    if (!localStorage) return;

    const deviceIdKey = 'pillarbox_device_id';
    let deviceId = localStorage.getItem(deviceIdKey);

    if (!deviceId) {
      deviceId = PillarboxMonitoring.randomUUID();
      localStorage.setItem(deviceIdKey, deviceId);
    }

    return deviceId;
  }

  /**
   * Generate a cryptographically secure random UUID.
   *
   * @returns {string}
   */
  static randomUUID() {
    if (!crypto.randomUUID) {
      // Polyfill from the author of uuid js which is simple and
      // cryptographically secure.
      // https://stackoverflow.com/a/2117523
      return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c =>
        // eslint-disable-next-line
        (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4)
          .toString(16));
    }

    return crypto.randomUUID();
  }

  /**
   * converts seconds into milliseconds.
   *
   * @param {number} seconds
   *
   * @returns {number} milliseconds as an integer value
   */
  static secondsToMilliseconds(seconds) {
    return parseInt(seconds * 1000);
  }

  /**
   * The timestamp in milliseconds.
   *
   * @return {number} milliseconds as an integer value
   */
  static timestamp() {
    return Date.now();
  }

  /**
   * The browser's user agent.
   *
   * @returns {string}
   */
  static userAgent() {
    return {
      user_agent: navigator.userAgent
    };
  }
}

export default PillarboxMonitoring;