video-js-hls_test.js 10.9 KB
(function (window) {
  /*
    ======== A Handy Little QUnit Reference ========
    http://api.qunitjs.com/

    Test methods:
    module(name, {[setup][ ,teardown]})
    test(name, callback)
    expect(numberOfAssertions)
    stop(increment)
    start(decrement)
    Test assertions:
    ok(value, [message])
    equal(actual, expected, [message])
    notEqual(actual, expected, [message])
    deepEqual(actual, expected, [message])
    notDeepEqual(actual, expected, [message])
    strictEqual(actual, expected, [message])
    notStrictEqual(actual, expected, [message])
    throws(block, [expected], [message])
  */
  var
    manifestController,
    segmentController,
    m3u8parser,
    parser,

    expectedHeader = [
      0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
      0x09, 0x00, 0x00, 0x00, 0x00
    ],
    testAudioTag,
    testVideoTag,
    testScriptTag,
    asciiFromBytes,
    testScriptString,
    testScriptEcmaArray;

  module('environment');

  test('is sane', function () {
    expect(1);
    ok(true);
  });

  module('segment parser', {
    setup: function () {
      parser = new window.videojs.hls.SegmentParser();
    }
  });

  test('creates an flv header', function () {
    var header = Array.prototype.slice.call(parser.getFlvHeader());
    ok(header, 'the header is truthy');
    equal(9 + 4, header.length, 'the header length is correct');
    equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"');
    equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"');
    equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"');

    deepEqual(expectedHeader, header, 'the rest of the header is correct');
  });

  test('parses the first bipbop segment', function () {
    var tag, bytes, i;
    parser.parseSegmentBinaryData(window.bcSegment);

    ok(parser.tagsAvailable(), 'tags are available');

    console.log('h264 tags:', parser.stats.h264Tags(),
		'aac tags:', parser.stats.aacTags());
  });

  testAudioTag = function (tag) {
    var
      byte = tag.bytes[11],
      format = (byte & 0xF0) >>> 4,
      soundRate = byte & 0x03,
      soundSize = (byte & 0x2) >>> 1,
      soundType = byte & 0x1,
      aacPacketType = tag.bytes[12];

    equal(10, format, 'the audio format is aac');
    equal(3, soundRate, 'the sound rate is 44kHhz');
    equal(1, soundSize, 'the sound size is 16-bit samples');
    equal(1, soundType, 'the sound type is stereo');

    ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type');
  };

  testVideoTag = function (tag) {
    var
      byte = tag.bytes[11],
      frameType = (byte & 0xF0) >>> 4,
      codecId = byte & 0x0F,
      packetType = tag.bytes[12],
      compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8,
      nalHeader;

    // payload starts at tag.bytes[16]


    // XXX: I'm not sure that frame types 3-5 are invalid
    ok(frameType === 1 || frameType === 2,
       'the frame type should be valid');

    equal(7, codecId, 'the codec ID is AVC for h264');
    ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
    if (packetType !== 1) {
      equal(0,
	    compositionTime,
	    'the composition time is zero for non-NALU packets');
    }

    // TODO: the rest of the bytes are an NLU unit
    if (packetType === 0) {
      // AVC decoder configuration record
    } else {
      // NAL units
      testNalUnit(tag.bytes.subarray(16));
    }
  };

  testNalUnit = function (bytes) {
    var
      nalHeader = bytes[0],
      unitType = nalHeader & 0x1F;

    equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
    // equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
    // ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
    // ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
  };


  asciiFromBytes = function (bytes) {
    var
      string = [],
      i = bytes.byteLength;

    while (i--) {
      string[i] = String.fromCharCode(bytes[i]);
    }
    return string.join('');
  };

  testScriptString = function (tag, offset, expected) {
    var
      type = tag.bytes[offset],
      stringLength = tag.view.getUint16(offset + 1),
      string,
    i = expected.length;

    equal(2, type, 'the script element is of string type');
    equal(stringLength, expected.length, 'the script string length is correct');
    string = asciiFromBytes(tag.bytes.subarray(offset + 3,
			                       offset + 3 + stringLength));
    equal(expected, string, 'the string value is "' + expected + '"');
  };

  testScriptEcmaArray = function (tag, start) {
    var
      numItems = tag.view.getUint32(start),
      i = numItems,
      offset = start + 4,
      length,
      type;

    while (i--) {
      length = tag.view.getUint16(offset);

      // advance offset to the property value
      offset += 2 + length;

      type = tag.bytes[offset];
      ok(type === 1 || type === 0,
	 'the ecma array property value type is number or boolean');
      offset++;
      if (type) {
	// boolean
	ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1,
	   'the script boolean value is 0 or 1');
	offset++;
      } else {
	// number
	ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
	offset += 8;
      }
    }
    equal(tag.bytes[offset], 0, 'the property array terminator is valid');
    equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid');
    equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid');
  };

  testScriptTag = function (tag) {
    testScriptString(tag, 11, 'onMetaData');

    // the onMetaData object is stored as an 'ecma array', an array with non-
    // integer indices (i.e. a dictionary or hash-map).
    equal(8, tag.bytes[24], 'onMetaData is of ecma array type');
    testScriptEcmaArray(tag, 25);
  };

  test('the flv tags are well-formed', function () {
    var
      tag,
      byte,
      type,
      lastTime = 0;
      parser.parseSegmentBinaryData(window.bcSegment);

    while (parser.tagsAvailable()) {
      tag = parser.getNextTag();
      type = tag.bytes[0];

      // generic flv headers
      ok(type === 8 || type === 9 || type === 18,
	 'the type field specifies audio, video or script');

      byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8;
      equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct');

      byte = tag.view.getUint32(5) & 0xFFFFFF00;
      ok(byte >= lastTime, 'the timestamp for the tag is greater than zero');
      lastTime = byte;

      // tag type-specific headers
      ({
	8: testAudioTag,
	9: testVideoTag,
	18: testScriptTag
      })[type](tag);

      // previous tag size
      equal(tag.bytes.byteLength - 4,
	    tag.view.getUint32(tag.bytes.byteLength - 4),
	    'the size of the previous tag is correct');
    }
  });

  /*
    M3U8 Test Suite
  */

  module('m3u8 parser', {
    setup: function () {
      m3u8parser = new window.videojs.hls.M3U8Parser();
    }
  });

  test('should create my parser', function () {
    ok(m3u8parser != undefined);
  });

  test('should successfully parse manifest data', function () {
    var parsedData = m3u8parser.parse(window.playlistData);
    ok(parsedData);
  });

  test('test for expected results', function () {
    var data = m3u8parser.parse(window.playlistData);

    notEqual(data, null, 'data is not NULL');
    equal(data.invalidReasons.length, 0, 'data has 0 invalid reasons');
    equal(data.hasValidM3UTag, true, 'data has valid EXTM3U');
    equal(data.targetDuration, 10, 'data has correct TARGET DURATION');
    equal(data.allowCache, "NO", 'acceptable ALLOW CACHE');
    equal(data.isPlaylist, true, 'data is parsed as a PLAYLIST as expected');
    equal(data.playlistType, "VOD", 'acceptable PLAYLIST TYPE');
    equal(data.mediaItems.length, 16, 'acceptable mediaItem count');
    equal(data.mediaSequence, 0, 'MEDIA SEQUENCE is correct');
    equal(data.totalDuration, -1, "ZEN TOTAL DURATION is unknown as expected");
    equal(data.hasEndTag, true, 'should have ENDLIST tag');
  });

  module('brightcove playlist', {
    setup: function () {
      m3u8parser = new window.videojs.hls.M3U8Parser();
    }
  });

  test('should parse a brightcove manifest data', function () {
    var data = m3u8parser.parse(window.brightcove_playlist_data);

    ok(data);
    equal(data.playlistItems.length, 4, 'Has correct rendition count');
    equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
    equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
    equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
    equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');

  }
      );

  module('manifest controller', {
    setup: function () {
      manifestController = new window.videojs.hls.ManifestController();
      this.vjsget = vjs.get;
      vjs.get = function (url, success, error) {
	success(window.brightcove_playlist_data);
      };
    },
    teardown: function () {
      vjs.get = this.vjsget;
    }
  });

  test('should create', function () {
    ok(manifestController);
  });

  test('should return a parsed object', function () {
    var data = manifestController.parseManifest(window.brightcove_playlist_data);

    ok(data);
    equal(data.playlistItems.length, 4, 'Has correct rendition count');
    equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
    equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
    equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
    equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');
  });

  test('should get a manifest from hermes', function () {
    manifestController.loadManifest('http://example.com/16x9-master.m3u8',
                                    function(responseData) {
	                              ok(responseData);
                                    },
                                    function(errorData) {
                                      ok(false, 'does not error');
                                    },
                                    function(updateData) {});
  });

  module('segment controller', {
    setup: function () {
      segmentController = new window.videojs.hls.SegmentController();
      this.vjsget = vjs.get;
      vjs.get = function (url, success, error) {
	console.log('load segment url', url);
	success(window.bcSegment);
      };
    },
    teardown: function () {
      vjs.get = this.vjsget;
    }
  });

  test('bandwidth calulation test', function () {
    var
      multiSecondData = segmentController.calculateThroughput(10000, 1000, 2000),
      subSecondData = segmentController.calculateThroughput(10000, 1000, 1500);
    equal(multiSecondData, 80000, 'MULTI-Second bits per second calculation');
    equal(subSecondData, 160000, 'SUB-Second bits per second calculation');
  });
})(this);