Home Reference Source

src/utils/vttcue.ts

/**
 * Copyright 2013 vtt.js Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

declare interface VTTCuePolyfill extends VTTCue {
  new (...args): VTTCuePolyfill;
  hasBeenReset: boolean;
  displayState: void;
}

export default (function () {
  if (typeof self !== 'undefined' && self.VTTCue) {
    return self.VTTCue;
  }

  const AllowedDirections = ['', 'lr', 'rl'] as const;
  type Direction = typeof AllowedDirections[number];

  const AllowedAlignments = [
    'start',
    'middle',
    'end',
    'left',
    'right',
  ] as const;
  type Alignment = typeof AllowedAlignments[number];

  function isAllowedValue<T, A>(allowed: T, value: string): A | false {
    if (typeof value !== 'string') {
      return false;
    }
    // necessary for assuring the generic conforms to the Array interface
    if (!Array.isArray(allowed)) {
      return false;
    }
    // reset the type so that the next narrowing works well
    const lcValue = value.toLowerCase() as any;
    // use the allow list to narrow the type to a specific subset of strings
    if (~allowed.indexOf(lcValue)) {
      return lcValue;
    }

    return false;
  }

  function findDirectionSetting(value: string) {
    return isAllowedValue<typeof AllowedDirections, Direction>(
      AllowedDirections,
      value
    );
  }

  function findAlignSetting(value: string) {
    return isAllowedValue<typeof AllowedAlignments, Alignment>(
      AllowedAlignments,
      value
    );
  }

  function extend(obj: Record<string, any>, ...rest: Record<string, any>[]) {
    let i = 1;
    for (; i < arguments.length; i++) {
      const cobj = arguments[i];
      for (const p in cobj) {
        obj[p] = cobj[p];
      }
    }

    return obj;
  }

  function VTTCue(startTime: number, endTime: number, text: string) {
    const cue = this as VTTCuePolyfill;
    const baseObj = { enumerable: true };
    /**
     * Shim implementation specific properties. These properties are not in
     * the spec.
     */

    // Lets us know when the VTTCue's data has changed in such a way that we need
    // to recompute its display state. This lets us compute its display state
    // lazily.
    cue.hasBeenReset = false;

    /**
     * VTTCue and TextTrackCue properties
     * http://dev.w3.org/html5/webvtt/#vttcue-interface
     */

    let _id = '';
    let _pauseOnExit = false;
    let _startTime = startTime;
    let _endTime = endTime;
    let _text = text;
    let _region = null;
    let _vertical: Direction = '';
    let _snapToLines = true;
    let _line: number | 'auto' = 'auto';
    let _lineAlign: Alignment = 'start';
    let _position = 50;
    let _positionAlign: Alignment = 'middle';
    let _size = 50;
    let _align: Alignment = 'middle';

    Object.defineProperty(
      cue,
      'id',
      extend({}, baseObj, {
        get: function () {
          return _id;
        },
        set: function (value: string) {
          _id = '' + value;
        },
      })
    );

    Object.defineProperty(
      cue,
      'pauseOnExit',
      extend({}, baseObj, {
        get: function () {
          return _pauseOnExit;
        },
        set: function (value: boolean) {
          _pauseOnExit = !!value;
        },
      })
    );

    Object.defineProperty(
      cue,
      'startTime',
      extend({}, baseObj, {
        get: function () {
          return _startTime;
        },
        set: function (value: number) {
          if (typeof value !== 'number') {
            throw new TypeError('Start time must be set to a number.');
          }

          _startTime = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'endTime',
      extend({}, baseObj, {
        get: function () {
          return _endTime;
        },
        set: function (value: number) {
          if (typeof value !== 'number') {
            throw new TypeError('End time must be set to a number.');
          }

          _endTime = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'text',
      extend({}, baseObj, {
        get: function () {
          return _text;
        },
        set: function (value: string) {
          _text = '' + value;
          this.hasBeenReset = true;
        },
      })
    );

    // todo: implement VTTRegion polyfill?
    Object.defineProperty(
      cue,
      'region',
      extend({}, baseObj, {
        get: function () {
          return _region;
        },
        set: function (value: any) {
          _region = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'vertical',
      extend({}, baseObj, {
        get: function () {
          return _vertical;
        },
        set: function (value: string) {
          const setting = findDirectionSetting(value);
          // Have to check for false because the setting an be an empty string.
          if (setting === false) {
            throw new SyntaxError(
              'An invalid or illegal string was specified.'
            );
          }

          _vertical = setting;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'snapToLines',
      extend({}, baseObj, {
        get: function () {
          return _snapToLines;
        },
        set: function (value: boolean) {
          _snapToLines = !!value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'line',
      extend({}, baseObj, {
        get: function () {
          return _line;
        },
        set: function (value: number | 'auto') {
          if (typeof value !== 'number' && value !== 'auto') {
            throw new SyntaxError(
              'An invalid number or illegal string was specified.'
            );
          }

          _line = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'lineAlign',
      extend({}, baseObj, {
        get: function () {
          return _lineAlign;
        },
        set: function (value: string) {
          const setting = findAlignSetting(value);
          if (!setting) {
            throw new SyntaxError(
              'An invalid or illegal string was specified.'
            );
          }

          _lineAlign = setting;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'position',
      extend({}, baseObj, {
        get: function () {
          return _position;
        },
        set: function (value: number) {
          if (value < 0 || value > 100) {
            throw new Error('Position must be between 0 and 100.');
          }

          _position = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'positionAlign',
      extend({}, baseObj, {
        get: function () {
          return _positionAlign;
        },
        set: function (value: string) {
          const setting = findAlignSetting(value);
          if (!setting) {
            throw new SyntaxError(
              'An invalid or illegal string was specified.'
            );
          }

          _positionAlign = setting;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'size',
      extend({}, baseObj, {
        get: function () {
          return _size;
        },
        set: function (value: number) {
          if (value < 0 || value > 100) {
            throw new Error('Size must be between 0 and 100.');
          }

          _size = value;
          this.hasBeenReset = true;
        },
      })
    );

    Object.defineProperty(
      cue,
      'align',
      extend({}, baseObj, {
        get: function () {
          return _align;
        },
        set: function (value: string) {
          const setting = findAlignSetting(value);
          if (!setting) {
            throw new SyntaxError(
              'An invalid or illegal string was specified.'
            );
          }

          _align = setting;
          this.hasBeenReset = true;
        },
      })
    );

    /**
     * Other <track> spec defined properties
     */

    // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state
    cue.displayState = undefined;
  }

  /**
   * VTTCue methods
   */

  VTTCue.prototype.getCueAsHTML = function () {
    // Assume WebVTT.convertCueToDOMTree is on the global.
    const WebVTT = (self as any).WebVTT;
    return WebVTT.convertCueToDOMTree(self, this.text);
  };
  // this is a polyfill hack
  return VTTCue as any as VTTCuePolyfill;
})();