track/track.js

const { TrackProperties } = require('./properties')
const { Source } = require('../source')
const Resource = require('nanoresource')
const assert = require('assert')
const ready = require('nanoresource-ready')
const debug = require('debug')('little-media-box:track')
const uuid = require('uuid/v4')

const {
  TrackPropertiesMissingFormatError,
  TrackPropertiesMissingStreamError,
  TrackTypeMismatchError
} = require('./error')

// quick util
const toTrackTypeName = (f) => f.name.toLowerCase().replace('track', '')
const noop = () => void 0

/**
 * The `Track` class represents a base class for other track classes
 * such as `VideoTrack`, `AudioTrack`, and `SubtitleTrack`.
 * @public
 * @class
 * @extends nanoresource
 */
class Track extends Resource {

  /**
   * Creates a new `Track` instance from input where
   * input may be another track, a `Source` instance or
   * some input for creating a `Source` instance, such as
   * URI string.
   * @static
   * @param {Source|String} source
   * @param {?(Object|Number)} opts
   * @param {?(Number)} opts.streamIndex
   * @return {Track}
   */
  static from(source, opts) {
    if ('number' === typeof opts) {
      opts = { streamIndex: opts }
    }

    if (!opts || 'object' !== typeof opts) {
      opts = {}
    }

    let srcProbe = null
    let streams = null
    const srcTracks = { audio: [], video: []}
    if (opts.probe) {
      srcProbe = opts.probe
      if (srcProbe.streams) {
        streams = srcProbe.streams
        streams.forEach(s => {
          switch(s.codec_type) {
            case 'video':
              srcTracks.video.push(s.index)
              break
            case 'audio':
            default:
              srcTracks.audio.push(s.index)
              break
          }
        })
      }
    }

    // coerce source into `Source` instance
    source = Source.from(source, opts)

    // derive stream index from the class level property `DEFAULT_STREAM_INDEX`
    // where `opts.index` takes precedence and `0` is the default
    //const { streamIndex = srcTracks[this.STREAM_TYPE][0] || this.DEFAULT_STREAM_INDEX || 0 } = opts
    // return new this(source, streamIndex, opts)
    // console.log('STREAMINDEX-OPTS', streamIndex, opts)
    return new this(source, srcTracks[this.STREAM_TYPE][0] || 0, opts)
    // return new this(source, srcTracks.audio[0] || streamIndex, opts)
  }

  /**
   * Computes the track type name for the class.
   * @static
   * @accessor
   * @type {String}
   */
  static get STREAM_TYPE() {
    return toTrackTypeName(this)
  }

  /**
   * Default stream index for a `Track`.
   * @static
   * @accessor
   * @type {Number}
   */
  static get DEFAULT_STREAM_INDEX() {
    return 0
  }

  /**
   * `Track` class constructor.
   * @param {Source} source
   * @param {Number} streamIndex
   * @param {?(Object)} opts
   * @param {?(String)} opts.id
   */
  constructor(source, streamIndex, opts) {
    super()

    if (!opts || 'object' !== typeof opts) {
      opts = {}
    }

    if ('number' !== typeof streamIndex || Number.isNaN(streamIndex)) {
      streamIndex = this.constructor.DEFAULT_STREAM_INDEX
    }

    assert(source instanceof Source,
      'Expecting `source` to be an instance of `Source`.')

    assert(streamIndex >= 0,
      'Expecting `streamIndex` to be >= 0. Got: ' + streamIndex)

    this.id = opts.id || uuid()
    this.source = source
    this.properties = new TrackProperties(this, { streamIndex })
  }

  /**
   * The track's source media type represented as a string (audio, video,
   * subtitle, etc).
   *
   * The static class property `STREAM_TYPE` on the instance's class constructor
   * is used to determine the track type falling back to the class name,
   * lower cased, with the string `/track/i` removed.
   * @accessor
   * @type {String}
   */
  get type() {
    if ('STREAM_TYPE' in this.constructor) {
      return this.constructor.STREAM_TYPE
    } else {
      return toTrackTypeName(this.constructor) || 'track'
    }
  }

  /**
   * The duration in seconds of the track's source stream.
   * @accessor
   * @type {Number}
   */
  get duration() {
    return this.properties.duration
  }

  /**
   * The language code for the track's source.
   * @accessor
   * @type {String}
   */
  get language() {
    return this.properties.language
  }

  /**
   * An object of known tags found in the track's source
   * container format and stream.
   * @accessor
   * @type {?(Object)}
   */
  get tags() {
    return this.properties.tags
  }

  /**
   * This property will be `true` if the track is the primary
   * track in the source container.
   * @accessor
   * @type {Boolean}
   */
  get isPrimary() {
    return this.properties.isPrimary
  }

  /**
   * The stream index for this track.
   * @accessor
   * @type {Number}
   */
  get streamIndex() {
    return this.properties.streamIndex
  }

  /**
   * Implements the abstract `_open()` method for `nanoresource`
   * Opens the internal source stream, probes for stream information,
   * and initializes track state based on the stream index.
   * @protected
   * @param {Function} callback
   */
  _open(callback) {
    this.properties.update((err) => {
      if (err) { return callback(err) }
      if (this.type !== this.properties.stream.codec_type) {
        callback(new TrackTypeMismatchError(this))
      } else {
        callback(null)
      }
    })
  }

  /**
   * Implements the abstract `_close()` method for `nanoresource`
   * Closes the resets internal state.
   * @protected
   * @param {Function} callback
   */
  _close(callback) {
    this.properties.reset(callback)
  }

  /**
   * Default abstract method implementation for `_validate()`
   * that does nothing but call `callback` on the next tick.
   * @protected
   * @abstract
   * @param {Function} callback
   */
  _validate(callback) {
    process.nextTick(callback, null)
  }

  /**
   * Wait for track and track source to be ready (opened) calling
   * `callback()` when it is.
   * @param {Function} callback
   */
  ready(callback) {
    assert('function' === typeof callback,
      'Expecting callback to be a function.')
    ready(this, (err) => {
      if (err) { return callback(err) }
      this.source.ready(callback)
    })
  }

  /**
   * Validates the track state properties calling `_validate(callback)`
   * for extending validation. Successful validation should not throw
   * an error.
   * @param {Function} callback
   */
  validate(callback) {
    assert('function' === typeof callback,
      'Expecting callback to be a function.')
    this.ready((err) => {
      if (err) { return callback(err) }
      const { stream, format } = this.properties

      if (null === stream) {
        const error = new TrackPropertiesMissingStreamError(this)
        return callback(TrackValidationError.from(error))
      }

      if (null === format) {
        const error = new TrackPropertiesMissingFormatError(this)
        return callback(TrackValidationError.from(error))
      }

      this._validate(callback)
    })
  }
}

/**
 * Module exports.
 */
module.exports = {
  Track
}