<file_sep>{
  "allowCache": true,
  "mediaSequence": 0,
  "segments": [
    {
      "duration": 10,
      "uri": "00001.ts"
    },
    {
      "duration": 10,
      "uri": "00002.ts"
    }
  ],
  "targetDuration": 10
}
<file_sep>{
  "allowCache": true,
  "mediaSequence": 0,
  "segments": [
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/gogo/00001.ts"
    }
  ],
  "endList": true
}<file_sep>(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
    parser,

    expectedHeader = [
      0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
      0x09, 0x00, 0x00, 0x00, 0x00
    ],

    extend = window.videojs.util.mergeOptions,

    testAudioTag,
    testVideoTag,
    testScriptTag,
    asciiFromBytes,
    testScriptString,
    testScriptEcmaArray,
    testNalUnit;

  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 PMTs with program descriptors', function() {
    var
      makePmt = function(options) {
        var
          result = [],
          entryCount = 0,
          k,
          sectionLength;
        for (k in options.pids) {
          entryCount++;
        }
        // table_id
        result.push(0x02);
        // section_syntax_indicator '0' reserved section_length
        // 13 + (program_info_length) + (n * 5 + ES_info_length[n])
        sectionLength = 13 + (5 * entryCount) + 17;
        result.push(0x80 | (0xF00 & sectionLength >>> 8));
        result.push(sectionLength & 0xFF);
        // program_number
        result.push(0x00);
        result.push(0x01);
        // reserved version_number current_next_indicator
        result.push(0x01);
        // section_number
        result.push(0x00);
        // last_section_number
        result.push(0x00);
        // reserved PCR_PID
        result.push(0xe1);
        result.push(0x00);
        // reserved program_info_length
        result.push(0xf0);
        result.push(0x11); // hard-coded 17 byte descriptor
        // program descriptors
        result = result.concat([
          0x25, 0x0f, 0xff, 0xff,
          0x49, 0x44, 0x33, 0x20,
          0xff, 0x49, 0x44, 0x33,
          0x20, 0x00, 0x1f, 0x00,
          0x01
        ]);
        for (k in options.pids) {
          // stream_type
          result.push(options.pids[k]);
          // reserved elementary_PID
          result.push(0xe0 | (k & 0x1f00) >>> 8);
          result.push(k & 0xff);
          // reserved ES_info_length
          result.push(0xf0);
          result.push(0x00); // ES_info_length = 0
        }
        // CRC_32
        result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
        return result;
      },
      makePat = function(options) {
        var
          result = [],
          k;
        // table_id
        result.push(0x00);
        // section_syntax_indicator '0' reserved section_length
        result.push(0x80);
        result.push(0x0d); // section_length for one program
        // transport_stream_id
        result.push(0x00);
        result.push(0x00);
        // reserved version_number current_next_indicator
        result.push(0x01); // current_next_indicator is 1
        // section_number
        result.push(0x00);
        // last_section_number
        result.push(0x00);
        for (k in options.programs) {
          // program_number
          result.push((k & 0xFF00) >>> 8);
          result.push(k & 0x00FF);
          // reserved program_map_pid
          result.push((options.programs[k] & 0x1f00) >>> 8);
          result.push(options.programs[k] & 0xff);
        }
        return result;
      },
      makePsi = function(options) {
        var result = [];

        // pointer_field
        if (options.payloadUnitStartIndicator) {
          result.push(0x00);
        }
        if (options.programs) {
          return result.concat(makePat(options));
        }
        return result.concat(makePmt(options));
      },
      makePacket = function(options) {
        var
          result = [],
          settings = extend({
            payloadUnitStartIndicator: true,
            pid: 0x00
          }, options);

        // header
        // sync_byte
        result.push(0x47);
        // transport_error_indicator payload_unit_start_indicator transport_priority PID
        result.push((settings.pid & 0x1f) << 8 | 0x40);
        result.push(settings.pid & 0xff);
        // transport_scrambling_control adaptation_field_control continuity_counter
        result.push(0x10);
        result = result.concat(makePsi(settings));

        // ensure the resulting packet is the correct size
        result.length = window.videojs.Hls.SegmentParser.MP2T_PACKET_LENGTH;
        return result;
      },
      h264Type = window.videojs.Hls.SegmentParser.STREAM_TYPES.h264,
      adtsType = window.videojs.Hls.SegmentParser.STREAM_TYPES.adts;

    parser.parseSegmentBinaryData(new Uint8Array(makePacket({
      programs: {
        0x01: [0x01]
      }
    }).concat(makePacket({
      pid: 0x01,
      pids: {
        0x02: h264Type, // h264 video
        0x03: adtsType // adts audio
      }
    }))));

    strictEqual(parser.stream.pmtPid, 0x01, 'PMT PID is 1');
    strictEqual(parser.stream.programMapTable[h264Type], 0x02, 'video is PID 2');
    strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
  });

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

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

  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;

    // 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;

    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
      byte,
      tag,
      type,
      currentPts = 0,
      lastTime = 0;
    parser.parseSegmentBinaryData(window.bcSegment);

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

      ok(tag.pts >= currentPts, 'presentation time stamps are increasing');
      currentPts = tag.pts;

      // generic flv headers
      switch (type) {
        case 8: ok(true, 'the type is audio');
        break;
        case 9: ok(true, 'the type is video');
        break;
        case 18: ok(true, 'the type is script');
        break;
        default: ok(false, 'the type (' + type + ') is unrecognized');
      }

      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,
         'timestamp is increasing. last pts: ' + lastTime + ' this pts: ' + byte);
      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');
    }
  });
})(window);
<file_sep>{
  "allowCache": true,
  "mediaSequence": 0,
  "playlistType": "EVENT",
  "segments": [
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
    },
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
    },
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
    },
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
    },
    {
      "duration": 10,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
    },
    {
      "duration": 8,
      "uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
    }
  ],
  "targetDuration": 10,
  "endList": true
}<file_sep>{
  "allowCache": false,
  "mediaSequence": 0,
  "playlistType": "VOD",
  "segments": [
    {
      "byterange": {
        "length": 522828,
        "offset": 0
      },
      "duration": 10,
      "uri": "hls_450k_video.ts"
    }
  ],
  "targetDuration": 10,
  "endList": true
}<file_sep>// Karma example configuration file
// NOTE: To configure Karma tests, do the following:
//       1.  Copy this file and rename the copy with a .conf.js extension, for example:  karma.conf.js
//       2.  Configure the properties below in your conf.js copy
//       3.  Run your tests

module.exports = function(config) {
  var customLaunchers = {
    chrome_sl: {
      singleRun: true,
      base: 'SauceLabs',
      browserName: 'chrome',
      platform: 'Windows 7',
      version: '34'
    },

    firefox_sl: {
      singleRun: true,
      base: 'SauceLabs',
      browserName: 'firefox',
      platform: 'Windows 8'
    },

    safari_sl: {
      singleRun: true,
      base: 'SauceLabs',
      browserName: 'safari',
      platform: 'OS X 10.8'
    },

    ipad_sl: {
      singleRun: true,
      base: 'SauceLabs',
      browserName: 'ipad',
      platform:'OS X 10.9',
      version: '7.1'
    },

    android_sl: {
      singleRun: true,
      base: 'SauceLabs',
      browserName: 'android',
      platform:'Linux'
    }
  };

  config.set({
    // base path, that will be used to resolve files and exclude
    basePath: '',

    frameworks: ['qunit'],

    // Set autoWatch to true if you plan to run `grunt karma` continuously, to automatically test changes as you make them.
    autoWatch: false,

    // Setting singleRun to true here will start up your specified browsers, run tests, and then shut down the browsers.  Helpful to have in a CI environment, where you don't want to leave browsers running continuously.
    singleRun: true,

    // custom launchers for sauce labs
    //define SL browsers
    customLaunchers: customLaunchers,

    // Start these browsers
    browsers: ['chrome_sl', 'ipad_sl'], //Object.keys(customLaunchers),

    // List of files / patterns to load in the browser
    // Add any new src files to this list.
    // If you add new unit tests, they will be picked up automatically by Karma,
    // unless you've added them to a nested directory, in which case you should
    // add their paths to this list.

    files: [
      '../node_modules/sinon/lib/sinon.js',
      '../node_modules/sinon/lib/sinon/util/event.js',
      '../node_modules/sinon/lib/sinon/util/fake_xml_http_request.js',
      '../node_modules/sinon/lib/sinon/util/xhr_ie.js',
      '../node_modules/sinon/lib/sinon/util/fake_timers.js',
      '../node_modules/video.js/dist/video-js/video.js',
      '../node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js',
      '../node_modules/pkcs7/dist/pkcs7.unpad.js',
      '../test/karma-qunit-shim.js',
      '../src/videojs-hls.js',
      '../src/xhr.js',
      '../src/flv-tag.js',
      '../src/exp-golomb.js',
      '../src/h264-stream.js',
      '../src/aac-stream.js',
      '../src/segment-parser.js',
      '../src/stream.js',
      '../src/m3u8/m3u8-parser.js',
      '../src/playlist-loader.js',
      '../src/decrypter.js',
      '../tmp/manifests.js',
      '../tmp/expected.js',
      'tsSegment-bc.js',
      '../src/bin-utils.js',
      '../test/*.js'
      ],

    plugins: [
      'karma-qunit',
      'karma-chrome-launcher',
      'karma-firefox-launcher',
      'karma-ie-launcher',
      'karma-opera-launcher',
      'karma-phantomjs-launcher',
      'karma-safari-launcher',
      'karma-sauce-launcher'
    ],

    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit'
    reporters: ['dots', 'progress'],

    // web server port
    port: 9876,

    // cli runner port
    runnerPort: 9100,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    //logLevel: config.LOG_INFO,

    // If browser does not capture in given timeout [ms], kill it
    captureTimeout: 60000,

    // global config for SauceLabs
    sauceLabs: {
      startConnect: true,
      tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER,
      build: process.env.TRAVIS_BUILD_NUMBER,
      testName: process.env.TRAVIS_BUILD_NUMBER + process.env.TRAVIS_BRANCH,
      recordScreenshots: false
    }
  });
};
<file_sep>{
  "allowCache": true,
  "mediaSequence": 0,
  "playlistType": "VOD",
  "segments": [
    {
      "duration": 6.64,
      "uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
    },
    {
      "duration": 8,
      "uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
    },
    {
      "duration": 8,
      "uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
    }
  ],
  "targetDuration": 8
}
<file_sep># Live HLS Research
This document is a collection of notes on Live HLS implementations in the wild.

There are two varieties of Live HLS. In the first, playlists are
persistent and strictly appended to. In the alternative form, the
maximum number of segments in a playlist is relatively stable and an
old segment is removed every time a new segment becomes available.

On iOS devices, both stream types report a duration of `Infinity`. The
`currentTime` is equal to the amount of the stream that has been
played back on the device.

## Akamai HD2

## OnceLIVE
"Sliding window" live streams.

### Variant Playlists
Once variant playlists look like standard HLS variant playlists.

### Media Playlists
OnceLIVE uses "sliding window" manifests for live playback. The media
playlists do not have an `EXT-X-ENDLIST` and don't declare a
`EXT-X-PLAYLIST-TYPE`.  On first request, the stream media playlist
returned four segment URLs with a starting media sequence of one,
preceded by a `EXT-X-DISCONTINUITY` tag. As playback progressed, that
number grew to 13 segment URLs, at which point it stabilized. That
would equate to a steady-state 65 second window at 5 seconds per
segment.

OnceLive documentation is [available on the Unicorn Media
website](http://www.unicornmedia.com/documents/2013/02/oncelive_implementationguide.pdf).

Here's a script to quickly parse out segment URLs:

```shell
curl $ONCE_MEDIA_PLAYLIST | grep '^http'
```

An example media playlist might look something like this:
```m3u8
#EXTM3U
#EXT-X-TARGETDURATION:5
#EXT-X-MEDIA-SEQUENCE:3
#EXTINF:5,3
http://example.com/0/1/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,4
http://example.com/1/2/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,5
http://example.com/2/3/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
#EXTINF:5,6
http://example.com/3/4/content.ts?visitguid=uuid&asseturl=http://once.example.com/asset.lrm&failoverurl=http://example.com/blank.jpg
```

## Zencoder Live
<file_sep>(function(window, videojs, undefined) {
  'use strict';
  /*
    ======== 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])
  */

// see docs/hlse.md for instructions on how test data was generated

var stringFromBytes = function(bytes) {
  var result = '', i;

  for (i = 0; i < bytes.length; i++) {
    result += String.fromCharCode(bytes[i]);
  }
  return result;
};

module('Decryption');

test('decrypts a single AES-128 with PKCS7 block', function() {
  var
    key =  new Uint32Array([0, 0, 0, 0]),
    initVector = key,
    // the string "<NAME>" encrypted
    encrypted = new Uint8Array([
      0xce, 0x90, 0x97, 0xd0,
      0x08, 0x46, 0x4d, 0x18,
      0x4f, 0xae, 0x01, 0x1c,
      0x82, 0xa8, 0xf0, 0x67]);

  deepEqual('how<NAME>',
            stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
            'decrypted with a byte array key');
});

test('decrypts multiple AES-128 blocks with CBC', function() {
  var
    key = new Uint32Array([0, 0, 0, 0]),
    initVector = key,
    // the string "0123456789abcdef01234" encrypted
    encrypted = new Uint8Array([
      0x14, 0xf5, 0xfe, 0x74,
      0x69, 0x66, 0xf2, 0x92,
      0x65, 0x1c, 0x22, 0x88,
      0xbb, 0xff, 0x46, 0x09,

      0x0b, 0xde, 0x5e, 0x71,
      0x77, 0x87, 0xeb, 0x84,
      0xa9, 0x54, 0xc2, 0x45,
      0xe9, 0x4e, 0x29, 0xb3
    ]);

  deepEqual('0123456789abcdef01234',
            stringFromBytes(videojs.Hls.decrypt(encrypted, key, initVector)),
            'decrypted multiple blocks');
});

})(window, window.videojs);
<file_sep>var grunt = require('grunt'),
    extname = require('path').extname;

grunt.file.recurse(process.cwd(), function(path) {
  var json;
  if (extname(path) === '.json') {
    json = grunt.file.readJSON(path);
    if (json.totalDuration) {
      delete json.totalDuration;
      grunt.file.write(path, JSON.stringify(json, null, '  '));
    }
  }
});
<file_sep>(function(window) {
  'use strict';
  var
    sinonXhr,
    clock,
    requests,
    videojs = window.videojs,

    // Attempts to produce an absolute URL to a given relative path
    // based on window.location.href
    urlTo = function(path) {
      return window.location.href
        .split('/')
        .slice(0, -1)
        .concat([path])
        .join('/');
    };

  module('Playlist Loader', {
    setup: function() {
      // fake XHRs
      sinonXhr = sinon.useFakeXMLHttpRequest();
      requests = [];
      sinonXhr.onCreate = function(xhr) {
        // force the XHR2 timeout polyfill
        xhr.timeout = undefined;
        requests.push(xhr);
      };

      // fake timers
      clock = sinon.useFakeTimers();
    },
    teardown: function() {
      sinonXhr.restore();
      clock.restore();
    }
  });

  test('throws if the playlist url is empty or undefined', function() {
    throws(function() {
      videojs.Hls.PlaylistLoader();
    }, 'requires an argument');
    throws(function() {
      videojs.Hls.PlaylistLoader('');
    }, 'does not accept the empty string');
  });

  test('starts without any metadata', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    strictEqual(loader.state, 'HAVE_NOTHING', 'no metadata has loaded yet');
  });

  test('requests the initial playlist immediately', function() {
    new videojs.Hls.PlaylistLoader('master.m3u8');
    strictEqual(requests.length, 1, 'made a request');
    strictEqual(requests[0].url, 'master.m3u8', 'requested the initial playlist');
  });

  test('moves to HAVE_MASTER after loading a master playlist', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:\n' +
                           'media.m3u8\n');
    ok(loader.master, 'the master playlist is available');
    strictEqual(loader.state, 'HAVE_MASTER', 'the state is correct');
  });

  test('jumps to HAVE_METADATA when initialized with a media playlist', function() {
    var
      loadedmetadatas = 0,
      loader = new videojs.Hls.PlaylistLoader('media.m3u8');
    loader.on('loadedmetadata', function() {
      loadedmetadatas++;
    });
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    ok(loader.master, 'infers a master playlist');
    ok(loader.media(), 'sets the media playlist');
    ok(loader.media().uri, 'sets the media playlist URI');
    strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
    strictEqual(requests.length, 0, 'no more requests are made');
    strictEqual(loadedmetadatas, 1, 'fired one loadedmetadata');
  });

  test('jumps to HAVE_METADATA when initialized with a live media playlist', function() {
    var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    ok(loader.master, 'infers a master playlist');
    ok(loader.media(), 'sets the media playlist');
    strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  });

  test('moves to HAVE_METADATA after loading a media playlist', function() {
    var
      loadedPlaylist = 0,
      loadedMetadata = 0,
      loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    loader.on('loadedplaylist', function() {
      loadedPlaylist++;
    });
    loader.on('loadedmetadata', function() {
      loadedMetadata++;
    });
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:\n' +
                           'media.m3u8\n' +
                           'alt.m3u8\n');
    strictEqual(loadedPlaylist, 1, 'fired loadedplaylist once');
    strictEqual(loadedMetadata, 0, 'did not fire loadedmetadata');
    strictEqual(requests.length, 1, 'requests the media playlist');
    strictEqual(requests[0].method, 'GET', 'GETs the media playlist');
    strictEqual(requests[0].url,
                urlTo('media.m3u8'),
                'requests the first playlist');

    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    ok(loader.master, 'sets the master playlist');
    ok(loader.media(), 'sets the media playlist');
    strictEqual(loadedPlaylist, 2, 'fired loadedplaylist twice');
    strictEqual(loadedMetadata, 1, 'fired loadedmetadata once');
    strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  });

  test('moves to HAVE_CURRENT_METADATA when refreshing the playlist', function() {
    var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000); // 10s, one target duration
    strictEqual(loader.state, 'HAVE_CURRENT_METADATA', 'the state is correct');
    strictEqual(requests.length, 1, 'requested playlist');
    strictEqual(requests[0].url,
                urlTo('live.m3u8'),
                'refreshes the media playlist');
  });

  test('returns to HAVE_METADATA after refreshing the playlist', function() {
    var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000); // 10s, one target duration
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '1.ts\n');
    strictEqual(loader.state, 'HAVE_METADATA', 'the state is correct');
  });

  test('emits an error when an initial playlist request fails', function() {
    var
      errors = [],
      loader = new videojs.Hls.PlaylistLoader('master.m3u8');

    loader.on('error', function() {
      errors.push(loader.error);
    });
    requests.pop().respond(500);

    strictEqual(errors.length, 1, 'emitted one error');
    strictEqual(errors[0].status, 500, 'http status is captured');
  });

  test('errors when an initial media playlist request fails', function() {
    var
      errors = [],
      loader = new videojs.Hls.PlaylistLoader('master.m3u8');

    loader.on('error', function() {
      errors.push(loader.error);
    });
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:\n' +
                           'media.m3u8\n');

    strictEqual(errors.length, 0, 'emitted no errors');

    requests.pop().respond(500);

    strictEqual(errors.length, 1, 'emitted one error');
    strictEqual(errors[0].status, 500, 'http status is captured');
  });


  // http://tools.ietf.org/html/draft-pantos-http-live-streaming-12#section-6.3.4
  test('halves the refresh timeout if a playlist is unchanged' +
       'since the last reload', function() {
    new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000); // trigger a refresh
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(5 * 1000); // half the default target-duration

    strictEqual(requests.length, 1, 'sent a request');
    strictEqual(requests[0].url,
                urlTo('live.m3u8'),
                'requested the media playlist');
  });

  test('media-sequence updates are considered a playlist change', function() {
    new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000); // trigger a refresh
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:1\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(5 * 1000); // half the default target-duration

    strictEqual(requests.length, 0, 'no request is sent');
  });

  test('emits an error if a media refresh fails', function() {
    var
      errors = 0,
      errorResponseText = 'custom error message',
      loader = new videojs.Hls.PlaylistLoader('live.m3u8');

    loader.on('error', function() {
      errors++;
    });
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000); // trigger a refresh
    requests.pop().respond(500, null, errorResponseText);

    strictEqual(errors, 1, 'emitted an error');
    strictEqual(loader.error.status, 500, 'captured the status code');
    strictEqual(loader.error.responseText, errorResponseText, 'captured the responseText');
  });

  test('switches media playlists when requested', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n');

    loader.media(loader.master.playlists[1]);
    strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');

    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'high-0.ts\n');
    strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
    strictEqual(loader.media(),
                loader.master.playlists[1],
                'updated the active media');
  });

  test('can switch media playlists based on URI', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n');

    loader.media('high.m3u8');
    strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');

    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'high-0.ts\n');
    strictEqual(loader.state, 'HAVE_METADATA', 'switched active media');
    strictEqual(loader.media(),
                loader.master.playlists[1],
                'updated the active media');
  });

  test('aborts in-flight playlist refreshes when switching', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n');
    clock.tick(10 * 1000);
    loader.media('high.m3u8');
    strictEqual(requests[0].aborted, true, 'aborted refresh request');
    strictEqual(loader.state, 'SWITCHING_MEDIA', 'updated the state');
  });

  test('switching to the active playlist is a no-op', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    loader.media('low.m3u8');

    strictEqual(requests.length, 0, 'no requests are sent');
  });

  test('switching to the active live playlist is a no-op', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n');
    loader.media('low.m3u8');

    strictEqual(requests.length, 0, 'no requests are sent');
  });

  test('switches back to loaded playlists without re-requesting them', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    loader.media('high.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'high-0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    loader.media('low.m3u8');

    strictEqual(requests.length, 0, 'no outstanding requests');
    strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist');
  });

  test('aborts outstanding requests if switching back to an already loaded playlist', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    loader.media('high.m3u8');
    loader.media('low.m3u8');

    strictEqual(requests.length, 1, 'requested high playlist');
    ok(requests[0].aborted, 'aborted playlist request');
    strictEqual(loader.state, 'HAVE_METADATA', 'returned to loaded playlist');
    strictEqual(loader.media(), loader.master.playlists[0], 'switched to loaded playlist');
  });


  test('does not abort requests when the same playlist is re-requested', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           'low-0.ts\n' +
                           '#EXT-X-ENDLIST\n');
    loader.media('high.m3u8');
    loader.media('high.m3u8');

    strictEqual(requests.length, 1, 'made only one request');
    ok(!requests[0].aborted, 'request not aborted');
  });

  test('throws an error if a media switch is initiated too early', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');

    throws(function() {
      loader.media('high.m3u8');
    }, 'threw an error from HAVE_NOTHING');

    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    throws(function() {
      loader.media('high.m3u8');
    }, 'throws an error from HAVE_MASTER');
  });

  test('throws an error if a switch to an unrecognized playlist is requested', function() {
    var loader = new videojs.Hls.PlaylistLoader('master.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'media.m3u8\n');

    throws(function() {
      loader.media('unrecognized.m3u8');
    }, 'throws an error');
  });

  test('dispose cancels the refresh timeout', function() {
    var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    loader.dispose();
    // a lot of time passes...
    clock.tick(15 * 1000);

    strictEqual(requests.length, 0, 'no refresh request was made');
  });

  test('dispose aborts pending refresh requests', function() {
    var loader = new videojs.Hls.PlaylistLoader('live.m3u8');
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:0\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n');
    clock.tick(10 * 1000);

    loader.dispose();
    ok(requests[0].aborted, 'refresh request aborted');
  });

  test('errors if requests take longer than 45s', function() {
    var
      loader = new videojs.Hls.PlaylistLoader('media.m3u8'),
      errors = 0;
    loader.on('error', function() {
      errors++;
    });
    clock.tick(45 * 1000);

    strictEqual(errors, 1, 'fired one error');
    strictEqual(loader.error.code, 2, 'fired a network error');
  });

  test('triggers an event when the active media changes', function() {
    var
      loader = new videojs.Hls.PlaylistLoader('master.m3u8'),
      mediaChanges = 0;
    loader.on('mediachange', function() {
      mediaChanges++;
    });
    requests.pop().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
                           'low.m3u8\n' +
                           '#EXT-X-STREAM-INF:BANDWIDTH=2\n' +
                           'high.m3u8\n');
    requests.shift().respond(200, null,
                             '#EXTM3U\n' +
                             '#EXT-X-MEDIA-SEQUENCE:0\n' +
                             '#EXTINF:10,\n' +
                             'low-0.ts\n' +
                             '#EXT-X-ENDLIST\n');
    strictEqual(mediaChanges, 0, 'initial selection is not a media change');

    loader.media('high.m3u8');
    strictEqual(mediaChanges, 0, 'mediachange does not fire immediately');

    requests.shift().respond(200, null,
                             '#EXTM3U\n' +
                             '#EXT-X-MEDIA-SEQUENCE:0\n' +
                             '#EXTINF:10,\n' +
                             'high-0.ts\n' +
                             '#EXT-X-ENDLIST\n');
    strictEqual(mediaChanges, 1, 'fired a mediachange');

    // switch back to an already loaded playlist
    loader.media('low.m3u8');
    strictEqual(mediaChanges, 2, 'fired a mediachange');

    // trigger a no-op switch
    loader.media('low.m3u8');
    strictEqual(mediaChanges, 2, 'ignored a no-op media change');
  });

  test('does not misintrepret playlists missing newlines at the end', function() {
    var loader = new videojs.Hls.PlaylistLoader('media.m3u8');
    requests.shift().respond(200, null,
                             '#EXTM3U\n' +
                             '#EXT-X-MEDIA-SEQUENCE:0\n' +
                             '#EXTINF:10,\n' +
                             'low-0.ts\n' +
                             '#EXT-X-ENDLIST'); // no newline
     ok(loader.media().endList, 'flushed the final line of input');
  });
})(window);
<file_sep>(function(videojs) {
module('H264 Stream');

var
  nalUnitTypes = window.videojs.Hls.NALUnitType,
  FlvTag = window.videojs.Hls.FlvTag;

test('metadata is generated for IDRs after a full NAL unit is written', function() {
  var
    h264Stream = new videojs.Hls.H264Stream(),
    accessUnitDelimiter = new Uint8Array([
      0x00,
      0x00,
      0x01,
      nalUnitTypes.access_unit_delimiter_rbsp
    ]),
    seqParamSet = new Uint8Array([
      0x00,
      0x00,
      0x01,
      0x60 | nalUnitTypes.seq_parameter_set_rbsp,
      0x00, // profile_idc
      0x00, // constraint_set flags
      0x00, // level_idc
      // seq_parameter_set_id ue(v) 0 => 1
      // log2_max_frame_num_minus4 ue(v) 1 => 010
      // pic_order_cnt_type ue(v) 0 => 1
      // log2_max_pic_order_cnt_lsb_minus4 ue(v) 1 => 010
      // max_num_ref_frames ue(v) 1 => 010
      // gaps_in_frame_num_value_allowed u(1) 0
      // pic_width_in_mbs_minus1 ue(v) 0 => 1
      // pic_height_in_map_units_minus1 ue(v) 0 => 1
      // frame_mbs_only_flag u(1) 1
      // direct_8x8_inference_flag u(1) 0
      // frame_cropping_flag u(1) 0
      // vui_parameters_present_flag u(1) 0
      // 1010 1010 0100 1110 00(00 0000)
      0xAA,
      0x4E,
      0x00
    ]),
    idr = new Uint8Array([
      0x00,
      0x00,
      0x01,
      nalUnitTypes.slice_layer_without_partitioning_rbsp_idr
    ]);

  h264Stream.setNextTimeStamp(0, 0, true);
  h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
  h264Stream.writeBytes(seqParamSet, 0, seqParamSet.byteLength);
  h264Stream.writeBytes(idr, 0, idr.byteLength);
  h264Stream.setNextTimeStamp(1, 1, true);

  strictEqual(h264Stream.tags.length, 3, 'three tags are written');
  ok(FlvTag.isMetaData(h264Stream.tags[0].bytes),
     'metadata is written');
  ok(FlvTag.isVideoFrame(h264Stream.tags[1].bytes),
     'picture parameter set is written');
  ok(h264Stream.tags[2].keyFrame, 'key frame is written');
});

test('starting PTS values can be negative', function() {
  var
    h264Stream = new videojs.Hls.H264Stream(),
    accessUnitDelimiter = new Uint8Array([
      0x00,
      0x00,
      0x01,
      nalUnitTypes.access_unit_delimiter_rbsp
    ]);

  h264Stream.setNextTimeStamp(-100, -100, true);
  h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
  h264Stream.setNextTimeStamp(-99, -99, true);
  h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
  h264Stream.setNextTimeStamp(0, 0, true);
  h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);
  // flush out the last tag
  h264Stream.writeBytes(accessUnitDelimiter, 0, accessUnitDelimiter.byteLength);

  strictEqual(h264Stream.tags.length, 3, 'three tags are ready');
  strictEqual(h264Stream.tags[0].pts, 0, 'the first PTS is zero');
  strictEqual(h264Stream.tags[0].dts, 0, 'the first DTS is zero');
  strictEqual(h264Stream.tags[1].pts, 1, 'the second PTS is one');
  strictEqual(h264Stream.tags[1].dts, 1, 'the second DTS is one');

  strictEqual(h264Stream.tags[2].pts, 100, 'the third PTS is 100');
  strictEqual(h264Stream.tags[2].dts, 100, 'the third DTS is 100');
});

})(window.videojs);
<file_sep>(function(window, videojs, undefined) {
  'use strict';
  /*
    ======== 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
  player,
  oldMediaSourceOpen,
  oldSegmentParser,
  oldSetTimeout,
  oldSourceBuffer,
  oldFlashSupported,
  oldNativeHlsSupport,
  oldDecrypt,
  requests,
  xhr,

  createPlayer = function(options) {
    var tech, video, player;
    video = document.createElement('video');
    document.querySelector('#qunit-fixture').appendChild(video);
    player = videojs(video, {
      flash: {
        swf: ''
      },
      hls: options || {}
    });

    player.buffered = function() {
      return videojs.createTimeRange(0, 0);
    };

    tech = player.el().querySelector('.vjs-tech');
    tech.vjs_getProperty = function() {};
    tech.vjs_setProperty = function() {};
    tech.vjs_src = function() {};
    tech.vjs_play = function() {};
    videojs.Flash.onReady(tech.id);

    return player;
  },
  openMediaSource = function(player) {
    player.hls.mediaSource.trigger({
      type: 'sourceopen'
    });
    // endOfStream triggers an exception if flash isn't available
    player.hls.mediaSource.endOfStream = function() {};
  },
  standardXHRResponse = function(request) {
    if (!request.url) {
      return;
    }

    var contentType = "application/json",
        // contents off the global object
        manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);

    if (manifestName) {
      manifestName = manifestName[1];
    } else {
      manifestName = request.url;
    }

    if (/\.m3u8?/.test(request.url)) {
      contentType = 'application/vnd.apple.mpegurl';
    } else if (/\.ts/.test(request.url)) {
      contentType = 'video/MP2T';
    }

    request.response = new Uint8Array([1]).buffer;
    request.respond(200,
                    { 'Content-Type': contentType },
                    window.manifests[manifestName]);
  },

  mockSegmentParser = function(tags) {
    if (tags === undefined) {
      tags = [];
    }
    return function() {
      this.getFlvHeader = function() {
        return 'flv';
      };
      this.parseSegmentBinaryData = function() {};
      this.flushTags = function() {};
      this.tagsAvailable = function() {
        return tags.length;
      };
      this.getTags = function() {
        return tags;
      };
      this.getNextTag = function() {
        return tags.shift();
      };
    };
  };

module('HLS', {
  setup: function() {
    oldMediaSourceOpen = videojs.MediaSource.open;
    videojs.MediaSource.open = function() {};

    // mock out Flash features for phantomjs
    oldFlashSupported = videojs.Flash.isSupported;
    videojs.Flash.isSupported = function() {
      return true;
    };

    oldSourceBuffer = window.videojs.SourceBuffer;
    window.videojs.SourceBuffer = function() {
      this.appendBuffer = function() {};
      this.abort = function() {};
    };

    // store functionality that some tests need to mock
    oldSegmentParser = videojs.Hls.SegmentParser;
    oldSetTimeout = window.setTimeout;

    oldNativeHlsSupport = videojs.Hls.supportsNativeHls;

    oldDecrypt = videojs.Hls.decrypt;
    videojs.Hls.decrypt = function() {
      return new Uint8Array([0]);
    };

    // fake XHRs
    xhr = sinon.useFakeXMLHttpRequest();
    requests = [];
    xhr.onCreate = function(xhr) {
      requests.push(xhr);
    };

    // create the test player
    player = createPlayer();
  },

  teardown: function() {
    player.dispose();
    videojs.Flash.isSupported = oldFlashSupported;
    videojs.MediaSource.open = oldMediaSourceOpen;
    videojs.Hls.SegmentParser = oldSegmentParser;
    videojs.Hls.supportsNativeHls = oldNativeHlsSupport;
    videojs.Hls.decrypt = oldDecrypt;
    videojs.SourceBuffer = oldSourceBuffer;
    window.setTimeout = oldSetTimeout;
    xhr.restore();
  }
});

test('starts playing if autoplay is specified', function() {
  var plays = 0;
  player.options().autoplay = true;
  player.src({
    src: 'manifest/playlist.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  // make sure play() is called *after* the media source opens
  player.play = function() {
    plays++;
  };
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  strictEqual(1, plays, 'play was called');
});

test('creates a PlaylistLoader on init', function() {
  var loadedmetadata = false;
  player.on('loadedmetadata', function() {
    loadedmetadata = true;
  });

  ok(!player.hls.playlists, 'waits for set src to create the loader');
  player.src({
    src:'manifest/playlist.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);
  ok(loadedmetadata, 'loadedmetadata fires');
  ok(player.hls.playlists.master, 'set the master playlist');
  ok(player.hls.playlists.media(), 'set the media playlist');
  ok(player.hls.playlists.media().segments, 'the segment entries are parsed');
  strictEqual(player.hls.playlists.master.playlists[0],
              player.hls.playlists.media(),
              'the playlist is selected');
});

test('sets the duration if one is available on the playlist', function() {
  var calls = 0;
  player.duration = function(value) {
    if (value === undefined) {
      return 0;
    }
    calls++;
  };
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  strictEqual(calls, 1, 'duration is set');
  standardXHRResponse(requests[1]);
  strictEqual(calls, 1, 'duration is set');
});

test('calculates the duration if needed', function() {
  var durations = [];
  player.duration = function(duration) {
    if (duration === undefined) {
      return 0;
    }
    durations.push(duration);
  };
  player.src({
    src: 'http://example.com/manifest/missingExtinf.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  strictEqual(durations.length, 1, 'duration is set');
  strictEqual(durations[0],
              player.hls.playlists.media().segments.length * 10,
              'duration is calculated');
});

test('starts downloading a segment on loadedmetadata', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  player.buffered = function() {
    return videojs.createTimeRange(0, 0);
  };
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);
  strictEqual(requests[1].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media-00001.ts',
              'the first segment is requested');
});

test('recognizes absolute URIs and requests them unmodified', function() {
  player.src({
    src: 'manifest/absoluteUris.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);
  strictEqual(requests[1].url,
              'http://example.com/00001.ts',
              'the first segment is requested');
});

test('recognizes domain-relative URLs', function() {
  player.src({
    src: 'manifest/domainUris.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);
  strictEqual(requests[1].url,
              window.location.origin + '/00001.ts',
              'the first segment is requested');
});

test('re-initializes the tech for each source', function() {
  var firstPlaylists, secondPlaylists, firstMSE, secondMSE, aborts;

  aborts = 0;

  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  firstPlaylists = player.hls.playlists;
  firstMSE = player.hls.mediaSource;
  player.hls.sourceBuffer.abort = function() {
    aborts++;
  };
  standardXHRResponse(requests.shift());
  standardXHRResponse(requests.shift());

  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  secondPlaylists = player.hls.playlists;
  secondMSE = player.hls.mediaSource;

  equal(1, aborts, 'aborted the old source buffer');
  ok(requests[0].aborted, 'aborted the old segment request');
  notStrictEqual(firstPlaylists, secondPlaylists, 'the playlist object is not reused');
  notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
});

test('triggers an error when a master playlist request errors', function() {
  var errors = 0;
  player.on('error', function() {
    errors++;
  });
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  requests.pop().respond(500);

  ok(player.error(), 'an error is triggered');
  strictEqual(1, errors, 'fired one error');
  strictEqual(2, player.error().code, 'a network error is triggered');
});

test('downloads media playlists after loading the master', function() {
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  // set bandwidth to a high number, so, we don't switch;
  player.hls.bandwidth = 500000;
  standardXHRResponse(requests[1]);
  standardXHRResponse(requests[2]);

  strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  strictEqual(requests[1].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media.m3u8',
              'media playlist requested');
  strictEqual(requests[2].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media-00001.ts',
              'first segment requested');
});

test('upshift if initial bandwidth is high', function() {
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.playlists.setBandwidth = function() {
    player.hls.playlists.bandwidth = 1000000000;
  };

  standardXHRResponse(requests[1]);
  standardXHRResponse(requests[2]);

  standardXHRResponse(requests[3]);

  strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  strictEqual(requests[1].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media.m3u8',
              'media playlist requested');
  strictEqual(requests[2].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media3.m3u8',
              'media playlist requested');
  strictEqual(requests[3].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media3-00001.ts',
              'first segment requested');
});

test('dont downshift if bandwidth is low', function() {
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.playlists.setBandwidth = function() {
    player.hls.playlists.bandwidth = 100;
  };

  standardXHRResponse(requests[1]);
  standardXHRResponse(requests[2]);

  strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
  strictEqual(requests[1].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media.m3u8',
              'media playlist requested');
  strictEqual(requests[2].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media-00001.ts',
              'first segment requested');
});

test('timeupdates do not check to fill the buffer until a media playlist is ready', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  player.trigger('timeupdate');

  strictEqual(1, requests.length, 'one request was made');
  strictEqual('manifest/media.m3u8', requests[0].url, 'media playlist requested');
});

test('calculates the bandwidth after downloading a segment', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  // set the request time to be a bit earlier so our bandwidth calculations are not NaN
  requests[1].requestTime = (new Date())-100;

  standardXHRResponse(requests[1]);

  ok(player.hls.bandwidth, 'bandwidth is calculated');
  ok(player.hls.bandwidth > 0,
     'bandwidth is positive: ' + player.hls.bandwidth);
  ok(player.hls.segmentXhrTime >= 0,
     'saves segment request time: ' + player.hls.segmentXhrTime + 's');
});

test('selects a playlist after segment downloads', function() {
  var calls = 0;
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  player.hls.selectPlaylist = function() {
    calls++;
    return player.hls.playlists.master.playlists[0];
  };
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.bandwidth = 3000000;
  standardXHRResponse(requests[1]);
  standardXHRResponse(requests[2]);

  strictEqual(calls, 2, 'selects after the initial segment');
  player.currentTime = function() {
    return 1;
  };
  player.buffered = function() {
    return videojs.createTimeRange(0, 2);
  };
  player.trigger('timeupdate');

  standardXHRResponse(requests[3]);

  strictEqual(calls, 3, 'selects after additional segments');
});

test('moves to the next segment if there is a network error', function() {
  var mediaIndex;

  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.bandwidth = 3000000;
  standardXHRResponse(requests[1]);

  mediaIndex = player.hls.mediaIndex;
  player.trigger('timeupdate');

  requests[2].respond(400);
  strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
});

test('updates the duration after switching playlists', function() {
  var
    calls = 0,
    selectedPlaylist = false;
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  player.hls.selectPlaylist = function() {
    selectedPlaylist = true;
    return player.hls.playlists.master.playlists[1];
  };
  player.duration = function(duration) {
    if (duration === undefined) {
      return 0;
    }
    // only track calls that occur after the playlist has been switched
    if (player.hls.playlists.media() === player.hls.playlists.master.playlists[1]) {
      calls++;
    }
  };
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);
  standardXHRResponse(requests[2]);
  standardXHRResponse(requests[3]);
  ok(selectedPlaylist, 'selected playlist');
  strictEqual(calls, 1, 'updates the duration');
});

test('downloads additional playlists if required', function() {
  var
    called = false,
    playlist = {
      uri: 'media3.m3u8'
    };
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.bandwidth = 3000000;
  standardXHRResponse(requests[1]);
  // before an m3u8 is downloaded, no segments are available
  player.hls.selectPlaylist = function() {
    if (!called) {
      called = true;
      return playlist;
    }
    playlist.segments = [1, 1, 1];
    return playlist;
  };

  // the playlist selection is revisited after a new segment is downloaded
  player.trigger('timeupdate');

  standardXHRResponse(requests[2]);
  standardXHRResponse(requests[3]);

  strictEqual(4, requests.length, 'requests were made');
  strictEqual(requests[3].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/' +
              playlist.uri,
              'made playlist request');
  strictEqual(playlist.uri,
              player.hls.playlists.media().uri,
              'a new playlists was selected');
  ok(player.hls.playlists.media().segments, 'segments are now available');
});

test('selects a playlist below the current bandwidth', function() {
  var playlist;
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  // the default playlist has a really high bitrate
  player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
  // playlist 1 has a very low bitrate
  player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
  // but the detected client bandwidth is really low
  player.hls.bandwidth = 10;

  playlist = player.hls.selectPlaylist();
  strictEqual(playlist,
              player.hls.playlists.master.playlists[1],
              'the low bitrate stream is selected');
});

test('raises the minimum bitrate for a stream proportionially', function() {
  var playlist;
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  // the default playlist's bandwidth + 10% is equal to the current bandwidth
  player.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
  player.hls.bandwidth = 11;

  // 9.9 * 1.1 < 11
  player.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
  playlist = player.hls.selectPlaylist();

  strictEqual(playlist,
              player.hls.playlists.master.playlists[1],
              'a lower bitrate stream is selected');
});

test('uses the lowest bitrate if no other is suitable', function() {
  var playlist;
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  // the lowest bitrate playlist is much greater than 1b/s
  player.hls.bandwidth = 1;
  playlist = player.hls.selectPlaylist();

  // playlist 1 has the lowest advertised bitrate
  strictEqual(playlist,
              player.hls.playlists.master.playlists[1],
              'the lowest bitrate stream is selected');
});

test('selects the correct rendition by player dimensions', function() {
  var playlist;

  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });

  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.width(640);
  player.height(360);
  player.hls.bandwidth = 3000000;

  playlist = player.hls.selectPlaylist();

  deepEqual(playlist.attributes.RESOLUTION, {width:396,height:224},'should return the correct resolution by player dimensions');
  equal(playlist.attributes.BANDWIDTH, 440000, 'should have the expected bandwidth in case of multiple');

  player.width(1920);
  player.height(1080);
  player.hls.bandwidth = 3000000;

  playlist = player.hls.selectPlaylist();

  deepEqual(playlist.attributes.RESOLUTION, {width:960,height:540},'should return the correct resolution by player dimensions');
  equal(playlist.attributes.BANDWIDTH, 1928000, 'should have the expected bandwidth in case of multiple');

});


test('does not download the next segment if the buffer is full', function() {
  var currentTime = 15;
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  player.currentTime = function() {
    return currentTime;
  };
  player.buffered = function() {
    return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH);
  };
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.trigger('timeupdate');

  strictEqual(requests.length, 1, 'no segment request was made');
});

test('downloads the next segment if the buffer is getting low', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);

  strictEqual(requests.length, 2, 'did not make a request');
  player.currentTime = function() {
    return 15;
  };
  player.buffered = function() {
    return videojs.createTimeRange(0, 19.999);
  };
  player.trigger('timeupdate');

  standardXHRResponse(requests[2]);

  strictEqual(requests.length, 3, 'made a request');
  strictEqual(requests[2].url,
              window.location.origin +
              window.location.pathname.split('/').slice(0, -1).join('/') +
              '/manifest/media-00002.ts',
              'made segment request');
});

test('stops downloading segments at the end of the playlist', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);
  requests = [];
  player.hls.mediaIndex = 4;
  player.trigger('timeupdate');

  strictEqual(requests.length, 0, 'no request is made');
});

test('only makes one segment request at a time', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests.pop());
  player.trigger('timeupdate');

  strictEqual(1, requests.length, 'one XHR is made');
  player.trigger('timeupdate');
  strictEqual(1, requests.length, 'only one XHR is made');
});

test('cancels outstanding XHRs when seeking', function() {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);
  player.hls.media = {
    segments: [{
      uri: '0.ts',
      duration: 10
    }, {
      uri: '1.ts',
      duration: 10
    }]
  };

  // trigger a segment download request
  player.trigger('timeupdate');
  // attempt to seek while the download is in progress
  player.currentTime(7);

  ok(requests[1].aborted, 'XHR aborted');
  strictEqual(requests.length, 3, 'opened new XHR');
});

test('when outstanding XHRs are cancelled, they get aborted properly', function() {
  var readystatechanges = 0;

  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);

  // trigger a segment download request
  player.trigger('timeupdate');

  player.hls.segmentXhr_.onreadystatechange = function() {
    readystatechanges++;
  };

  // attempt to seek while the download is in progress
  player.currentTime(12);

  ok(requests[1].aborted, 'XHR aborted');
  strictEqual(requests.length, 3, 'opened new XHR');
  notEqual(player.hls.segmentXhr_.url, requests[1].url, 'a new segment is request that is not the aborted one');
  strictEqual(readystatechanges, 0, 'onreadystatechange was not called');
});

test('segmentXhr is properly nulled out when dispose is called', function() {
  var
    readystatechanges = 0,
    oldDispose = videojs.Flash.prototype.dispose;
  videojs.Flash.prototype.dispose = function() {};

  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);

  // trigger a segment download request
  player.trigger('timeupdate');

  player.hls.segmentXhr_.onreadystatechange = function() {
    readystatechanges++;
  };

  player.hls.dispose();

  ok(requests[1].aborted, 'XHR aborted');
  strictEqual(requests.length, 2, 'did not open a new XHR');
  equal(player.hls.segmentXhr_, null, 'the segment xhr is nulled out');
  strictEqual(readystatechanges, 0, 'onreadystatechange was not called');

  videojs.Flash.prototype.dispose = oldDispose;
});

test('flushes the parser after each segment', function() {
  var flushes = 0;
  // mock out the segment parser
  videojs.Hls.SegmentParser = function() {
    this.getFlvHeader = function() {
      return [];
    };
    this.parseSegmentBinaryData = function() {};
    this.flushTags = function() {
      flushes++;
    };
    this.tagsAvailable = function() {};
  };

  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);
  strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
});

test('drops tags before the target timestamp when seeking', function() {
  var i = 10,
      tags = [],
      bytes = [];

  // mock out the parser and source buffer
  videojs.Hls.SegmentParser = mockSegmentParser(tags);
  window.videojs.SourceBuffer = function() {
    this.appendBuffer = function(chunk) {
      bytes.push(chunk);
    };
    this.abort = function() {};
  };

  // push a tag into the buffer
  tags.push({ pts: 0, bytes: 0 });

  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);

  // mock out a new segment of FLV tags
  bytes = [];
  while (i--) {
    tags.unshift({
      pts: i * 1000,
      bytes: i
    });
  }
  player.currentTime(7);
  standardXHRResponse(requests[2]);

  deepEqual(bytes, [7,8,9], 'three tags are appended');
});

test('calls abort() on the SourceBuffer before seeking', function() {
  var
    aborts = 0,
    bytes = [],
    tags = [{ pts: 0, bytes: 0 }];


  // track calls to abort()
  videojs.Hls.SegmentParser = mockSegmentParser(tags);
  window.videojs.SourceBuffer = function() {
    this.appendBuffer = function(chunk) {
      bytes.push(chunk);
    };
    this.abort = function() {
      aborts++;
    };
  };

  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);

  // drainBuffer() uses the first PTS value to account for any timestamp discontinuities in the stream
  // adding a tag with a PTS of zero looks like a stream with no discontinuities
  tags.push({ pts: 0, bytes: 0 });
  tags.push({ pts: 7000, bytes: 7 });
  // seek to 7s
  player.currentTime(7);
  standardXHRResponse(requests[2]);

  strictEqual(1, aborts, 'aborted pending buffer');
});

test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
  var errorTriggered = false;
  player.on('error', function() {
    errorTriggered = true;
  });
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  requests.pop().respond(404);

  equal(errorTriggered,
        true,
        'Missing Playlist error event should trigger');
  equal(player.error().code,
        2,
        'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
  ok(player.error().message, 'included an error message');
});

test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });

  openMediaSource(player);

  standardXHRResponse(requests[0]);
  requests[1].respond(404);
  ok(player.hls.error.message, 'an error message is available');
  equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
});

test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
  player.src({
    src: 'manifest/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });

  openMediaSource(player);

  standardXHRResponse(requests[0]);
  requests[1].respond(500);
  ok(player.hls.error.message, 'an error message is available');
  equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
});

test('duration is Infinity for live playlists', function() {
  player.src({
    src: 'http://example.com/manifest/missingEndlist.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  strictEqual(player.duration(), Infinity, 'duration is infinity');
  ok((' ' + player.el().className + ' ').indexOf(' vjs-live ') >= 0, 'added vjs-live class');
});

test('updates the media index when a playlist reloads', function() {
  player.src({
    src: 'http://example.com/live-updating.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  requests[0].respond(200, null,
                      '#EXTM3U\n' +
                      '#EXTINF:10,\n' +
                      '0.ts\n' +
                      '#EXTINF:10,\n' +
                      '1.ts\n' +
                      '#EXTINF:10,\n' +
                      '2.ts\n');
  standardXHRResponse(requests[1]);
  // play the stream until 2.ts is playing
  player.hls.mediaIndex = 3;

  // reload the updated playlist
  player.hls.playlists.media = function() {
    return {
      segments: [{
        uri: '1.ts'
      }, {
        uri: '2.ts'
      }, {
        uri: '3.ts'
      }]
    };
  };
  player.hls.playlists.trigger('loadedplaylist');

  strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
});

test('live playlist starts 30s before live', function() {
  player.src({
    src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);

  player.hls.playlists.trigger('loadedmetadata');

  strictEqual(player.hls.mediaIndex, 6, 'mediaIndex is updated after the reload');
});

test('mediaIndex is zero before the first segment loads', function() {
  window.manifests['first-seg-load'] =
    '#EXTM3U\n' +
    '#EXTINF:10,\n' +
    '0.ts\n';
  player.src({
    src: 'http://example.com/first-seg-load.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  strictEqual(player.hls.mediaIndex, 0, 'mediaIndex is zero');
});

test('reloads out-of-date live playlists when switching variants', function() {
  player.src({
    src: 'http://example.com/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  player.hls.master = {
    playlists: [{
      mediaSequence: 15,
      segments: [1, 1, 1]
    }, {
      uri: 'http://example.com/variant-update.m3u8',
      mediaSequence: 0,
      segments: [1, 1]
    }]
  };
  // playing segment 15 on playlist zero
  player.hls.media = player.hls.master.playlists[0];
  player.mediaIndex = 1;
  window.manifests['variant-update'] = '#EXTM3U\n' +
    '#EXT-X-MEDIA-SEQUENCE:16\n' +
    '#EXTINF:10,\n' +
    '16.ts\n' +
    '#EXTINF:10,\n' +
    '17.ts\n';

  // switch playlists
  player.hls.selectPlaylist = function() {
    return player.hls.master.playlists[1];
  };
  // timeupdate downloads segment 16 then switches playlists
  player.trigger('timeupdate');

  strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment');
});

test('if withCredentials option is used, withCredentials is set on the XHR object', function() {
  player.dispose();
  player = createPlayer({
    withCredentials: true
  });
  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  ok(requests[0].withCredentials, "with credentials should be set to true if that option is passed in");
});

test('does not break if the playlist has no segments', function() {
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  try {
    openMediaSource(player);
    requests[0].respond(200, null,
                        '#EXTM3U\n' +
                        '#EXT-X-PLAYLIST-TYPE:VOD\n' +
                        '#EXT-X-TARGETDURATION:10\n');
  } catch(e) {
    ok(false, 'an error was thrown');
    throw e;
  }
  ok(true, 'no error was thrown');
  strictEqual(requests.length, 1, 'no requests for non-existent segments were queued');
});

test('waits until the buffer is empty before appending bytes at a discontinuity', function() {
  var aborts = 0, setTime, currentTime, bufferEnd;

  player.src({
    src: 'disc.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  player.currentTime = function() { return currentTime; };
  player.buffered = function() {
    return videojs.createTimeRange(0, bufferEnd);
  };
  player.hls.sourceBuffer.abort = function() {
    aborts++;
  };
  player.hls.el().vjs_setProperty = function(name, value) {
    if (name === 'currentTime') {
      return setTime = value;
    }
  };

  requests.pop().respond(200, null,
                         '#EXTM3U\n' +
                         '#EXTINF:10,0\n' +
                         '1.ts\n' +
                         '#EXT-X-DISCONTINUITY\n' +
                         '#EXTINF:10,0\n' +
                         '2.ts\n');
  standardXHRResponse(requests.pop());

  // play to 6s to trigger the next segment request
  currentTime = 6;
  bufferEnd = 10;
  player.trigger('timeupdate');
  strictEqual(aborts, 0, 'no aborts before the buffer empties');

  standardXHRResponse(requests.pop());
  strictEqual(aborts, 0, 'no aborts before the buffer empties');

  // pretend the buffer has emptied
  player.trigger('waiting');
  strictEqual(aborts, 1, 'aborted before appending the new segment');
  strictEqual(setTime, 10, 'updated the time after crossing the discontinuity');
});

test('clears the segment buffer on seek', function() {
  var aborts = 0, tags = [], currentTime, bufferEnd, oldCurrentTime;

  videojs.Hls.SegmentParser = mockSegmentParser(tags);

  player.src({
    src: 'disc.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  oldCurrentTime = player.currentTime;
  player.currentTime = function(time) {
    if (time !== undefined) {
      return oldCurrentTime.call(player, time);
    }
    return currentTime;
  };
  player.buffered = function() {
    return videojs.createTimeRange(0, bufferEnd);
  };
  player.hls.sourceBuffer.abort = function() {
    aborts++;
  };

  requests.pop().respond(200, null,
                         '#EXTM3U\n' +
                         '#EXTINF:10,0\n' +
                         '1.ts\n' +
                         '#EXT-X-DISCONTINUITY\n' +
                         '#EXTINF:10,0\n' +
                         '2.ts\n');
  standardXHRResponse(requests.pop());

  // play to 6s to trigger the next segment request
  currentTime = 6;
  bufferEnd = 10;
  player.trigger('timeupdate');

  standardXHRResponse(requests.pop());

  // seek back to the beginning
  player.currentTime(0);
  tags.push({ pts: 0, bytes: 0 });
  standardXHRResponse(requests.pop());
  strictEqual(aborts, 1, 'aborted once for the seek');

  // the source buffer empties. is 2.ts still in the segment buffer?
  player.trigger('waiting');
  strictEqual(aborts, 1, 'cleared the segment buffer on a seek');
});

test('resets the switching algorithm if a request times out', function() {
  player.src({
    src: 'master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  standardXHRResponse(requests.shift()); // master

  player.hls.bandwidth = 3000000;
  standardXHRResponse(requests.shift()); // media.m3u8
  // simulate a segment timeout
  requests[0].timedout = true;
  requests.shift().abort();

  standardXHRResponse(requests.shift());

  strictEqual(player.hls.playlists.media(),
              player.hls.playlists.master.playlists[1],
              'reset to the lowest bitrate playlist');
});

test('disposes the playlist loader', function() {
  var disposes = 0, player, loaderDispose;
  player = createPlayer();
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  loaderDispose = player.hls.playlists.dispose;
  player.hls.playlists.dispose = function() {
    disposes++;
    loaderDispose.call(player.hls.playlists);
  };

  player.dispose();
  strictEqual(disposes, 1, 'disposed playlist loader');
});

test('remove event handlers on dispose', function() {
  var
    player,
    onhandlers = 0,
    offhandlers = 0,
    oldOn,
    oldOff;

  player = createPlayer();
  oldOn = player.on;
  oldOff = player.off;
  player.on = function(type, handler) {
    onhandlers++;
    oldOn.call(player, type, handler);
  };
  player.off = function(type, handler) {
    // ignore the top-level videojs removals that aren't relevant to HLS
    if (type && type !== 'dispose') {
      offhandlers++;
    }
    oldOff.call(player, type, handler);
  };
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  standardXHRResponse(requests[0]);
  standardXHRResponse(requests[1]);

  player.dispose();

  equal(offhandlers, onhandlers, 'the amount of on and off handlers is the same');

  player.off = oldOff;
  player.on = oldOn;
});

test('aborts the source buffer on disposal', function() {
  var aborts = 0, player;
  player = createPlayer();
  player.src({
    src: 'manifest/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  player.hls.sourceBuffer.abort = function() {
    aborts++;
  };

  player.dispose();
  strictEqual(aborts, 1, 'aborted the source buffer');
});

test('only supports HLS MIME types', function() {
  ok(videojs.Hls.canPlaySource({
    type: 'aPplicatiOn/x-MPegUrl'
  }), 'supports x-mpegurl');
  ok(videojs.Hls.canPlaySource({
    type: 'aPplicatiOn/VnD.aPPle.MpEgUrL'
  }), 'supports vnd.apple.mpegurl');

  ok(!videojs.Hls.canPlaySource({
    type: 'video/mp4'
  }), 'does not support mp4');
  ok(!videojs.Hls.canPlaySource({
    type: 'video/x-flv'
  }), 'does not support flv');
});

test('adds Hls to the default tech order', function() {
  strictEqual(videojs.options.techOrder[0], 'hls', 'first entry is Hls');
});

test('has no effect if native HLS is available', function() {
  var player;
  videojs.Hls.supportsNativeHls = true;
  player = createPlayer();
  player.src({
    src: 'http://example.com/manifest/master.m3u8',
    type: 'application/x-mpegURL'
  });

  ok(!player.hls, 'did not load hls tech');
  player.dispose();
});

test('is not supported on browsers without typed arrays', function() {
  var oldArray = window.Uint8Array;
  window.Uint8Array = null;
  ok(!videojs.Hls.isSupported(), 'HLS is not supported');

  // cleanup
  window.Uint8Array = oldArray;
});

test('tracks the bytes downloaded', function() {
  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  strictEqual(player.hls.bytesReceived, 0, 'no bytes received');

  requests.shift().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n' +
                           '#EXTINF:10,\n' +
                           '1.ts\n' +
                           '#EXT-X-ENDLIST\n');
  // transmit some segment bytes
  requests[0].response = new ArrayBuffer(17);
  requests.shift().respond(200, null, '');

  strictEqual(player.hls.bytesReceived, 17, 'tracked bytes received');

  player.trigger('timeupdate');

  // transmit some more
  requests[0].response = new ArrayBuffer(5);
  requests.shift().respond(200, null, '');

  strictEqual(player.hls.bytesReceived, 22, 'tracked more bytes');
});

test('re-emits mediachange events', function() {
  var mediaChanges = 0;
  player.on('mediachange', function() {
    mediaChanges++;
  });

  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  player.hls.playlists.trigger('mediachange');
  strictEqual(mediaChanges, 1, 'fired mediachange');
});

test('can be disposed before finishing initialization', function() {
  var player = createPlayer(), readyHandlers = [];
  player.ready = function(callback) {
    readyHandlers.push(callback);
  };
  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  player.src({
    src: 'http://example.com/media.mp4',
    type: 'video/mp4'
  });
  ok(readyHandlers.length > 0, 'registered a ready handler');
  try {
    while (readyHandlers.length) {
      readyHandlers.shift().call(player);
    }
    ok(true, 'did not throw an exception');
  } catch (e) {
    ok(false, 'threw an exception');
  }
});

test('calls ended() on the media source at the end of a playlist', function() {
  var endOfStreams = 0;
  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  player.hls.mediaSource.endOfStream = function() {
    endOfStreams++;
  };
  // playlist response
  requests.shift().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n' +
                           '#EXT-X-ENDLIST\n');
  // segment response
  requests[0].response = new ArrayBuffer(17);
  requests.shift().respond(200, null, '');

  strictEqual(endOfStreams, 1, 'ended media source');
});

test('calling play() at the end of a video resets the media index', function() {
  player.src({
    src: 'http://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  requests.shift().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXTINF:10,\n' +
                           '0.ts\n' +
                           '#EXT-X-ENDLIST\n');
  standardXHRResponse(requests.shift());

  strictEqual(player.hls.mediaIndex, 1, 'index is 1 after the first segment');
  player.hls.ended = function() {
    return true;
  };
  player.play();
  strictEqual(player.hls.mediaIndex, 0, 'index is 1 after the first segment');
});

test('calling fetchKeys() when a new playlist is loaded will create an XHR', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52'
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53'
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };

  player.hls.playlists.trigger('loadedplaylist');
  strictEqual(requests.length, 2, 'a key XHR is created');
  strictEqual(requests[1].url, player.hls.playlists.media().segments[0].key.uri, 'a key XHR is created with correct uri');

  player.hls.playlists.media = oldMedia;
});

test('a new keys XHR is created when a previous key XHR finishes', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52'
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53'
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };
  // we're inject the media playlist, so drop the request
  requests.shift();

  player.hls.playlists.trigger('loadedplaylist');
  // key response
  requests[0].response = new Uint32Array([0, 0, 0, 0]).buffer;
  requests.shift().respond(200, null, '');
  strictEqual(requests.length, 1, 'a key XHR is created');
  strictEqual(requests[0].url, player.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri');

  player.hls.playlists.media = oldMedia;
});

test('calling fetchKeys() when a seek happens will create an XHR', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        duration: 10,
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52'
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        duration: 10,
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53'
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };

  player.hls.fetchKeys(player.hls.playlists.media(), 0);
  player.currentTime(11);
  ok(requests[1].aborted, 'the key XHR should be aborted');
  equal(requests.length, 3, 'we should get a new key XHR');
  equal(requests[2].url, player.hls.playlists.media().segments[1].key.uri, 'urls should match');

  player.hls.playlists.media = oldMedia;
});

test('calling fetchKeys() when a key XHR is in progress will *not* create an XHR', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52'
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53'
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };

  strictEqual(requests.length, 1, 'no key XHR created for the player');
  player.hls.playlists.trigger('loadedplaylist');
  player.hls.fetchKeys(player.hls.playlists.media(), 0);
  strictEqual(requests.length, 2, 'only the original XHR is available');

  player.hls.playlists.media = oldMedia;
});

test('calling fetchKeys() when all keys are fetched, will *not* create an XHR', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52',
          bytes: new Uint8Array([1])
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53',
          bytes: new Uint8Array([1])
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };

  player.hls.fetchKeys(player.hls.playlists.media(), 0);
  strictEqual(requests.length, 1, 'no XHR for keys created since they were all downloaded');

  player.hls.playlists.media = oldMedia;
});

test('retries key requests once upon failure', function() {
  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  var oldMedia = player.hls.playlists.media;
  player.hls.playlists.media = function() {
    return {
      segments: [{
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=52'
        },
        uri: 'http://media.example.com/fileSequence52-A.ts'
      }, {
        key: {
          'method': 'AES-128',
          'uri': 'https://priv.example.com/key.php?r=53'
        },
        uri: 'http://media.example.com/fileSequence53-B.ts'
      }]
    };
  };

  player.hls.fetchKeys(player.hls.playlists.media(), 0);

  requests[1].respond(404);
  equal(requests.length, 3, 'create a new XHR for the same key');
  equal(requests[2].url, requests[1].url, 'should be the same key');

  requests[2].respond(404);
  equal(requests.length, 4, 'create a new XHR for the same key');
  notEqual(requests[3].url, requests[2].url, 'should be the same key');
  equal(requests[3].url, player.hls.playlists.media().segments[1].key.uri);

  player.hls.playlists.media = oldMedia;
});

test('skip segments if key requests fail more than once', function() {
  var bytes = [],
      tags = [{ pats: 0, bytes: 0 }];

  videojs.Hls.SegmentParser = mockSegmentParser(tags);
  window.videojs.SourceBuffer = function() {
    this.appendBuffer = function(chunk) {
      bytes.push(chunk);
    };
    this.abort = function() {};
  };

  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  requests.pop().respond(200, null,
                         '#EXTM3U\n' +
                         '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
                         '#EXTINF:2.833,\n' +
                         'http://media.example.com/fileSequence52-A.ts\n' +
                         '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
                         '#EXTINF:15.0,\n' +
                         'http://media.example.com/fileSequence53-A.ts\n');

  player.hls.playlists.trigger('loadedplaylist');

  player.trigger('timeupdate');

  // respond to ts segment
  standardXHRResponse(requests.pop());
  // fail key
  requests.pop().respond(404);
  // fail key, again
  requests.pop().respond(404);

  // key for second segment
  requests[0].response = new Uint32Array([0,0,0,0]).buffer;
  requests[0].respond(200, null, '');
  requests.shift();

  equal(bytes.length, 1, 'bytes from the ts segments should not be added');

  player.trigger('timeupdate');

  tags.length = 0;
  tags.push({pts: 0, bytes: 1});

  // second segment
  standardXHRResponse(requests.pop());

  equal(bytes.length, 2, 'bytes from the second ts segment should be added');
  equal(bytes[1], 1, 'the bytes from the second segment are added and not the first');
});

test('the key is supplied to the decrypter in the correct format', function() {
  var keys = [];

  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  requests.pop().respond(200, null,
                         '#EXTM3U\n' +
                         '#EXT-X-MEDIA-SEQUENCE:5\n' +
                         '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
                         '#EXTINF:2.833,\n' +
                         'http://media.example.com/fileSequence52-A.ts\n' +
                         '#EXTINF:15.0,\n' +
                         'http://media.example.com/fileSequence52-B.ts\n');


  videojs.Hls.decrypt = function(bytes, key) {
    keys.push(key);
    return new Uint8Array([0]);
  };

  requests[0].response = new Uint32Array([0,1,2,3]).buffer;
  requests[0].respond(200, null, '');
  requests.shift();
  standardXHRResponse(requests.pop());

  equal(keys.length, 1, 'only one call to decrypt was made');
  deepEqual(keys[0],
            new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]),
            'passed the specified segment key');

});
test('supplies the media sequence of current segment as the IV by default, if no IV is specified', function() {
  var ivs = [];

  player.src({
    src: 'https://example.com/encrypted-media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  requests.pop().respond(200, null,
                         '#EXTM3U\n' +
                         '#EXT-X-MEDIA-SEQUENCE:5\n' +
                         '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
                         '#EXTINF:2.833,\n' +
                         'http://media.example.com/fileSequence52-A.ts\n' +
                         '#EXTINF:15.0,\n' +
                         'http://media.example.com/fileSequence52-B.ts\n');


  videojs.Hls.decrypt = function(bytes, key, iv) {
    ivs.push(iv);
    return new Uint8Array([0]);
  };

  requests[0].response = new Uint32Array([0,0,0,0]).buffer;
  requests[0].respond(200, null, '');
  requests.shift();
  standardXHRResponse(requests.pop());

  equal(ivs.length, 1, 'only one call to decrypt was made');
  deepEqual(ivs[0],
        new Uint32Array([0, 0, 0, 5]),
        'the IV for the segment is the media sequence');
});

test('switching playlists with an outstanding key request does not stall playback', function() {
  var media = '#EXTM3U\n' +
    '#EXT-X-MEDIA-SEQUENCE:5\n' +
    '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
    '#EXTINF:2.833,\n' +
    'http://media.example.com/fileSequence52-A.ts\n' +
    '#EXTINF:15.0,\n' +
    'http://media.example.com/fileSequence52-B.ts\n';
  player.src({
    src: 'https://example.com/master.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  // master playlist
  standardXHRResponse(requests.shift());
  // media playlist
  requests.shift().respond(200, null, media);
  // mock out media switching from this point on
  player.hls.playlists.media = function() {
    return player.hls.playlists.master.playlists[0];
  };
  // don't respond to the initial key request
  requests.shift();
  // first segment of the original media playlist
  standardXHRResponse(requests.shift());

  // "switch" media
  player.hls.playlists.trigger('mediachange');

  player.trigger('timeupdate');

  ok(requests.length, 'made a request');
  equal(requests[0].url,
        'https://priv.example.com/key.php?r=52',
        'requested the segment and key');
});

test('resovles relative key URLs against the playlist', function() {
  player.src({
    src: 'https://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);

  requests.shift().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:5\n' +
                           '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
                           '#EXTINF:2.833,\n' +
                           'http://media.example.com/fileSequence52-A.ts\n');
  equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL');
});

test('treats invalid keys as a key request failure', function() {
  var tags = [{ pts: 0, bytes: 0 }], bytes = [];
  videojs.Hls.SegmentParser = mockSegmentParser(tags);
  window.videojs.SourceBuffer = function() {
    this.appendBuffer = function(chunk) {
      bytes.push(chunk);
    };
    this.abort = function() {};
  };
  player.src({
    src: 'https://example.com/media.m3u8',
    type: 'application/vnd.apple.mpegurl'
  });
  openMediaSource(player);
  requests.shift().respond(200, null,
                           '#EXTM3U\n' +
                           '#EXT-X-MEDIA-SEQUENCE:5\n' +
                           '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
                           '#EXTINF:2.833,\n' +
                           'http://media.example.com/fileSequence52-A.ts\n' +
                           '#EXT-X-KEY:METHOD=NONE\n' +
                           '#EXTINF:15.0,\n' +
                           'http://media.example.com/fileSequence52-B.ts\n');
  // keys should be 16 bytes long
  requests[0].response = new Uint8Array(1).buffer;
  requests.shift().respond(200, null, '');
  // segment request
  standardXHRResponse(requests.shift());

  equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key');

  // the retried response is invalid, too
  requests[0].response = new Uint8Array(1);
  requests.shift().respond(200, null, '');

  // the first segment should be dropped and playback moves on
  player.trigger('timeupdate');
  equal(bytes.length, 1, 'did not append bytes');
  equal(bytes[0], 'flv', 'appended the flv header');

  tags.length = 0;
  tags.push({ pts: 1, bytes: 1 });
  // second segment request
  standardXHRResponse(requests.shift());

  equal(bytes.length, 2, 'appended bytes');
  equal(1, bytes[1], 'skipped to the second segment');
});

})(window, window.videojs);
<file_sep>/*
 * videojs-hls
 *
 * Copyright (c) 2014 Brightcove
 * All rights reserved.
 *
 * This file contains an adaptation of the AES decryption algorithm
 * from the Standford Javascript Cryptography Library. That work is
 * covered by the following copyright and permissions notice:
 *
 * Copyright 2009-2010 <NAME>, <NAME>, <NAME>.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials provided
 *    with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation
 * are those of the authors and should not be interpreted as representing
 * official policies, either expressed or implied, of the authors.
 */
(function(window, videojs, unpad) {
'use strict';

var AES, decrypt;

/**
 * Schedule out an AES key for both encryption and decryption. This
 * is a low-level class. Use a cipher mode to do bulk encryption.
 *
 * @constructor
 * @param key {Array} The key as an array of 4, 6 or 8 words.
 */
AES = function (key) {
  this._precompute();
  
  var i, j, tmp,
    encKey, decKey,
    sbox = this._tables[0][4], decTable = this._tables[1],
    keyLen = key.length, rcon = 1;
  
  if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) {
    throw new Error("Invalid aes key size");
  }
  
  encKey = key.slice(0);
  decKey = [];
  this._key = [encKey, decKey];
  
  // schedule encryption keys
  for (i = keyLen; i < 4 * keyLen + 28; i++) {
    tmp = encKey[i-1];
    
    // apply sbox
    if (i%keyLen === 0 || (keyLen === 8 && i%keyLen === 4)) {
      tmp = sbox[tmp>>>24]<<24 ^ sbox[tmp>>16&255]<<16 ^ sbox[tmp>>8&255]<<8 ^ sbox[tmp&255];
      
      // shift rows and add rcon
      if (i%keyLen === 0) {
        tmp = tmp<<8 ^ tmp>>>24 ^ rcon<<24;
        rcon = rcon<<1 ^ (rcon>>7)*283;
      }
    }
    
    encKey[i] = encKey[i-keyLen] ^ tmp;
  }
  
  // schedule decryption keys
  for (j = 0; i; j++, i--) {
    tmp = encKey[j&3 ? i : i - 4];
    if (i<=4 || j<4) {
      decKey[j] = tmp;
    } else {
      decKey[j] = decTable[0][sbox[tmp>>>24      ]] ^
                  decTable[1][sbox[tmp>>16  & 255]] ^
                  decTable[2][sbox[tmp>>8   & 255]] ^
                  decTable[3][sbox[tmp      & 255]];
    }
  }
};

AES.prototype = {
  /**
   * The expanded S-box and inverse S-box tables. These will be computed
   * on the client so that we don't have to send them down the wire.
   *
   * There are two tables, _tables[0] is for encryption and
   * _tables[1] is for decryption.
   *
   * The first 4 sub-tables are the expanded S-box with MixColumns. The
   * last (_tables[01][4]) is the S-box itself.
   *
   * @private
   */
  _tables: [[[],[],[],[],[]],[[],[],[],[],[]]],

  /**
   * Expand the S-box tables.
   *
   * @private
   */
  _precompute: function () {
   var encTable = this._tables[0], decTable = this._tables[1],
       sbox = encTable[4], sboxInv = decTable[4],
       i, x, xInv, d=[], th=[], x2, x4, x8, s, tEnc, tDec;

    // Compute double and third tables
   for (i = 0; i < 256; i++) {
     th[( d[i] = i<<1 ^ (i>>7)*283 )^i]=i;
   }
   
   for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) {
     // Compute sbox
     s = xInv ^ xInv<<1 ^ xInv<<2 ^ xInv<<3 ^ xInv<<4;
     s = s>>8 ^ s&255 ^ 99;
     sbox[x] = s;
     sboxInv[s] = x;
     
     // Compute MixColumns
     x8 = d[x4 = d[x2 = d[x]]];
     tDec = x8*0x1010101 ^ x4*0x10001 ^ x2*0x101 ^ x*0x1010100;
     tEnc = d[s]*0x101 ^ s*0x1010100;
     
     for (i = 0; i < 4; i++) {
       encTable[i][x] = tEnc = tEnc<<24 ^ tEnc>>>8;
       decTable[i][s] = tDec = tDec<<24 ^ tDec>>>8;
     }
   }
   
   // Compactify. Considerable speedup on Firefox.
   for (i = 0; i < 5; i++) {
     encTable[i] = encTable[i].slice(0);
     decTable[i] = decTable[i].slice(0);
   }
  },
  
  /**
   * Decrypt an array of 4 big-endian words.
   * @param {Array} data The ciphertext.
   * @return {Array} The plaintext.
   */
  decrypt:function (input) {
    if (input.length !== 4) {
      throw new Error("Invalid aes block size");
    }
    
    var key = this._key[1],
        // state variables a,b,c,d are loaded with pre-whitened data
        a = input[0]           ^ key[0],
        b = input[3] ^ key[1],
        c = input[2]           ^ key[2],
        d = input[1] ^ key[3],
        a2, b2, c2,
        
        nInnerRounds = key.length/4 - 2,
        i,
        kIndex = 4,
        out = [0,0,0,0],
        table = this._tables[1],
        
        // load up the tables
        t0    = table[0],
        t1    = table[1],
        t2    = table[2],
        t3    = table[3],
        sbox  = table[4];
 
    // Inner rounds. Cribbed from OpenSSL.
    for (i = 0; i < nInnerRounds; i++) {
      a2 = t0[a>>>24] ^ t1[b>>16 & 255] ^ t2[c>>8 & 255] ^ t3[d & 255] ^ key[kIndex];
      b2 = t0[b>>>24] ^ t1[c>>16 & 255] ^ t2[d>>8 & 255] ^ t3[a & 255] ^ key[kIndex + 1];
      c2 = t0[c>>>24] ^ t1[d>>16 & 255] ^ t2[a>>8 & 255] ^ t3[b & 255] ^ key[kIndex + 2];
      d  = t0[d>>>24] ^ t1[a>>16 & 255] ^ t2[b>>8 & 255] ^ t3[c & 255] ^ key[kIndex + 3];
      kIndex += 4;
      a=a2; b=b2; c=c2;
    }
        
    // Last round.
    for (i = 0; i < 4; i++) {
      out[3 & -i] =
        sbox[a>>>24      ]<<24 ^ 
        sbox[b>>16  & 255]<<16 ^
        sbox[c>>8   & 255]<<8  ^
        sbox[d      & 255]     ^
        key[kIndex++];
      a2=a; a=b; b=c; c=d; d=a2;
    }
    
    return out;
  }
};

decrypt = function(encrypted, key, initVector) {
  var
    encryptedView = new DataView(encrypted.buffer),
    platformEndian = new Uint32Array(encrypted.byteLength / 4),
    decipher = new AES(Array.prototype.slice.call(key)),
    decrypted = new Uint8Array(encrypted.byteLength),
    decryptedView = new DataView(decrypted.buffer),
    decryptedBlock,
    word,
    byte;

  // convert big-endian input to platform byte order for decryption
  for (byte = 0; byte < encrypted.byteLength; byte += 4) {
    platformEndian[byte >>> 2] = encryptedView.getUint32(byte);
  }
  // decrypt four word sequences, applying cipher-block chaining (CBC)
  // to each decrypted block
  for (word = 0; word < platformEndian.length; word += 4) {
    // decrypt the block
    decryptedBlock = decipher.decrypt(platformEndian.subarray(word, word + 4));

    // XOR with the IV, and restore network byte-order to obtain the
    // plaintext
    byte = word << 2;
    decryptedView.setUint32(byte, decryptedBlock[0] ^ initVector[0]);
    decryptedView.setUint32(byte + 4, decryptedBlock[1] ^ initVector[1]);
    decryptedView.setUint32(byte + 8, decryptedBlock[2] ^ initVector[2]);
    decryptedView.setUint32(byte + 12, decryptedBlock[3] ^ initVector[3]);

    // setup the IV for the next round
    initVector = platformEndian.subarray(word, word + 4);
  }

  // remove any padding
  return unpad(decrypted);
};

// exports
videojs.Hls.decrypt = decrypt;

})(window, window.videojs, window.pkcs7.unpad);
<file_sep>(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 FlvTag = window.videojs.Hls.FlvTag;

module('FLV tag');

test('writeBytes with zero length writes the entire array', function() {
  var
    tag = new FlvTag(FlvTag.VIDEO_TAG),
    headerLength = tag.length;
  tag.writeBytes(new Uint8Array([0x1, 0x2, 0x3]));

  equal(3 + headerLength, tag.length, '3 payload bytes are written');
});

test('writeShort writes a two byte sequence', function() {
  var
    tag = new FlvTag(FlvTag.VIDEO_TAG),
    headerLength = tag.length;
  tag.writeShort(0x0102);

  equal(2 + headerLength, tag.length, '2 bytes are written');
  equal(0x0102,
        new DataView(tag.bytes.buffer).getUint16(tag.length - 2),
        'the value is written');
});

test('writeBytes grows the internal byte array dynamically', function() {
  var
    tag = new FlvTag(FlvTag.VIDEO_TAG),
    tooManyBytes = new Uint8Array(tag.bytes.byteLength + 1);

  try {
    tag.writeBytes(tooManyBytes);
    ok(true, 'the buffer grew to fit the data');
  } catch(e) {
    ok(!e, 'the buffer should grow');
  }
});

})(this);  
