videojs-contrib-hls.test.js 105 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378
  1. /* eslint-disable max-len */
  2. import document from 'global/document';
  3. import videojs from 'video.js';
  4. import Events from 'video.js';
  5. import QUnit from 'qunit';
  6. import testDataManifests from './test-manifests.js';
  7. import {
  8. useFakeEnvironment,
  9. useFakeMediaSource,
  10. createPlayer,
  11. openMediaSource,
  12. standardXHRResponse,
  13. absoluteUrl
  14. } from './test-helpers.js';
  15. /* eslint-disable no-unused-vars */
  16. // we need this so that it can register hls with videojs
  17. import {HlsSourceHandler, HlsHandler, Hls} from '../src/videojs-contrib-hls';
  18. import window from 'global/window';
  19. // we need this so the plugin registers itself
  20. import 'videojs-contrib-quality-levels';
  21. /* eslint-enable no-unused-vars */
  22. const Flash = videojs.getTech('Flash');
  23. const ogHlsHandlerSetupQualityLevels = videojs.HlsHandler.prototype.setupQualityLevels_;
  24. let nextId = 0;
  25. // do a shallow copy of the properties of source onto the target object
  26. const merge = function(target, source) {
  27. let name;
  28. for (name in source) {
  29. target[name] = source[name];
  30. }
  31. };
  32. QUnit.module('HLS', {
  33. beforeEach(assert) {
  34. this.env = useFakeEnvironment(assert);
  35. this.requests = this.env.requests;
  36. this.mse = useFakeMediaSource();
  37. this.clock = this.env.clock;
  38. this.old = {};
  39. // mock out Flash features for phantomjs
  40. this.old.Flash = videojs.mergeOptions({}, Flash);
  41. /* eslint-disable camelcase */
  42. Flash.embed = function(swf, flashVars) {
  43. let el = document.createElement('div');
  44. el.id = 'vjs_mock_flash_' + nextId++;
  45. el.className = 'vjs-tech vjs-mock-flash';
  46. el.duration = Infinity;
  47. el.vjs_load = function() {};
  48. el.vjs_getProperty = function(attr) {
  49. if (attr === 'buffered') {
  50. return [[0, 0]];
  51. }
  52. return el[attr];
  53. };
  54. el.vjs_setProperty = function(attr, value) {
  55. el[attr] = value;
  56. };
  57. el.vjs_src = function() {};
  58. el.vjs_play = function() {};
  59. el.vjs_discontinuity = function() {};
  60. if (flashVars.autoplay) {
  61. el.autoplay = true;
  62. }
  63. if (flashVars.preload) {
  64. el.preload = flashVars.preload;
  65. }
  66. el.currentTime = 0;
  67. return el;
  68. };
  69. /* eslint-enable camelcase */
  70. this.old.FlashSupported = Flash.isSupported;
  71. Flash.isSupported = function() {
  72. return true;
  73. };
  74. // store functionality that some tests need to mock
  75. this.old.GlobalOptions = videojs.mergeOptions(videojs.options);
  76. // force the HLS tech to run
  77. this.old.NativeHlsSupport = videojs.Hls.supportsNativeHls;
  78. videojs.Hls.supportsNativeHls = false;
  79. this.old.Decrypt = videojs.Hls.Decrypter;
  80. videojs.Hls.Decrypter = function() {};
  81. // save and restore browser detection for the Firefox-specific tests
  82. this.old.browser = videojs.browser;
  83. videojs.browser = videojs.mergeOptions({}, videojs.browser);
  84. this.standardXHRResponse = (request, data) => {
  85. standardXHRResponse(request, data);
  86. // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
  87. // we have to use clock.tick to get the expected side effects of
  88. // SegmentLoader#handleUpdateEnd_
  89. this.clock.tick(1);
  90. };
  91. // setup a player
  92. this.player = createPlayer();
  93. this.clock.tick(1);
  94. },
  95. afterEach() {
  96. this.env.restore();
  97. this.mse.restore();
  98. merge(videojs.options, this.old.GlobalOptions);
  99. Flash.isSupported = this.old.FlashSupported;
  100. merge(Flash, this.old.Flash);
  101. videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
  102. videojs.Hls.Decrypter = this.old.Decrypt;
  103. videojs.browser = this.old.browser;
  104. this.player.dispose();
  105. }
  106. });
  107. QUnit.test('deprecation warning is show when using player.hls', function(assert) {
  108. let oldWarn = videojs.log.warn;
  109. let warning = '';
  110. let hlsPlayerAccessEvents = 0;
  111. this.player.src({
  112. src: 'manifest/playlist.m3u8',
  113. type: 'application/vnd.apple.mpegurl'
  114. });
  115. this.clock.tick(1);
  116. this.player.tech_.on('usage', (event) => {
  117. if (event.name === 'hls-player-access') {
  118. hlsPlayerAccessEvents++;
  119. }
  120. });
  121. videojs.log.warn = (text) => {
  122. warning = text;
  123. };
  124. assert.equal(hlsPlayerAccessEvents, 0, 'no hls-player-access event was fired');
  125. let hls = this.player.hls;
  126. assert.equal(hlsPlayerAccessEvents, 1, 'an hls-player-access event was fired');
  127. assert.equal(warning, 'player.hls is deprecated. Use player.tech_.hls instead.', 'warning would have been shown');
  128. assert.ok(hls, 'an instance of hls is returned by player.hls');
  129. videojs.log.warn = oldWarn;
  130. });
  131. QUnit.test('starts playing if autoplay is specified', function(assert) {
  132. this.player.autoplay(true);
  133. this.player.src({
  134. src: 'manifest/playlist.m3u8',
  135. type: 'application/vnd.apple.mpegurl'
  136. });
  137. this.clock.tick(1);
  138. // make sure play() is called *after* the media source opens
  139. openMediaSource(this.player, this.clock);
  140. this.standardXHRResponse(this.requests[0]);
  141. assert.ok(!this.player.paused(), 'not paused');
  142. });
  143. QUnit.test('stats are reset on each new source', function(assert) {
  144. this.player.src({
  145. src: 'manifest/playlist.m3u8',
  146. type: 'application/vnd.apple.mpegurl'
  147. });
  148. this.clock.tick(1);
  149. // make sure play() is called *after* the media source opens
  150. openMediaSource(this.player, this.clock);
  151. this.standardXHRResponse(this.requests.shift());
  152. this.standardXHRResponse(this.requests.shift());
  153. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'stat is set');
  154. this.player.src({
  155. src: 'manifest/master.m3u8',
  156. type: 'application/vnd.apple.mpegurl'
  157. });
  158. this.clock.tick(1);
  159. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 0, 'stat is reset');
  160. });
  161. QUnit.test('XHR requests first byte range on play', function(assert) {
  162. this.player.src({
  163. src: 'manifest/playlist.m3u8',
  164. type: 'application/vnd.apple.mpegurl'
  165. });
  166. this.clock.tick(1);
  167. this.player.tech_.triggerReady();
  168. this.clock.tick(1);
  169. this.player.tech_.trigger('play');
  170. openMediaSource(this.player, this.clock);
  171. this.standardXHRResponse(this.requests[0]);
  172. assert.equal(this.requests[1].headers.Range, 'bytes=0-522827');
  173. });
  174. QUnit.test('Seeking requests correct byte range', function(assert) {
  175. this.player.src({
  176. src: 'manifest/playlist.m3u8',
  177. type: 'application/vnd.apple.mpegurl'
  178. });
  179. this.clock.tick(1);
  180. this.player.tech_.trigger('play');
  181. openMediaSource(this.player, this.clock);
  182. this.standardXHRResponse(this.requests[0]);
  183. this.clock.tick(1);
  184. this.player.currentTime(41);
  185. this.clock.tick(2);
  186. assert.equal(this.requests[2].headers.Range, 'bytes=2299992-2835603');
  187. });
  188. QUnit.test('autoplay seeks to the live point after playlist load', function(assert) {
  189. let currentTime = 0;
  190. this.player.autoplay(true);
  191. this.player.on('seeking', () => {
  192. currentTime = this.player.currentTime();
  193. });
  194. this.player.src({
  195. src: 'liveStart30sBefore.m3u8',
  196. type: 'application/vnd.apple.mpegurl'
  197. });
  198. this.clock.tick(1);
  199. openMediaSource(this.player, this.clock);
  200. this.player.tech_.trigger('play');
  201. this.standardXHRResponse(this.requests.shift());
  202. this.clock.tick(1);
  203. assert.notEqual(currentTime, 0, 'seeked on autoplay');
  204. });
  205. QUnit.test('autoplay seeks to the live point after media source open', function(assert) {
  206. let currentTime = 0;
  207. this.player.autoplay(true);
  208. this.player.on('seeking', () => {
  209. currentTime = this.player.currentTime();
  210. });
  211. this.player.src({
  212. src: 'liveStart30sBefore.m3u8',
  213. type: 'application/vnd.apple.mpegurl'
  214. });
  215. this.clock.tick(1);
  216. this.player.tech_.triggerReady();
  217. this.clock.tick(1);
  218. this.standardXHRResponse(this.requests.shift());
  219. openMediaSource(this.player, this.clock);
  220. this.player.tech_.trigger('play');
  221. this.clock.tick(1);
  222. assert.notEqual(currentTime, 0, 'seeked on autoplay');
  223. });
  224. QUnit.test('autoplay seeks to the live point after tech fires loadedmetadata in ie11',
  225. function(assert) {
  226. videojs.browser.IE_VERSION = 11;
  227. let currentTime = 0;
  228. this.player.autoplay(true);
  229. this.player.on('seeking', () => {
  230. currentTime = this.player.currentTime();
  231. });
  232. this.player.src({
  233. src: 'liveStart30sBefore.m3u8',
  234. type: 'application/vnd.apple.mpegurl'
  235. });
  236. this.clock.tick(1);
  237. openMediaSource(this.player, this.clock);
  238. this.player.tech_.trigger('play');
  239. this.standardXHRResponse(this.requests.shift());
  240. this.clock.tick(1);
  241. assert.equal(currentTime, 0, 'have not played yet');
  242. this.player.tech_.trigger('loadedmetadata');
  243. this.clock.tick(1);
  244. assert.notEqual(currentTime, 0, 'seeked after tech is ready');
  245. });
  246. QUnit.test('duration is set when the source opens after the playlist is loaded',
  247. function(assert) {
  248. this.player.src({
  249. src: 'media.m3u8',
  250. type: 'application/vnd.apple.mpegurl'
  251. });
  252. this.clock.tick(1);
  253. this.player.tech_.triggerReady();
  254. this.clock.tick(1);
  255. this.standardXHRResponse(this.requests.shift());
  256. openMediaSource(this.player, this.clock);
  257. assert.equal(this.player.tech_.hls.mediaSource.duration,
  258. 40,
  259. 'set the duration');
  260. });
  261. QUnit.test('codecs are passed to the source buffer', function(assert) {
  262. let codecs = [];
  263. this.player.src({
  264. src: 'custom-codecs.m3u8',
  265. type: 'application/vnd.apple.mpegurl'
  266. });
  267. this.clock.tick(1);
  268. openMediaSource(this.player, this.clock);
  269. let addSourceBuffer = this.player.tech_.hls.mediaSource.addSourceBuffer;
  270. this.player.tech_.hls.mediaSource.addSourceBuffer = function(codec) {
  271. codecs.push(codec);
  272. return addSourceBuffer.call(this, codec);
  273. };
  274. this.requests.shift().respond(200, null,
  275. '#EXTM3U\n' +
  276. '#EXT-X-STREAM-INF:CODECS="avc1.dd00dd, mp4a.40.f"\n' +
  277. 'media.m3u8\n');
  278. this.standardXHRResponse(this.requests.shift());
  279. assert.equal(codecs.length, 1, 'created a source buffer');
  280. assert.equal(codecs[0], 'video/mp2t; codecs="avc1.dd00dd, mp4a.40.f"', 'specified the codecs');
  281. });
  282. QUnit.test('including HLS as a tech does not error', function(assert) {
  283. let player = createPlayer({
  284. techOrder: ['hls', 'html5']
  285. });
  286. this.clock.tick(1);
  287. assert.ok(player, 'created the player');
  288. assert.equal(this.env.log.warn.calls, 2, 'logged two warnings for deprecations');
  289. });
  290. QUnit.test('creates a PlaylistLoader on init', function(assert) {
  291. this.player.src({
  292. src: 'manifest/playlist.m3u8',
  293. type: 'application/vnd.apple.mpegurl'
  294. });
  295. this.clock.tick(1);
  296. openMediaSource(this.player, this.clock);
  297. this.player.src({
  298. src: 'manifest/playlist.m3u8',
  299. type: 'application/vnd.apple.mpegurl'
  300. });
  301. this.clock.tick(1);
  302. openMediaSource(this.player, this.clock);
  303. assert.equal(this.requests[0].aborted, true, 'aborted previous src');
  304. this.standardXHRResponse(this.requests[1]);
  305. assert.ok(this.player.tech_.hls.playlists.master,
  306. 'set the master playlist');
  307. assert.ok(this.player.tech_.hls.playlists.media(),
  308. 'set the media playlist');
  309. assert.ok(this.player.tech_.hls.playlists.media().segments,
  310. 'the segment entries are parsed');
  311. assert.strictEqual(this.player.tech_.hls.playlists.master.playlists[0],
  312. this.player.tech_.hls.playlists.media(),
  313. 'the playlist is selected');
  314. });
  315. QUnit.test('sets the duration if one is available on the playlist', function(assert) {
  316. let events = 0;
  317. this.player.src({
  318. src: 'manifest/media.m3u8',
  319. type: 'application/vnd.apple.mpegurl'
  320. });
  321. this.clock.tick(1);
  322. openMediaSource(this.player, this.clock);
  323. this.player.tech_.on('durationchange', function() {
  324. events++;
  325. });
  326. this.standardXHRResponse(this.requests[0]);
  327. assert.equal(this.player.tech_.hls.mediaSource.duration,
  328. 40,
  329. 'set the duration');
  330. assert.equal(events, 1, 'durationchange is fired');
  331. });
  332. QUnit.test('estimates individual segment durations if needed', function(assert) {
  333. let changes = 0;
  334. this.player.src({
  335. src: 'http://example.com/manifest/missingExtinf.m3u8',
  336. type: 'application/vnd.apple.mpegurl'
  337. });
  338. this.clock.tick(1);
  339. openMediaSource(this.player, this.clock);
  340. this.player.tech_.hls.mediaSource.duration = NaN;
  341. this.player.tech_.on('durationchange', function() {
  342. changes++;
  343. });
  344. this.standardXHRResponse(this.requests[0]);
  345. assert.strictEqual(this.player.tech_.hls.mediaSource.duration,
  346. this.player.tech_.hls.playlists.media().segments.length * 10,
  347. 'duration is updated');
  348. assert.strictEqual(changes, 1, 'one durationchange fired');
  349. });
  350. QUnit.test('translates seekable by the starting time for live playlists', function(assert) {
  351. let seekable;
  352. this.player.src({
  353. src: 'media.m3u8',
  354. type: 'application/vnd.apple.mpegurl'
  355. });
  356. this.clock.tick(1);
  357. openMediaSource(this.player, this.clock);
  358. this.requests.shift().respond(200, null,
  359. '#EXTM3U\n' +
  360. '#EXT-X-MEDIA-SEQUENCE:15\n' +
  361. '#EXT-X-TARGETDURATION:10\n' +
  362. '#EXTINF:10,\n' +
  363. '0.ts\n' +
  364. '#EXTINF:10,\n' +
  365. '1.ts\n' +
  366. '#EXTINF:10,\n' +
  367. '2.ts\n' +
  368. '#EXTINF:10,\n' +
  369. '3.ts\n');
  370. seekable = this.player.seekable();
  371. assert.equal(seekable.length, 1, 'one seekable range');
  372. assert.equal(seekable.start(0), 0, 'the earliest possible position is at zero');
  373. assert.equal(seekable.end(0), 10, 'end is relative to the start');
  374. });
  375. QUnit.test('starts downloading a segment on loadedmetadata', function(assert) {
  376. this.player.src({
  377. src: 'manifest/media.m3u8',
  378. type: 'application/vnd.apple.mpegurl'
  379. });
  380. this.clock.tick(1);
  381. this.player.buffered = function() {
  382. return videojs.createTimeRange(0, 0);
  383. };
  384. openMediaSource(this.player, this.clock);
  385. this.standardXHRResponse(this.requests[0]);
  386. this.standardXHRResponse(this.requests[1]);
  387. assert.strictEqual(this.requests[1].url,
  388. absoluteUrl('manifest/media-00001.ts'),
  389. 'the first segment is requested');
  390. // verify stats
  391. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  392. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  393. });
  394. QUnit.test('re-initializes the handler for each source', function(assert) {
  395. let firstPlaylists;
  396. let secondPlaylists;
  397. let firstMSE;
  398. let secondMSE;
  399. let aborts = 0;
  400. let masterPlaylistController;
  401. this.player.src({
  402. src: 'manifest/master.m3u8',
  403. type: 'application/vnd.apple.mpegurl'
  404. });
  405. this.clock.tick(1);
  406. openMediaSource(this.player, this.clock);
  407. firstPlaylists = this.player.tech_.hls.playlists;
  408. firstMSE = this.player.tech_.hls.mediaSource;
  409. this.standardXHRResponse(this.requests.shift());
  410. this.standardXHRResponse(this.requests.shift());
  411. masterPlaylistController = this.player.tech_.hls.masterPlaylistController_;
  412. masterPlaylistController.mainSegmentLoader_.sourceUpdater_.sourceBuffer_.abort = () => {
  413. aborts++;
  414. };
  415. this.player.src({
  416. src: 'manifest/master.m3u8',
  417. type: 'application/vnd.apple.mpegurl'
  418. });
  419. this.clock.tick(1);
  420. openMediaSource(this.player, this.clock);
  421. secondPlaylists = this.player.tech_.hls.playlists;
  422. secondMSE = this.player.tech_.hls.mediaSource;
  423. assert.equal(1, aborts, 'aborted the old source buffer');
  424. assert.ok(this.requests[0].aborted, 'aborted the old segment request');
  425. assert.notStrictEqual(firstPlaylists,
  426. secondPlaylists,
  427. 'the playlist object is not reused');
  428. assert.notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused');
  429. });
  430. QUnit.test('triggers a media source error when an initial playlist request errors',
  431. function(assert) {
  432. this.player.src({
  433. src: 'manifest/master.m3u8',
  434. type: 'application/vnd.apple.mpegurl'
  435. });
  436. this.clock.tick(1);
  437. openMediaSource(this.player, this.clock);
  438. this.requests.pop().respond(500);
  439. assert.equal(this.player.tech_.hls.mediaSource.error_,
  440. 'network',
  441. 'a network error is triggered');
  442. });
  443. QUnit.test(
  444. 'triggers a player error when an initial playlist request errors and the media source ' +
  445. 'isn\'t open',
  446. function(assert) {
  447. const done = assert.async();
  448. const origError = videojs.log.error;
  449. const errLogs = [];
  450. const endOfStreams = [];
  451. videojs.log.error = (log) => errLogs.push(log);
  452. this.player.src({
  453. src: 'manifest/master.m3u8',
  454. type: 'application/vnd.apple.mpegurl'
  455. });
  456. openMediaSource(this.player, this.clock);
  457. this.player.tech_.hls.masterPlaylistController_.mediaSource.endOfStream = (type) => {
  458. endOfStreams.push(type);
  459. throw new Error();
  460. };
  461. this.player.on('error', () => {
  462. const error = this.player.error();
  463. assert.equal(endOfStreams.length, 1, 'one endOfStream called');
  464. assert.equal(endOfStreams[0], 'network', 'endOfStream called with network');
  465. assert.equal(error.code, 2, 'error has correct code');
  466. assert.equal(error.message,
  467. 'HLS playlist request error at URL: manifest/master.m3u8',
  468. 'error has correct message');
  469. assert.equal(errLogs.length, 1, 'logged an error');
  470. videojs.log.error = origError;
  471. assert.notOk(this.player.tech_.hls.mediaSource.error_, 'no media source error');
  472. done();
  473. });
  474. this.requests.pop().respond(500);
  475. });
  476. QUnit.test('downloads media playlists after loading the master', function(assert) {
  477. this.player.src({
  478. src: 'manifest/master.m3u8',
  479. type: 'application/vnd.apple.mpegurl'
  480. });
  481. this.clock.tick(1);
  482. openMediaSource(this.player, this.clock);
  483. this.player.tech_.hls.bandwidth = 20e10;
  484. this.standardXHRResponse(this.requests[0]);
  485. this.standardXHRResponse(this.requests[1]);
  486. this.standardXHRResponse(this.requests[2]);
  487. assert.strictEqual(this.requests[0].url,
  488. 'manifest/master.m3u8',
  489. 'master playlist requested');
  490. assert.strictEqual(this.requests[1].url,
  491. absoluteUrl('manifest/media2.m3u8'),
  492. 'media playlist requested');
  493. assert.strictEqual(this.requests[2].url,
  494. absoluteUrl('manifest/media2-00001.ts'),
  495. 'first segment requested');
  496. // verify stats
  497. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  498. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  499. });
  500. QUnit.test('setting bandwidth resets throughput', function(assert) {
  501. this.player.src({
  502. src: 'manifest/master.m3u8',
  503. type: 'application/vnd.apple.mpegurl'
  504. });
  505. this.clock.tick(1);
  506. this.player.tech_.hls.throughput = 1000;
  507. assert.strictEqual(this.player.tech_.hls.throughput,
  508. 1000,
  509. 'throughput is set');
  510. this.player.tech_.hls.bandwidth = 20e10;
  511. assert.strictEqual(this.player.tech_.hls.throughput,
  512. 0,
  513. 'throughput is reset when bandwidth is specified');
  514. });
  515. QUnit.test('a thoughput of zero is ignored in systemBandwidth', function(assert) {
  516. this.player.src({
  517. src: 'manifest/master.m3u8',
  518. type: 'application/vnd.apple.mpegurl'
  519. });
  520. this.clock.tick(1);
  521. this.player.tech_.hls.bandwidth = 20e10;
  522. assert.strictEqual(this.player.tech_.hls.throughput,
  523. 0,
  524. 'throughput is reset when bandwidth is specified');
  525. assert.strictEqual(this.player.tech_.hls.systemBandwidth,
  526. 20e10,
  527. 'systemBandwidth is the same as bandwidth');
  528. });
  529. QUnit.test('systemBandwidth is a combination of thoughput and bandwidth', function(assert) {
  530. this.player.src({
  531. src: 'manifest/master.m3u8',
  532. type: 'application/vnd.apple.mpegurl'
  533. });
  534. this.clock.tick(1);
  535. this.player.tech_.hls.bandwidth = 20e10;
  536. this.player.tech_.hls.throughput = 20e10;
  537. // 1 / ( 1 / 20e10 + 1 / 20e10) = 10e10
  538. assert.strictEqual(this.player.tech_.hls.systemBandwidth,
  539. 10e10,
  540. 'systemBandwidth is the combination of bandwidth and throughput');
  541. });
  542. QUnit.test('upshifts if the initial bandwidth hint is high', function(assert) {
  543. this.player.src({
  544. src: 'manifest/master.m3u8',
  545. type: 'application/vnd.apple.mpegurl'
  546. });
  547. this.clock.tick(1);
  548. openMediaSource(this.player, this.clock);
  549. this.player.tech_.hls.bandwidth = 10e20;
  550. this.standardXHRResponse(this.requests[0]);
  551. this.standardXHRResponse(this.requests[1]);
  552. this.standardXHRResponse(this.requests[2]);
  553. assert.strictEqual(
  554. this.requests[0].url,
  555. 'manifest/master.m3u8',
  556. 'master playlist requested'
  557. );
  558. assert.strictEqual(
  559. this.requests[1].url,
  560. absoluteUrl('manifest/media2.m3u8'),
  561. 'media playlist requested'
  562. );
  563. assert.strictEqual(
  564. this.requests[2].url,
  565. absoluteUrl('manifest/media2-00001.ts'),
  566. 'first segment requested'
  567. );
  568. // verify stats
  569. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  570. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  571. });
  572. QUnit.test('downshifts if the initial bandwidth hint is low', function(assert) {
  573. this.player.src({
  574. src: 'manifest/master.m3u8',
  575. type: 'application/vnd.apple.mpegurl'
  576. });
  577. this.clock.tick(1);
  578. openMediaSource(this.player, this.clock);
  579. this.player.tech_.hls.bandwidth = 100;
  580. this.standardXHRResponse(this.requests[0]);
  581. this.standardXHRResponse(this.requests[1]);
  582. this.standardXHRResponse(this.requests[2]);
  583. assert.strictEqual(this.requests[0].url,
  584. 'manifest/master.m3u8',
  585. 'master playlist requested');
  586. assert.strictEqual(this.requests[1].url,
  587. absoluteUrl('manifest/media1.m3u8'),
  588. 'media playlist requested');
  589. assert.strictEqual(this.requests[2].url,
  590. absoluteUrl('manifest/media1-00001.ts'),
  591. 'first segment requested');
  592. // verify stats
  593. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  594. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  595. });
  596. QUnit.test('buffer checks are noops until a media playlist is ready', function(assert) {
  597. this.player.src({
  598. src: 'manifest/media.m3u8',
  599. type: 'application/vnd.apple.mpegurl'
  600. });
  601. this.clock.tick(1);
  602. openMediaSource(this.player, this.clock);
  603. this.clock.tick(10 * 1000);
  604. assert.strictEqual(1, this.requests.length, 'one request was made');
  605. assert.strictEqual(this.requests[0].url,
  606. 'manifest/media.m3u8',
  607. 'media playlist requested');
  608. });
  609. QUnit.test('buffer checks are noops when only the master is ready', function(assert) {
  610. this.player.src({
  611. src: 'manifest/master.m3u8',
  612. type: 'application/vnd.apple.mpegurl'
  613. });
  614. this.clock.tick(1);
  615. openMediaSource(this.player, this.clock);
  616. // master
  617. this.standardXHRResponse(this.requests.shift());
  618. // media
  619. this.standardXHRResponse(this.requests.shift());
  620. // ignore any outstanding segment requests
  621. this.requests.length = 0;
  622. // load in a new playlist which will cause playlists.media() to be
  623. // undefined while it is being fetched
  624. this.player.src({
  625. src: 'manifest/master.m3u8',
  626. type: 'application/vnd.apple.mpegurl'
  627. });
  628. openMediaSource(this.player, this.clock);
  629. // respond with the master playlist but don't send the media playlist yet
  630. // force media1 to be requested
  631. this.player.tech_.hls.bandwidth = 1;
  632. // master
  633. this.standardXHRResponse(this.requests.shift());
  634. this.clock.tick(10 * 1000);
  635. assert.strictEqual(this.requests.length, 1, 'one request was made');
  636. assert.strictEqual(this.requests[0].url,
  637. absoluteUrl('manifest/media1.m3u8'),
  638. 'media playlist requested');
  639. // verify stats
  640. assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
  641. });
  642. QUnit.test('selects a playlist below the current bandwidth', function(assert) {
  643. let playlist;
  644. this.player.src({
  645. src: 'manifest/master.m3u8',
  646. type: 'application/vnd.apple.mpegurl'
  647. });
  648. this.clock.tick(1);
  649. openMediaSource(this.player, this.clock);
  650. this.standardXHRResponse(this.requests[0]);
  651. // the default playlist has a really high bitrate
  652. this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10;
  653. // playlist 1 has a very low bitrate
  654. this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1;
  655. // but the detected client bandwidth is really low
  656. this.player.tech_.hls.bandwidth = 10;
  657. playlist = this.player.tech_.hls.selectPlaylist();
  658. assert.strictEqual(playlist,
  659. this.player.tech_.hls.playlists.master.playlists[1],
  660. 'the low bitrate stream is selected');
  661. // verify stats
  662. assert.equal(this.player.tech_.hls.stats.bandwidth, 10, 'bandwidth set above');
  663. });
  664. QUnit.test('selects a primary rendtion when there are multiple rendtions share same attributes', function(assert) {
  665. let playlist;
  666. this.player.src({
  667. src: 'manifest/master.m3u8',
  668. type: 'application/vnd.apple.mpegurl'
  669. });
  670. openMediaSource(this.player, this.clock);
  671. standardXHRResponse(this.requests[0]);
  672. // covers playlists with same bandwidth but different resolution and different bandwidth but same resolution
  673. this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 528;
  674. this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 528;
  675. this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
  676. this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;
  677. this.player.tech_.hls.bandwidth = 1000;
  678. playlist = this.player.tech_.hls.selectPlaylist();
  679. assert.strictEqual(playlist,
  680. this.player.tech_.hls.playlists.master.playlists[2],
  681. 'select the rendition with largest bandwidth and just-larger-than video player');
  682. // verify stats
  683. assert.equal(this.player.tech_.hls.stats.bandwidth, 1000, 'bandwidth set above');
  684. // covers playlists share same bandwidth and resolutions
  685. this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 728;
  686. this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.width = 960;
  687. this.player.tech_.hls.playlists.master.playlists[0].attributes.RESOLUTION.height = 540;
  688. this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 728;
  689. this.player.tech_.hls.playlists.master.playlists[2].attributes.BANDWIDTH = 728;
  690. this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.width = 960;
  691. this.player.tech_.hls.playlists.master.playlists[2].attributes.RESOLUTION.height = 540;
  692. this.player.tech_.hls.playlists.master.playlists[3].attributes.BANDWIDTH = 728;
  693. this.player.tech_.hls.bandwidth = 1000;
  694. playlist = this.player.tech_.hls.selectPlaylist();
  695. assert.strictEqual(playlist,
  696. this.player.tech_.hls.playlists.master.playlists[0],
  697. 'the primary rendition is selected');
  698. });
  699. QUnit.test('allows initial bandwidth to be provided', function(assert) {
  700. this.player.src({
  701. src: 'manifest/master.m3u8',
  702. type: 'application/vnd.apple.mpegurl'
  703. });
  704. this.clock.tick(1);
  705. openMediaSource(this.player, this.clock);
  706. this.player.tech_.hls.bandwidth = 500;
  707. this.requests[0].bandwidth = 1;
  708. this.requests.shift().respond(200, null,
  709. '#EXTM3U\n' +
  710. '#EXT-X-PLAYLIST-TYPE:VOD\n' +
  711. '#EXT-X-TARGETDURATION:10\n');
  712. assert.equal(this.player.tech_.hls.bandwidth,
  713. 500,
  714. 'prefers user-specified initial bandwidth');
  715. // verify stats
  716. assert.equal(this.player.tech_.hls.stats.bandwidth, 500, 'bandwidth set above');
  717. });
  718. QUnit.test('raises the minimum bitrate for a stream proportionially', function(assert) {
  719. let playlist;
  720. this.player.src({
  721. src: 'manifest/master.m3u8',
  722. type: 'application/vnd.apple.mpegurl'
  723. });
  724. this.clock.tick(1);
  725. openMediaSource(this.player, this.clock);
  726. this.standardXHRResponse(this.requests[0]);
  727. // the default playlist's bandwidth + 10% is assert.equal to the current bandwidth
  728. this.player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10;
  729. this.player.tech_.hls.bandwidth = 11;
  730. // 9.9 * 1.1 < 11
  731. this.player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9;
  732. playlist = this.player.tech_.hls.selectPlaylist();
  733. assert.strictEqual(playlist,
  734. this.player.tech_.hls.playlists.master.playlists[1],
  735. 'a lower bitrate stream is selected');
  736. // verify stats
  737. assert.equal(this.player.tech_.hls.stats.bandwidth, 11, 'bandwidth set above');
  738. });
  739. QUnit.test('uses the lowest bitrate if no other is suitable', function(assert) {
  740. let playlist;
  741. this.player.src({
  742. src: 'manifest/master.m3u8',
  743. type: 'application/vnd.apple.mpegurl'
  744. });
  745. this.clock.tick(1);
  746. openMediaSource(this.player, this.clock);
  747. this.standardXHRResponse(this.requests[0]);
  748. // the lowest bitrate playlist is much greater than 1b/s
  749. this.player.tech_.hls.bandwidth = 1;
  750. playlist = this.player.tech_.hls.selectPlaylist();
  751. // playlist 1 has the lowest advertised bitrate
  752. assert.strictEqual(playlist,
  753. this.player.tech_.hls.playlists.master.playlists[1],
  754. 'the lowest bitrate stream is selected');
  755. // verify stats
  756. assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
  757. });
  758. QUnit.test('selects the correct rendition by tech dimensions', function(assert) {
  759. let playlist;
  760. let hls;
  761. this.player.src({
  762. src: 'manifest/master.m3u8',
  763. type: 'application/vnd.apple.mpegurl'
  764. });
  765. this.clock.tick(1);
  766. openMediaSource(this.player, this.clock);
  767. this.standardXHRResponse(this.requests[0]);
  768. hls = this.player.tech_.hls;
  769. this.player.width(640);
  770. this.player.height(360);
  771. hls.bandwidth = 3000000;
  772. playlist = hls.selectPlaylist();
  773. assert.deepEqual(playlist.attributes.RESOLUTION,
  774. {width: 960, height: 540},
  775. 'should return the correct resolution by tech dimensions');
  776. assert.equal(playlist.attributes.BANDWIDTH,
  777. 1928000,
  778. 'should have the expected bandwidth in case of multiple');
  779. this.player.width(1920);
  780. this.player.height(1080);
  781. hls.bandwidth = 3000000;
  782. playlist = hls.selectPlaylist();
  783. assert.deepEqual(playlist.attributes.RESOLUTION,
  784. {width: 960, height: 540},
  785. 'should return the correct resolution by tech dimensions');
  786. assert.equal(playlist.attributes.BANDWIDTH,
  787. 1928000,
  788. 'should have the expected bandwidth in case of multiple');
  789. this.player.width(396);
  790. this.player.height(224);
  791. playlist = hls.selectPlaylist();
  792. assert.deepEqual(playlist.attributes.RESOLUTION,
  793. {width: 396, height: 224},
  794. 'should return the correct resolution by ' +
  795. 'tech dimensions, if exact match');
  796. assert.equal(playlist.attributes.BANDWIDTH,
  797. 440000,
  798. 'should have the expected bandwidth in case of multiple, if exact match');
  799. this.player.width(395);
  800. this.player.height(222);
  801. playlist = this.player.tech_.hls.selectPlaylist();
  802. assert.deepEqual(playlist.attributes.RESOLUTION,
  803. {width: 396, height: 224},
  804. 'should return the next larger resolution by tech dimensions, ' +
  805. 'if no exact match exists');
  806. assert.equal(playlist.attributes.BANDWIDTH,
  807. 440000,
  808. 'should have the expected bandwidth in case of multiple, if exact match');
  809. // verify stats
  810. assert.equal(this.player.tech_.hls.stats.bandwidth, 3000000, 'bandwidth set above');
  811. });
  812. QUnit.test('selects the highest bitrate playlist when the player dimensions are ' +
  813. 'larger than any of the variants', function(assert) {
  814. let playlist;
  815. this.player.src({
  816. src: 'manifest/master.m3u8',
  817. type: 'application/vnd.apple.mpegurl'
  818. });
  819. this.clock.tick(1);
  820. openMediaSource(this.player, this.clock);
  821. // master
  822. this.requests.shift().respond(200, null,
  823. '#EXTM3U\n' +
  824. '#EXT-X-STREAM-INF:BANDWIDTH=1000,RESOLUTION=2x1\n' +
  825. 'media.m3u8\n' +
  826. '#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' +
  827. 'media1.m3u8\n');
  828. // media
  829. this.standardXHRResponse(this.requests.shift());
  830. this.player.tech_.hls.bandwidth = 1e10;
  831. this.player.width(1024);
  832. this.player.height(768);
  833. playlist = this.player.tech_.hls.selectPlaylist();
  834. assert.equal(playlist.attributes.BANDWIDTH,
  835. 1000,
  836. 'selected the highest bandwidth variant');
  837. // verify stats
  838. assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
  839. });
  840. QUnit.test('filters playlists that are currently excluded', function(assert) {
  841. let playlist;
  842. this.player.src({
  843. src: 'manifest/master.m3u8',
  844. type: 'application/vnd.apple.mpegurl'
  845. });
  846. this.clock.tick(1);
  847. openMediaSource(this.player, this.clock);
  848. this.player.tech_.hls.bandwidth = 1e10;
  849. // master
  850. this.requests.shift().respond(200, null,
  851. '#EXTM3U\n' +
  852. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  853. 'media.m3u8\n' +
  854. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  855. 'media1.m3u8\n');
  856. // media
  857. this.standardXHRResponse(this.requests.shift());
  858. // exclude the current playlist
  859. this.player.tech_.hls.playlists.master.playlists[0].excludeUntil = +new Date() + 1000;
  860. playlist = this.player.tech_.hls.selectPlaylist();
  861. assert.equal(playlist,
  862. this.player.tech_.hls.playlists.master.playlists[1],
  863. 'respected exclusions');
  864. // timeout the exclusion
  865. this.clock.tick(1000);
  866. playlist = this.player.tech_.hls.selectPlaylist();
  867. assert.equal(playlist,
  868. this.player.tech_.hls.playlists.master.playlists[0],
  869. 'expired the exclusion');
  870. // verify stats
  871. assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
  872. });
  873. QUnit.test('does not blacklist compatible H.264 codec strings', function(assert) {
  874. let master;
  875. this.player.src({
  876. src: 'manifest/master.m3u8',
  877. type: 'application/vnd.apple.mpegurl'
  878. });
  879. this.clock.tick(1);
  880. openMediaSource(this.player, this.clock);
  881. this.player.tech_.hls.bandwidth = 1;
  882. // master
  883. this.requests.shift()
  884. .respond(200, null,
  885. '#EXTM3U\n' +
  886. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.5"\n' +
  887. 'media.m3u8\n' +
  888. '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400f,mp4a.40.5"\n' +
  889. 'media1.m3u8\n');
  890. // media
  891. this.standardXHRResponse(this.requests.shift());
  892. master = this.player.tech_.hls.playlists.master;
  893. assert.strictEqual(typeof master.playlists[0].excludeUntil,
  894. 'undefined',
  895. 'did not blacklist');
  896. assert.strictEqual(typeof master.playlists[1].excludeUntil,
  897. 'undefined',
  898. 'did not blacklist');
  899. // verify stats
  900. assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
  901. });
  902. QUnit.test('does not blacklist compatible AAC codec strings', function(assert) {
  903. let master;
  904. this.player.src({
  905. src: 'manifest/master.m3u8',
  906. type: 'application/vnd.apple.mpegurl'
  907. });
  908. this.clock.tick(1);
  909. openMediaSource(this.player, this.clock);
  910. this.player.tech_.hls.bandwidth = 1;
  911. // master
  912. this.requests.shift()
  913. .respond(200, null,
  914. '#EXTM3U\n' +
  915. '#EXT-X-STREAM-INF:BANDWIDTH=1,CODECS="avc1.4d400d,mp4a.40.2"\n' +
  916. 'media.m3u8\n' +
  917. '#EXT-X-STREAM-INF:BANDWIDTH=10,CODECS="avc1.4d400d,not-an-audio-codec"\n' +
  918. 'media1.m3u8\n');
  919. // media
  920. this.standardXHRResponse(this.requests.shift());
  921. master = this.player.tech_.hls.playlists.master;
  922. assert.strictEqual(typeof master.playlists[0].excludeUntil,
  923. 'undefined',
  924. 'did not blacklist mp4a.40.2');
  925. assert.strictEqual(master.playlists[1].excludeUntil,
  926. Infinity,
  927. 'blacklisted invalid audio codec');
  928. // verify stats
  929. assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth set above');
  930. });
  931. QUnit.test('cancels outstanding XHRs when seeking', function(assert) {
  932. this.player.src({
  933. src: 'manifest/media.m3u8',
  934. type: 'application/vnd.apple.mpegurl'
  935. });
  936. this.clock.tick(1);
  937. openMediaSource(this.player, this.clock);
  938. this.standardXHRResponse(this.requests[0]);
  939. this.player.tech_.hls.media = {
  940. segments: [{
  941. uri: '0.ts',
  942. duration: 10
  943. }, {
  944. uri: '1.ts',
  945. duration: 10
  946. }]
  947. };
  948. // attempt to seek while the download is in progress
  949. this.player.currentTime(7);
  950. this.clock.tick(2);
  951. assert.ok(this.requests[1].aborted, 'XHR aborted');
  952. assert.strictEqual(this.requests.length, 3, 'opened new XHR');
  953. });
  954. QUnit.test('does not abort segment loading for in-buffer seeking', function(assert) {
  955. this.player.src({
  956. src: 'manifest/media.m3u8',
  957. type: 'application/vnd.apple.mpegurl'
  958. });
  959. this.clock.tick(1);
  960. openMediaSource(this.player, this.clock);
  961. this.standardXHRResponse(this.requests.shift());
  962. this.player.tech_.buffered = function() {
  963. return videojs.createTimeRange(0, 20);
  964. };
  965. this.player.tech_.setCurrentTime(11);
  966. this.clock.tick(1);
  967. assert.equal(this.requests.length, 1, 'did not abort the outstanding request');
  968. });
  969. QUnit.test('segment 404 should trigger blacklisting of media', function(assert) {
  970. let media;
  971. this.player.src({
  972. src: 'manifest/master.m3u8',
  973. type: 'application/vnd.apple.mpegurl'
  974. });
  975. this.clock.tick(1);
  976. openMediaSource(this.player, this.clock);
  977. this.player.tech_.hls.bandwidth = 20000;
  978. // master
  979. this.standardXHRResponse(this.requests[0]);
  980. // media
  981. this.standardXHRResponse(this.requests[1]);
  982. media = this.player.tech_.hls.playlists.media_;
  983. // segment
  984. this.requests[2].respond(400);
  985. assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
  986. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  987. // verify stats
  988. assert.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
  989. });
  990. QUnit.test('playlist 404 should blacklist media', function(assert) {
  991. let media;
  992. let url;
  993. let blacklistplaylist = 0;
  994. let retryplaylist = 0;
  995. let hlsRenditionBlacklistedEvents = 0;
  996. this.player.src({
  997. src: 'manifest/master.m3u8',
  998. type: 'application/vnd.apple.mpegurl'
  999. });
  1000. this.clock.tick(1);
  1001. openMediaSource(this.player, this.clock);
  1002. this.player.tech_.on('blacklistplaylist', () => blacklistplaylist++);
  1003. this.player.tech_.on('retryplaylist', () => retryplaylist++);
  1004. this.player.tech_.on('usage', (event) => {
  1005. if (event.name === 'hls-rendition-blacklisted') {
  1006. hlsRenditionBlacklistedEvents++;
  1007. }
  1008. });
  1009. this.player.tech_.hls.bandwidth = 1e10;
  1010. // master
  1011. this.requests[0].respond(200, null,
  1012. '#EXTM3U\n' +
  1013. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  1014. 'media.m3u8\n' +
  1015. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1016. 'media1.m3u8\n');
  1017. assert.equal(typeof this.player.tech_.hls.playlists.media_,
  1018. 'undefined',
  1019. 'no media is initially set');
  1020. assert.equal(blacklistplaylist, 0, 'there is no blacklisted playlist');
  1021. assert.equal(hlsRenditionBlacklistedEvents, 0, 'no hls-rendition-blacklisted event was fired');
  1022. // media
  1023. this.requests[1].respond(404);
  1024. url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
  1025. media = this.player.tech_.hls.playlists.master.playlists[url];
  1026. assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
  1027. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1028. assert.equal(this.env.log.warn.args[0],
  1029. 'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
  1030. 'log generic error message');
  1031. assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
  1032. assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
  1033. assert.equal(retryplaylist, 0, 'haven\'t retried any playlist');
  1034. // request for the final available media
  1035. this.requests[2].respond(404);
  1036. url = this.requests[2].url.slice(this.requests[2].url.lastIndexOf('/') + 1);
  1037. media = this.player.tech_.hls.playlists.master.playlists[url];
  1038. // media wasn't blacklisted because it's final rendition
  1039. assert.ok(!media.excludeUntil, 'media not blacklisted after playlist 404');
  1040. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1041. assert.equal(this.env.log.warn.args[1],
  1042. 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
  1043. 'log specific error message for final playlist');
  1044. assert.equal(retryplaylist, 1, 'retried final playlist for once');
  1045. assert.equal(blacklistplaylist, 1, 'there is one blacklisted playlist');
  1046. assert.equal(hlsRenditionBlacklistedEvents, 1, 'an hls-rendition-blacklisted event was fired');
  1047. this.clock.tick(2 * 1000);
  1048. // no new request was made since it hasn't been half the segment duration
  1049. assert.strictEqual(3, this.requests.length, 'no new request was made');
  1050. this.clock.tick(3 * 1000);
  1051. // continue loading the final remaining playlist after it wasn't blacklisted
  1052. // when half the segment duaration passed
  1053. assert.strictEqual(4, this.requests.length, 'one more request was made');
  1054. assert.strictEqual(this.requests[3].url,
  1055. absoluteUrl('manifest/media1.m3u8'),
  1056. 'media playlist requested');
  1057. // verify stats
  1058. assert.equal(this.player.tech_.hls.stats.bandwidth, 1e10, 'bandwidth set above');
  1059. });
  1060. QUnit.test('blacklists playlist if it has stopped being updated', function(assert) {
  1061. let playliststuck = 0;
  1062. this.player.src({
  1063. src: 'master.m3u8',
  1064. type: 'application/vnd.apple.mpegurl'
  1065. });
  1066. openMediaSource(this.player, this.clock);
  1067. this.player.tech_.triggerReady();
  1068. this.standardXHRResponse(this.requests.shift());
  1069. this.player.tech_.hls.masterPlaylistController_.seekable = function() {
  1070. return videojs.createTimeRange(90, 130);
  1071. };
  1072. this.player.tech_.setCurrentTime(170);
  1073. this.player.tech_.buffered = function() {
  1074. return videojs.createTimeRange(0, 170);
  1075. };
  1076. Hls.Playlist.playlistEnd = function() {
  1077. return 170;
  1078. };
  1079. this.player.tech_.on('playliststuck', () => playliststuck++);
  1080. this.requests.shift().respond(200, null,
  1081. '#EXTM3U\n' +
  1082. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1083. '#EXTINF:10,\n' +
  1084. '16.ts\n');
  1085. assert.ok(!this.player.tech_.hls.playlists.media().excludeUntil, 'playlist was not blacklisted');
  1086. assert.equal(this.env.log.warn.calls, 0, 'no warning logged for blacklist');
  1087. assert.equal(playliststuck, 0, 'there is no stuck playlist');
  1088. this.player.tech_.trigger('play');
  1089. this.player.tech_.trigger('playing');
  1090. // trigger a refresh
  1091. this.clock.tick(10 * 1000);
  1092. this.requests.shift().respond(200, null,
  1093. '#EXTM3U\n' +
  1094. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1095. '#EXTINF:10,\n' +
  1096. '16.ts\n');
  1097. assert.ok(this.player.tech_.hls.playlists.media().excludeUntil > 0, 'playlist blacklisted for some time');
  1098. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1099. assert.equal(this.env.log.warn.args[0],
  1100. 'Problem encountered with the current HLS playlist. Playlist no longer updating. Switching to another playlist.',
  1101. 'log specific error message for not updated playlist');
  1102. assert.equal(playliststuck, 1, 'there is one stuck playlist');
  1103. });
  1104. QUnit.test('never blacklist the playlist if it is the only playlist', function(assert) {
  1105. let media;
  1106. this.player.src({
  1107. src: 'manifest/media.m3u8',
  1108. type: 'application/vnd.apple.mpegurl'
  1109. });
  1110. openMediaSource(this.player, this.clock);
  1111. this.requests.shift().respond(200, null,
  1112. '#EXTM3U\n' +
  1113. '#EXTINF:10,\n' +
  1114. '0.ts\n');
  1115. this.clock.tick(10 * 1000);
  1116. this.requests.shift().respond(404);
  1117. media = this.player.tech_.hls.playlists.media();
  1118. // media wasn't blacklisted because it's final rendition
  1119. assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
  1120. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1121. assert.equal(this.env.log.warn.args[0],
  1122. 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
  1123. 'log specific error message for final playlist');
  1124. });
  1125. QUnit.test('error on the first playlist request does not trigger an error ' +
  1126. 'when there is master playlist with only one media playlist', function(assert) {
  1127. this.player.src({
  1128. src: 'manifest/master.m3u8',
  1129. type: 'application/vnd.apple.mpegurl'
  1130. });
  1131. openMediaSource(this.player, this.clock);
  1132. this.requests[0]
  1133. .respond(200, null,
  1134. '#EXTM3U\n' +
  1135. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  1136. 'media.m3u8\n');
  1137. this.requests[1].respond(404);
  1138. let url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
  1139. let media = this.player.tech_.hls.playlists.master.playlists[url];
  1140. // media wasn't blacklisted because it's final rendition
  1141. assert.ok(!media.excludeUntil, 'media was not blacklisted after playlist 404');
  1142. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1143. assert.equal(this.env.log.warn.args[0],
  1144. 'Problem encountered with the current HLS playlist. Trying again since it is the final playlist.',
  1145. 'log specific error message for final playlist');
  1146. });
  1147. QUnit.test('seeking in an empty playlist is a non-erroring noop', function(assert) {
  1148. let requestsLength;
  1149. this.player.src({
  1150. src: 'manifest/empty-live.m3u8',
  1151. type: 'application/vnd.apple.mpegurl'
  1152. });
  1153. this.clock.tick(1);
  1154. openMediaSource(this.player, this.clock);
  1155. this.requests.shift().respond(200, null, '#EXTM3U\n');
  1156. requestsLength = this.requests.length;
  1157. this.player.tech_.setCurrentTime(183);
  1158. this.clock.tick(1);
  1159. assert.equal(this.requests.length, requestsLength, 'made no additional requests');
  1160. });
  1161. QUnit.test('fire loadedmetadata once we successfully load a playlist', function(assert) {
  1162. let count = 0;
  1163. this.player.src({
  1164. src: 'manifest/master.m3u8',
  1165. type: 'application/vnd.apple.mpegurl'
  1166. });
  1167. this.clock.tick(1);
  1168. openMediaSource(this.player, this.clock);
  1169. let hls = this.player.tech_.hls;
  1170. hls.bandwidth = 20000;
  1171. hls.masterPlaylistController_.masterPlaylistLoader_.on('loadedmetadata', function() {
  1172. count += 1;
  1173. });
  1174. // master
  1175. this.standardXHRResponse(this.requests.shift());
  1176. assert.equal(count, 0,
  1177. 'loadedMedia not triggered before requesting playlist');
  1178. // media
  1179. this.requests.shift().respond(404);
  1180. assert.equal(count, 0,
  1181. 'loadedMedia not triggered after playlist 404');
  1182. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1183. // media
  1184. this.standardXHRResponse(this.requests.shift());
  1185. assert.equal(count, 1,
  1186. 'loadedMedia triggered after successful recovery from 404');
  1187. // verify stats
  1188. assert.equal(this.player.tech_.hls.stats.bandwidth, 20000, 'bandwidth set above');
  1189. });
  1190. QUnit.test('sets seekable and duration for live playlists', function(assert) {
  1191. this.player.src({
  1192. src: 'http://example.com/manifest/missingEndlist.m3u8',
  1193. type: 'application/vnd.apple.mpegurl'
  1194. });
  1195. this.clock.tick(1);
  1196. openMediaSource(this.player, this.clock);
  1197. this.standardXHRResponse(this.requests[0]);
  1198. assert.equal(this.player.tech_.hls.mediaSource.seekable.length,
  1199. 1,
  1200. 'set one seekable range');
  1201. assert.equal(this.player.tech_.hls.mediaSource.seekable.start(0),
  1202. this.player.tech_.hls.seekable().start(0),
  1203. 'set seekable start');
  1204. assert.equal(this.player.tech_.hls.mediaSource.seekable.end(0),
  1205. this.player.tech_.hls.seekable().end(0),
  1206. 'set seekable end');
  1207. assert.strictEqual(this.player.tech_.hls.mediaSource.duration,
  1208. Infinity,
  1209. 'duration on the mediaSource is infinity');
  1210. });
  1211. QUnit.test('live playlist starts with correct currentTime value', function(assert) {
  1212. this.player.src({
  1213. src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
  1214. type: 'application/vnd.apple.mpegurl'
  1215. });
  1216. this.clock.tick(1);
  1217. openMediaSource(this.player, this.clock);
  1218. this.standardXHRResponse(this.requests[0]);
  1219. this.player.tech_.hls.playlists.trigger('loadedmetadata');
  1220. this.player.tech_.paused = function() {
  1221. return false;
  1222. };
  1223. this.player.tech_.trigger('play');
  1224. this.clock.tick(1);
  1225. let media = this.player.tech_.hls.playlists.media();
  1226. assert.strictEqual(this.player.currentTime(),
  1227. Hls.Playlist.seekable(media).end(0),
  1228. 'currentTime is updated at playback');
  1229. });
  1230. QUnit.test('estimates seekable ranges for live streams that have been paused for a long time', function(assert) {
  1231. this.player.src({
  1232. src: 'http://example.com/manifest/liveStart30sBefore.m3u8',
  1233. type: 'application/vnd.apple.mpegurl'
  1234. });
  1235. this.clock.tick(1);
  1236. openMediaSource(this.player, this.clock);
  1237. this.standardXHRResponse(this.requests.shift());
  1238. this.player.tech_.hls.playlists.media().mediaSequence = 172;
  1239. this.player.tech_.hls.playlists.media().syncInfo = {
  1240. mediaSequence: 130,
  1241. time: 80
  1242. };
  1243. this.player.tech_.hls.masterPlaylistController_.onSyncInfoUpdate_();
  1244. assert.equal(this.player.seekable().start(0),
  1245. 500,
  1246. 'offset the seekable start');
  1247. });
  1248. QUnit.test('resets the time to the live point when resuming a live stream after a ' +
  1249. 'long break', function(assert) {
  1250. let seekTarget;
  1251. this.player.src({
  1252. src: 'live0.m3u8',
  1253. type: 'application/vnd.apple.mpegurl'
  1254. });
  1255. this.clock.tick(1);
  1256. openMediaSource(this.player, this.clock);
  1257. this.requests.shift().respond(200, null,
  1258. '#EXTM3U\n' +
  1259. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1260. '#EXTINF:10,\n' +
  1261. '16.ts\n');
  1262. // mock out the player to simulate a live stream that has been
  1263. // playing for awhile
  1264. this.player.tech_.hls.seekable = function() {
  1265. return videojs.createTimeRange(160, 170);
  1266. };
  1267. this.player.tech_.setCurrentTime = function(time) {
  1268. if (typeof time !== 'undefined') {
  1269. seekTarget = time;
  1270. }
  1271. };
  1272. this.player.tech_.played = function() {
  1273. return videojs.createTimeRange(120, 170);
  1274. };
  1275. this.player.tech_.trigger('playing');
  1276. let seekable = this.player.seekable();
  1277. this.player.tech_.trigger('play');
  1278. assert.equal(seekTarget, seekable.end(seekable.length - 1), 'seeked to live point');
  1279. this.player.tech_.trigger('seeked');
  1280. });
  1281. QUnit.test('reloads out-of-date live playlists when switching variants', function(assert) {
  1282. let oldManifest = testDataManifests['variant-update'];
  1283. this.player.src({
  1284. src: 'http://example.com/master.m3u8',
  1285. type: 'application/vnd.apple.mpegurl'
  1286. });
  1287. this.clock.tick(1);
  1288. openMediaSource(this.player, this.clock);
  1289. this.player.tech_.hls.master = {
  1290. playlists: [{
  1291. mediaSequence: 15,
  1292. segments: [1, 1, 1]
  1293. }, {
  1294. uri: 'http://example.com/variant-update.m3u8',
  1295. mediaSequence: 0,
  1296. segments: [1, 1]
  1297. }]
  1298. };
  1299. // playing segment 15 on playlist zero
  1300. this.player.tech_.hls.media = this.player.tech_.hls.master.playlists[0];
  1301. this.player.mediaIndex = 1;
  1302. testDataManifests['variant-update'] = '#EXTM3U\n' +
  1303. '#EXT-X-MEDIA-SEQUENCE:16\n' +
  1304. '#EXTINF:10,\n' +
  1305. '16.ts\n' +
  1306. '#EXTINF:10,\n' +
  1307. '17.ts\n';
  1308. // switch playlists
  1309. this.player.tech_.hls.selectPlaylist = function() {
  1310. return this.player.tech_.hls.master.playlists[1];
  1311. };
  1312. // timeupdate downloads segment 16 then switches playlists
  1313. this.player.trigger('timeupdate');
  1314. assert.strictEqual(this.player.mediaIndex, 1, 'mediaIndex points at the next segment');
  1315. testDataManifests['variant-update'] = oldManifest;
  1316. });
  1317. QUnit.test('if withCredentials global option is used, withCredentials is set on the XHR object', function(assert) {
  1318. let hlsOptions = videojs.options.hls;
  1319. this.player.dispose();
  1320. videojs.options.hls = {
  1321. withCredentials: true
  1322. };
  1323. this.player = createPlayer();
  1324. this.player.src({
  1325. src: 'http://example.com/media.m3u8',
  1326. type: 'application/vnd.apple.mpegurl'
  1327. });
  1328. this.clock.tick(1);
  1329. openMediaSource(this.player, this.clock);
  1330. assert.ok(this.requests[0].withCredentials,
  1331. 'with credentials should be set to true if that option is passed in');
  1332. videojs.options.hls = hlsOptions;
  1333. });
  1334. QUnit.test('the withCredentials option overrides the global default', function(assert) {
  1335. let hlsOptions = videojs.options.hls;
  1336. this.player.dispose();
  1337. videojs.options.hls = {
  1338. withCredentials: true
  1339. };
  1340. this.player = createPlayer();
  1341. this.player.src({
  1342. src: 'http://example.com/media.m3u8',
  1343. type: 'application/vnd.apple.mpegurl',
  1344. withCredentials: false
  1345. });
  1346. this.clock.tick(1);
  1347. openMediaSource(this.player, this.clock);
  1348. assert.ok(!this.requests[0].withCredentials,
  1349. 'with credentials should be set to false if if overrode global option');
  1350. videojs.options.hls = hlsOptions;
  1351. });
  1352. QUnit.test('if handleManifestRedirects global option is used, it should be passed to PlaylistLoader', function(assert) {
  1353. let hlsOptions = videojs.options.hls;
  1354. this.player.dispose();
  1355. videojs.options.hls = {
  1356. handleManifestRedirects: true
  1357. };
  1358. this.player = createPlayer();
  1359. this.player.src({
  1360. src: 'http://example.com/media.m3u8',
  1361. type: 'application/vnd.apple.mpegurl'
  1362. });
  1363. this.clock.tick(1);
  1364. assert.ok(this.player.tech_.hls.masterPlaylistController_.masterPlaylistLoader_.handleManifestRedirects);
  1365. videojs.options.hls = hlsOptions;
  1366. });
  1367. QUnit.test('the handleManifestRedirects option overrides the global default', function(assert) {
  1368. let hlsOptions = videojs.options.hls;
  1369. this.player.dispose();
  1370. videojs.options.hls = {
  1371. handleManifestRedirects: true
  1372. };
  1373. this.player = createPlayer();
  1374. this.player.src({
  1375. src: 'http://example.com/media.m3u8',
  1376. type: 'application/vnd.apple.mpegurl',
  1377. handleManifestRedirects: false
  1378. });
  1379. this.clock.tick(1);
  1380. assert.notOk(this.player.tech_.hls.masterPlaylistController_.masterPlaylistLoader_.handleManifestRedirects);
  1381. videojs.options.hls = hlsOptions;
  1382. });
  1383. QUnit.test('playlist blacklisting duration is set through options', function(assert) {
  1384. let hlsOptions = videojs.options.hls;
  1385. let url;
  1386. let media;
  1387. this.player.dispose();
  1388. videojs.options.hls = {
  1389. blacklistDuration: 3 * 60
  1390. };
  1391. this.player = createPlayer();
  1392. this.player.src({
  1393. src: 'http://example.com/master.m3u8',
  1394. type: 'application/vnd.apple.mpegurl'
  1395. });
  1396. this.player.tech_.triggerReady();
  1397. openMediaSource(this.player, this.clock);
  1398. this.requests[0].respond(200, null,
  1399. '#EXTM3U\n' +
  1400. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  1401. 'media.m3u8\n' +
  1402. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  1403. 'media1.m3u8\n');
  1404. this.requests[1].respond(404);
  1405. // media
  1406. url = this.requests[1].url.slice(this.requests[1].url.lastIndexOf('/') + 1);
  1407. media = this.player.tech_.hls.playlists.master.playlists[url];
  1408. assert.ok(media.excludeUntil > 0, 'original media blacklisted for some time');
  1409. assert.equal(this.env.log.warn.calls, 1, 'warning logged for blacklist');
  1410. assert.equal(this.env.log.warn.args[0],
  1411. 'Problem encountered with the current HLS playlist. HLS playlist request error at URL: media.m3u8 Switching to another playlist.',
  1412. 'log generic error message');
  1413. this.clock.tick(2 * 60 * 1000);
  1414. assert.ok(media.excludeUntil - Date.now() > 0, 'original media still be blacklisted');
  1415. this.clock.tick(1 * 60 * 1000);
  1416. assert.equal(media.excludeUntil, Date.now(), 'media\'s exclude time reach to the current time');
  1417. assert.equal(this.env.log.warn.calls, 3, 'warning logged for blacklist');
  1418. videojs.options.hls = hlsOptions;
  1419. });
  1420. QUnit.test('if mode global option is used, mode is set to global option', function(assert) {
  1421. let hlsOptions = videojs.options.hls;
  1422. this.player.dispose();
  1423. videojs.options.hls = {
  1424. mode: 'flash'
  1425. };
  1426. this.player = createPlayer();
  1427. this.player.src({
  1428. src: 'http://example.com/media.m3u8',
  1429. type: 'application/vnd.apple.mpegurl'
  1430. });
  1431. this.clock.tick(1);
  1432. openMediaSource(this.player, this.clock);
  1433. assert.equal(this.player.tech_.hls.options_.mode, 'flash', 'mode set to flash');
  1434. videojs.options.hls = hlsOptions;
  1435. });
  1436. QUnit.test('respects bandwidth option of 0', function(assert) {
  1437. this.player.dispose();
  1438. this.player = createPlayer({ html5: { hls: { bandwidth: 0 } } });
  1439. this.player.src({
  1440. src: 'http://example.com/media.m3u8',
  1441. type: 'application/vnd.apple.mpegurl'
  1442. });
  1443. this.clock.tick(1);
  1444. openMediaSource(this.player, this.clock);
  1445. assert.equal(this.player.tech_.hls.bandwidth, 0, 'set bandwidth to 0');
  1446. });
  1447. QUnit.test('uses default bandwidth option if non-numerical value provided', function(assert) {
  1448. this.player.dispose();
  1449. this.player = createPlayer({ html5: { hls: { bandwidth: 'garbage' } } });
  1450. this.player.src({
  1451. src: 'http://example.com/media.m3u8',
  1452. type: 'application/vnd.apple.mpegurl'
  1453. });
  1454. this.clock.tick(1);
  1455. openMediaSource(this.player, this.clock);
  1456. assert.equal(this.player.tech_.hls.bandwidth, 4194304, 'set bandwidth to default');
  1457. });
  1458. QUnit.test('uses default bandwidth if browser is Android', function(assert) {
  1459. this.player.dispose();
  1460. const origIsAndroid = videojs.browser.IS_ANDROID;
  1461. videojs.browser.IS_ANDROID = false;
  1462. this.player = createPlayer();
  1463. this.player.src({
  1464. src: 'http://example.com/media.m3u8',
  1465. type: 'application/vnd.apple.mpegurl'
  1466. });
  1467. openMediaSource(this.player, this.clock);
  1468. assert.equal(this.player.tech_.hls.bandwidth,
  1469. 4194304,
  1470. 'set bandwidth to desktop default');
  1471. this.player.dispose();
  1472. videojs.browser.IS_ANDROID = true;
  1473. this.player = createPlayer();
  1474. this.player.src({
  1475. src: 'http://example.com/media.m3u8',
  1476. type: 'application/vnd.apple.mpegurl'
  1477. });
  1478. openMediaSource(this.player, this.clock);
  1479. assert.equal(this.player.tech_.hls.bandwidth,
  1480. 4194304,
  1481. 'set bandwidth to mobile default');
  1482. videojs.browser.IS_ANDROID = origIsAndroid;
  1483. });
  1484. QUnit.test('does not break if the playlist has no segments', function(assert) {
  1485. this.player.src({
  1486. src: 'manifest/master.m3u8',
  1487. type: 'application/vnd.apple.mpegurl'
  1488. });
  1489. this.clock.tick(1);
  1490. try {
  1491. openMediaSource(this.player, this.clock);
  1492. this.requests[0].respond(200, null,
  1493. '#EXTM3U\n' +
  1494. '#EXT-X-PLAYLIST-TYPE:VOD\n' +
  1495. '#EXT-X-TARGETDURATION:10\n');
  1496. } catch (e) {
  1497. assert.ok(false, 'an error was thrown');
  1498. throw e;
  1499. }
  1500. assert.ok(true, 'no error was thrown');
  1501. assert.strictEqual(
  1502. this.requests.length,
  1503. 1,
  1504. 'no this.requestsfor non-existent segments were queued'
  1505. );
  1506. });
  1507. QUnit.test('can seek before the source buffer opens', function(assert) {
  1508. this.player.src({
  1509. src: 'media.m3u8',
  1510. type: 'application/vnd.apple.mpegurl'
  1511. });
  1512. this.clock.tick(1);
  1513. this.player.tech_.triggerReady();
  1514. this.clock.tick(1);
  1515. this.standardXHRResponse(this.requests.shift());
  1516. this.player.triggerReady();
  1517. this.player.currentTime(1);
  1518. assert.equal(this.player.currentTime(), 1, 'seeked');
  1519. });
  1520. QUnit.test('resets the switching algorithm if a request times out', function(assert) {
  1521. this.player.src({
  1522. src: 'master.m3u8',
  1523. type: 'application/vnd.apple.mpegurl'
  1524. });
  1525. this.clock.tick(1);
  1526. openMediaSource(this.player, this.clock);
  1527. this.player.tech_.hls.bandwidth = 1e20;
  1528. // master
  1529. this.standardXHRResponse(this.requests.shift());
  1530. // media.m3u8
  1531. this.standardXHRResponse(this.requests.shift());
  1532. // simulate a segment timeout
  1533. this.requests[0].timedout = true;
  1534. // segment
  1535. this.requests.shift().abort();
  1536. this.standardXHRResponse(this.requests.shift());
  1537. assert.strictEqual(this.player.tech_.hls.playlists.media(),
  1538. this.player.tech_.hls.playlists.master.playlists[1],
  1539. 'reset to the lowest bitrate playlist');
  1540. // verify stats
  1541. assert.equal(this.player.tech_.hls.stats.bandwidth, 1, 'bandwidth is reset too');
  1542. });
  1543. QUnit.test('disposes the playlist loader', function(assert) {
  1544. let disposes = 0;
  1545. let player;
  1546. let loaderDispose;
  1547. player = createPlayer();
  1548. player.src({
  1549. src: 'manifest/master.m3u8',
  1550. type: 'application/vnd.apple.mpegurl'
  1551. });
  1552. this.clock.tick(1);
  1553. openMediaSource(player, this.clock);
  1554. loaderDispose = player.tech_.hls.playlists.dispose;
  1555. player.tech_.hls.playlists.dispose = function() {
  1556. disposes++;
  1557. loaderDispose.call(player.tech_.hls.playlists);
  1558. };
  1559. player.dispose();
  1560. assert.strictEqual(disposes, 1, 'disposed playlist loader');
  1561. });
  1562. QUnit.test('remove event handlers on dispose', function(assert) {
  1563. let player;
  1564. let unscoped = 0;
  1565. player = createPlayer();
  1566. const origPlayerOn = player.on.bind(player);
  1567. const origPlayerOff = player.off.bind(player);
  1568. player.on = function(...args) {
  1569. if (typeof args[0] !== 'object') {
  1570. unscoped++;
  1571. }
  1572. origPlayerOn(...args);
  1573. };
  1574. player.off = function(...args) {
  1575. if (typeof args[0] !== 'object') {
  1576. unscoped--;
  1577. }
  1578. origPlayerOff(...args);
  1579. };
  1580. player.src({
  1581. src: 'manifest/master.m3u8',
  1582. type: 'application/vnd.apple.mpegurl'
  1583. });
  1584. this.clock.tick(1);
  1585. openMediaSource(player, this.clock);
  1586. this.standardXHRResponse(this.requests[0]);
  1587. this.standardXHRResponse(this.requests[1]);
  1588. assert.ok(unscoped > 0, 'has unscoped handlers');
  1589. player.dispose();
  1590. assert.ok(unscoped <= 0, 'no unscoped handlers');
  1591. });
  1592. QUnit.test('the source handler supports HLS mime types', function(assert) {
  1593. const techs = ['html5', 'flash'];
  1594. techs.forEach(function(techName) {
  1595. assert.ok(HlsSourceHandler(techName).canHandleSource({
  1596. type: 'aPplicatiOn/x-MPegUrl'
  1597. }), 'supports x-mpegurl');
  1598. assert.ok(HlsSourceHandler(techName).canHandleSource({
  1599. type: 'aPplicatiOn/VnD.aPPle.MpEgUrL'
  1600. }), 'supports vnd.apple.mpegurl');
  1601. assert.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/VnD.aPPle.MpEgUrL'),
  1602. 'supports vnd.apple.mpegurl');
  1603. assert.ok(HlsSourceHandler(techName).canPlayType('aPplicatiOn/x-MPegUrl'),
  1604. 'supports x-mpegurl');
  1605. assert.ok(!(HlsSourceHandler(techName).canHandleSource({
  1606. type: 'video/mp4'
  1607. }) instanceof HlsHandler), 'does not support mp4');
  1608. assert.ok(!(HlsSourceHandler(techName).canHandleSource({
  1609. type: 'video/x-flv'
  1610. }) instanceof HlsHandler), 'does not support flv');
  1611. assert.ok(!(HlsSourceHandler(techName).canPlayType('video/mp4')),
  1612. 'does not support mp4');
  1613. assert.ok(!(HlsSourceHandler(techName).canPlayType('video/x-flv')),
  1614. 'does not support flv');
  1615. });
  1616. });
  1617. QUnit.test('source handler does not support sources when IE 10 or below', function(assert) {
  1618. videojs.browser.IE_VERSION = 10;
  1619. ['html5', 'flash'].forEach(function(techName) {
  1620. assert.ok(!HlsSourceHandler(techName).canHandleSource({
  1621. type: 'application/x-mpegURL'
  1622. }), 'does not support when browser is IE10');
  1623. });
  1624. });
  1625. QUnit.test('fires loadstart manually if Flash is used', function(assert) {
  1626. videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
  1627. let tech = new (videojs.getTech('Flash'))({});
  1628. let loadstarts = 0;
  1629. tech.on('loadstart', function() {
  1630. loadstarts++;
  1631. });
  1632. HlsSourceHandler('flash').handleSource({
  1633. src: 'movie.m3u8',
  1634. type: 'application/x-mpegURL'
  1635. }, tech);
  1636. assert.equal(loadstarts, 0, 'loadstart is not synchronous');
  1637. this.clock.tick(1);
  1638. assert.equal(loadstarts, 1, 'fired loadstart');
  1639. tech.dispose();
  1640. videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
  1641. });
  1642. QUnit.test('has no effect if native HLS is available', function(assert) {
  1643. const Html5 = videojs.getTech('Html5');
  1644. const oldHtml5CanPlaySource = Html5.canPlaySource;
  1645. let player;
  1646. Html5.canPlaySource = () => true;
  1647. Hls.supportsNativeHls = true;
  1648. player = createPlayer();
  1649. player.src({
  1650. src: 'http://example.com/manifest/master.m3u8',
  1651. type: 'application/x-mpegURL'
  1652. });
  1653. this.clock.tick(1);
  1654. assert.ok(!player.tech_.hls, 'did not load hls tech');
  1655. player.dispose();
  1656. Html5.canPlaySource = oldHtml5CanPlaySource;
  1657. });
  1658. QUnit.test('loads if native HLS is available and override is set locally', function(assert) {
  1659. let player;
  1660. Hls.supportsNativeHls = true;
  1661. player = createPlayer({html5: {hls: {overrideNative: true}}});
  1662. this.clock.tick(1);
  1663. player.tech_.featuresNativeVideoTracks = true;
  1664. assert.throws(function() {
  1665. player.src({
  1666. src: 'http://example.com/manifest/master.m3u8',
  1667. type: 'application/x-mpegURL'
  1668. });
  1669. this.clock.tick(1);
  1670. }, 'errors if native tracks are enabled');
  1671. player.dispose();
  1672. player = createPlayer({html5: {hls: {overrideNative: true}}});
  1673. this.clock.tick(1);
  1674. player.tech_.featuresNativeVideoTracks = false;
  1675. player.tech_.featuresNativeAudioTracks = false;
  1676. player.src({
  1677. src: 'http://example.com/manifest/master.m3u8',
  1678. type: 'application/x-mpegURL'
  1679. });
  1680. this.clock.tick(1);
  1681. assert.ok(player.tech_.hls, 'did load hls tech');
  1682. player.dispose();
  1683. });
  1684. QUnit.test('loads if native HLS is available and override is set globally', function(assert) {
  1685. videojs.options.hls.overrideNative = true;
  1686. let player;
  1687. Hls.supportsNativeHls = true;
  1688. player = createPlayer();
  1689. player.tech_.featuresNativeVideoTracks = true;
  1690. assert.throws(function() {
  1691. player.src({
  1692. src: 'http://example.com/manifest/master.m3u8',
  1693. type: 'application/x-mpegURL'
  1694. });
  1695. this.clock.tick(1);
  1696. }, 'errors if native tracks are enabled');
  1697. player.dispose();
  1698. player = createPlayer();
  1699. player.tech_.featuresNativeVideoTracks = false;
  1700. player.tech_.featuresNativeAudioTracks = false;
  1701. player.src({
  1702. src: 'http://example.com/manifest/master.m3u8',
  1703. type: 'application/x-mpegURL'
  1704. });
  1705. this.clock.tick(1);
  1706. assert.ok(player.tech_.hls, 'did load hls tech');
  1707. player.dispose();
  1708. });
  1709. QUnit.test('re-emits mediachange events', function(assert) {
  1710. let mediaChanges = 0;
  1711. this.player.on('mediachange', function() {
  1712. mediaChanges++;
  1713. });
  1714. this.player.src({
  1715. src: 'http://example.com/media.m3u8',
  1716. type: 'application/vnd.apple.mpegurl'
  1717. });
  1718. this.clock.tick(1);
  1719. openMediaSource(this.player, this.clock);
  1720. this.standardXHRResponse(this.requests.shift());
  1721. this.player.tech_.hls.playlists.trigger('mediachange');
  1722. assert.strictEqual(mediaChanges, 1, 'fired mediachange');
  1723. });
  1724. QUnit.test('can be disposed before finishing initialization', function(assert) {
  1725. let readyHandlers = [];
  1726. this.player.ready = function(callback) {
  1727. readyHandlers.push(callback);
  1728. };
  1729. this.player.src({
  1730. src: 'http://example.com/media.m3u8',
  1731. type: 'application/vnd.apple.mpegurl'
  1732. });
  1733. this.clock.tick(1);
  1734. readyHandlers.shift().call(this.player);
  1735. this.player.src({
  1736. src: 'http://example.com/media.mp4',
  1737. type: 'video/mp4'
  1738. });
  1739. assert.ok(readyHandlers.length > 0, 'registered a ready handler');
  1740. try {
  1741. while (readyHandlers.length) {
  1742. readyHandlers.shift().call(this.player);
  1743. openMediaSource(this.player, this.clock);
  1744. }
  1745. assert.ok(true, 'did not throw an exception');
  1746. } catch (e) {
  1747. assert.ok(false, 'threw an exception');
  1748. }
  1749. });
  1750. QUnit.test('calling play() at the end of a video replays', function(assert) {
  1751. let seekTime = -1;
  1752. this.player.src({
  1753. src: 'http://example.com/media.m3u8',
  1754. type: 'application/vnd.apple.mpegurl'
  1755. });
  1756. this.clock.tick(1);
  1757. openMediaSource(this.player, this.clock);
  1758. this.player.tech_.setCurrentTime = function(time) {
  1759. if (typeof time !== 'undefined') {
  1760. seekTime = time;
  1761. }
  1762. return 0;
  1763. };
  1764. this.requests.shift().respond(200, null,
  1765. '#EXTM3U\n' +
  1766. '#EXTINF:10,\n' +
  1767. '0.ts\n' +
  1768. '#EXT-X-ENDLIST\n');
  1769. this.clock.tick(1);
  1770. this.standardXHRResponse(this.requests.shift());
  1771. this.player.tech_.ended = function() {
  1772. return true;
  1773. };
  1774. this.player.tech_.trigger('play');
  1775. this.clock.tick(1);
  1776. assert.equal(seekTime, 0, 'seeked to the beginning');
  1777. // verify stats
  1778. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  1779. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  1780. });
  1781. QUnit.test('keys are resolved relative to the master playlist', function(assert) {
  1782. this.player.src({
  1783. src: 'video/master-encrypted.m3u8',
  1784. type: 'application/vnd.apple.mpegurl'
  1785. });
  1786. this.clock.tick(1);
  1787. openMediaSource(this.player, this.clock);
  1788. this.requests.shift().respond(200, null,
  1789. '#EXTM3U\n' +
  1790. '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' +
  1791. 'playlist/playlist.m3u8\n' +
  1792. '#EXT-X-ENDLIST\n');
  1793. this.clock.tick(1);
  1794. this.requests.shift().respond(200, null,
  1795. '#EXTM3U\n' +
  1796. '#EXT-X-TARGETDURATION:15\n' +
  1797. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1798. '#EXTINF:2.833,\n' +
  1799. 'http://media.example.com/fileSequence1.ts\n' +
  1800. '#EXT-X-ENDLIST\n');
  1801. this.clock.tick(1);
  1802. assert.equal(this.requests.length, 2, 'requested the key');
  1803. assert.equal(this.requests[0].url,
  1804. absoluteUrl('video/playlist/keys/key.php'),
  1805. 'resolves multiple relative paths');
  1806. // verify stats
  1807. assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
  1808. });
  1809. QUnit.test('keys are resolved relative to their containing playlist', function(assert) {
  1810. this.player.src({
  1811. src: 'video/media-encrypted.m3u8',
  1812. type: 'application/vnd.apple.mpegurl'
  1813. });
  1814. this.clock.tick(1);
  1815. openMediaSource(this.player, this.clock);
  1816. this.requests.shift().respond(200, null,
  1817. '#EXTM3U\n' +
  1818. '#EXT-X-TARGETDURATION:15\n' +
  1819. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1820. '#EXTINF:2.833,\n' +
  1821. 'http://media.example.com/fileSequence1.ts\n' +
  1822. '#EXT-X-ENDLIST\n');
  1823. this.clock.tick(1);
  1824. assert.equal(this.requests.length, 2, 'requested a key');
  1825. assert.equal(this.requests[0].url,
  1826. absoluteUrl('video/keys/key.php'),
  1827. 'resolves multiple relative paths');
  1828. });
  1829. QUnit.test('seeking should abort an outstanding key request and create a new one', function(assert) {
  1830. this.player.src({
  1831. src: 'https://example.com/encrypted.m3u8',
  1832. type: 'application/vnd.apple.mpegurl'
  1833. });
  1834. this.clock.tick(1);
  1835. openMediaSource(this.player, this.clock);
  1836. this.requests.shift().respond(200, null,
  1837. '#EXTM3U\n' +
  1838. '#EXT-X-TARGETDURATION:15\n' +
  1839. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' +
  1840. '#EXTINF:9,\n' +
  1841. 'http://media.example.com/fileSequence1.ts\n' +
  1842. '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' +
  1843. '#EXTINF:9,\n' +
  1844. 'http://media.example.com/fileSequence2.ts\n' +
  1845. '#EXT-X-ENDLIST\n');
  1846. this.clock.tick(1);
  1847. // segment 1
  1848. this.standardXHRResponse(this.requests.pop());
  1849. this.player.currentTime(11);
  1850. this.clock.tick(2);
  1851. assert.ok(this.requests[0].aborted, 'the key XHR should be aborted');
  1852. // aborted key 1
  1853. this.requests.shift();
  1854. assert.equal(this.requests.length, 2, 'requested the new key');
  1855. assert.equal(this.requests[0].url,
  1856. 'https://example.com/' +
  1857. this.player.tech_.hls.playlists.media().segments[1].key.uri,
  1858. 'urls should match');
  1859. // verify stats
  1860. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  1861. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  1862. });
  1863. QUnit.test('switching playlists with an outstanding key request aborts request and ' +
  1864. 'loads segment', function(assert) {
  1865. let keyXhr;
  1866. let media = '#EXTM3U\n' +
  1867. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  1868. '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
  1869. '#EXTINF:2.833,\n' +
  1870. 'http://media.example.com/fileSequence52-A.ts\n' +
  1871. '#EXTINF:15.0,\n' +
  1872. 'http://media.example.com/fileSequence52-B.ts\n' +
  1873. '#EXT-X-ENDLIST\n';
  1874. this.player.src({
  1875. src: 'https://example.com/master.m3u8',
  1876. type: 'application/vnd.apple.mpegurl'
  1877. });
  1878. this.clock.tick(1);
  1879. openMediaSource(this.player, this.clock);
  1880. this.player.tech_.trigger('play');
  1881. this.clock.tick(1);
  1882. // master playlist
  1883. this.standardXHRResponse(this.requests.shift());
  1884. // media playlist
  1885. this.requests.shift().respond(200, null, media);
  1886. this.clock.tick(1);
  1887. // first segment of the original media playlist
  1888. this.standardXHRResponse(this.requests.pop());
  1889. assert.equal(this.requests.length, 1, 'key request only one outstanding');
  1890. keyXhr = this.requests.shift();
  1891. assert.ok(!keyXhr.aborted, 'key request outstanding');
  1892. this.player.tech_.hls.playlists.trigger('mediachanging');
  1893. this.player.tech_.hls.playlists.trigger('mediachange');
  1894. this.clock.tick(1);
  1895. assert.ok(keyXhr.aborted, 'key request aborted');
  1896. assert.equal(this.requests.length, 2, 'loaded key and segment');
  1897. assert.equal(this.requests[0].url,
  1898. 'https://priv.example.com/key.php?r=52',
  1899. 'requested the key');
  1900. assert.equal(this.requests[1].url,
  1901. 'http://media.example.com/fileSequence52-A.ts',
  1902. 'requested the segment');
  1903. // verify stats
  1904. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  1905. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, '1 request');
  1906. });
  1907. QUnit.test('does not download segments if preload option set to none', function(assert) {
  1908. this.player.preload('none');
  1909. this.player.src({
  1910. src: 'master.m3u8',
  1911. type: 'application/vnd.apple.mpegurl'
  1912. });
  1913. this.clock.tick(1);
  1914. openMediaSource(this.player, this.clock);
  1915. // master
  1916. this.standardXHRResponse(this.requests.shift());
  1917. // media
  1918. this.standardXHRResponse(this.requests.shift());
  1919. this.clock.tick(10 * 1000);
  1920. this.requests = this.requests.filter(function(request) {
  1921. return !(/m3u8$/).test(request.uri);
  1922. });
  1923. assert.equal(this.requests.length, 0, 'did not download any segments');
  1924. // verify stats
  1925. assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
  1926. });
  1927. // workaround https://bugzilla.mozilla.org/show_bug.cgi?id=548397
  1928. QUnit.test('selectPlaylist does not fail if getComputedStyle returns null', function(assert) {
  1929. let oldGetComputedStyle = window.getComputedStyle;
  1930. window.getComputedStyle = function() {
  1931. return null;
  1932. };
  1933. this.player.src({
  1934. src: 'master.m3u8',
  1935. type: 'application/vnd.apple.mpegurl'
  1936. });
  1937. this.clock.tick(1);
  1938. openMediaSource(this.player, this.clock);
  1939. // master
  1940. this.standardXHRResponse(this.requests.shift());
  1941. // media
  1942. this.standardXHRResponse(this.requests.shift());
  1943. this.player.tech_.hls.selectPlaylist();
  1944. assert.ok(true, 'should not throw');
  1945. window.getComputedStyle = oldGetComputedStyle;
  1946. // verify stats
  1947. assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
  1948. });
  1949. QUnit.test('resolves relative key URLs against the playlist', function(assert) {
  1950. this.player.src({
  1951. src: 'https://example.com/media.m3u8',
  1952. type: 'application/vnd.apple.mpegurl'
  1953. });
  1954. this.clock.tick(1);
  1955. openMediaSource(this.player, this.clock);
  1956. this.requests.shift()
  1957. .respond(200, null,
  1958. '#EXTM3U\n' +
  1959. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  1960. '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' +
  1961. '#EXTINF:2.833,\n' +
  1962. 'http://media.example.com/fileSequence52-A.ts\n' +
  1963. '#EXT-X-ENDLIST\n');
  1964. this.clock.tick(1);
  1965. assert.equal(this.requests[0].url,
  1966. 'https://example.com/key.php?r=52',
  1967. 'resolves the key URL');
  1968. });
  1969. QUnit.test('adds 1 default audio track if we have not parsed any and the playlist is loaded', function(assert) {
  1970. this.player.src({
  1971. src: 'manifest/master.m3u8',
  1972. type: 'application/vnd.apple.mpegurl'
  1973. });
  1974. this.clock.tick(1);
  1975. assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
  1976. openMediaSource(this.player, this.clock);
  1977. // master
  1978. this.standardXHRResponse(this.requests.shift());
  1979. // media
  1980. this.standardXHRResponse(this.requests.shift());
  1981. assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
  1982. assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
  1983. });
  1984. QUnit.test('adds 1 default audio track if in flash mode', function(assert) {
  1985. let hlsOptions = videojs.options.hls;
  1986. this.player.dispose();
  1987. videojs.options.hls = {
  1988. mode: 'flash'
  1989. };
  1990. this.player = createPlayer();
  1991. this.player.src({
  1992. src: 'manifest/multipleAudioGroups.m3u8',
  1993. type: 'application/vnd.apple.mpegurl'
  1994. });
  1995. this.clock.tick(1);
  1996. assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
  1997. openMediaSource(this.player, this.clock);
  1998. // master
  1999. this.standardXHRResponse(this.requests.shift());
  2000. // media
  2001. this.standardXHRResponse(this.requests.shift());
  2002. assert.equal(this.player.audioTracks().length, 1, 'one audio track after load');
  2003. assert.equal(this.player.audioTracks()[0].label, 'default', 'set the label');
  2004. videojs.options.hls = hlsOptions;
  2005. });
  2006. QUnit.test('adds audio tracks if we have parsed some from a playlist', function(assert) {
  2007. this.player.src({
  2008. src: 'manifest/multipleAudioGroups.m3u8',
  2009. type: 'application/vnd.apple.mpegurl'
  2010. });
  2011. this.clock.tick(1);
  2012. assert.equal(this.player.audioTracks().length, 0, 'zero audio tracks at load time');
  2013. openMediaSource(this.player, this.clock);
  2014. // master
  2015. this.standardXHRResponse(this.requests.shift());
  2016. // media
  2017. this.standardXHRResponse(this.requests.shift());
  2018. let vjsAudioTracks = this.player.audioTracks();
  2019. assert.equal(vjsAudioTracks.length, 3, '3 active vjs tracks');
  2020. assert.equal(vjsAudioTracks[0].enabled, true, 'default track is enabled');
  2021. vjsAudioTracks[1].enabled = true;
  2022. assert.equal(vjsAudioTracks[1].enabled, true, 'new track is enabled on vjs');
  2023. assert.equal(vjsAudioTracks[0].enabled, false, 'main track is disabled');
  2024. });
  2025. QUnit.test('cleans up the buffer when loading live segments', function(assert) {
  2026. let removes = [];
  2027. let seekable = videojs.createTimeRanges([[0, 70]]);
  2028. this.player.src({
  2029. src: 'liveStart30sBefore.m3u8',
  2030. type: 'application/vnd.apple.mpegurl'
  2031. });
  2032. this.clock.tick(1);
  2033. openMediaSource(this.player, this.clock);
  2034. this.player.tech_.hls.masterPlaylistController_.seekable = function() {
  2035. return seekable;
  2036. };
  2037. // This is so we do not track first call to remove during segment loader init
  2038. this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
  2039. this.resetLoader();
  2040. };
  2041. this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
  2042. let buffer = new (videojs.extend(videojs.EventTarget, {
  2043. constructor() {},
  2044. abort() {},
  2045. buffered: videojs.createTimeRange(),
  2046. appendBuffer() {},
  2047. remove(start, end) {
  2048. removes.push([start, end]);
  2049. }
  2050. }))();
  2051. this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
  2052. return buffer;
  2053. };
  2054. this.player.tech_.hls.bandwidth = 20e10;
  2055. this.player.tech_.triggerReady();
  2056. this.standardXHRResponse(this.requests[0]);
  2057. this.player.tech_.hls.playlists.trigger('loadedmetadata');
  2058. this.player.tech_.trigger('canplay');
  2059. this.player.tech_.paused = function() {
  2060. return false;
  2061. };
  2062. this.player.tech_.trigger('play');
  2063. this.clock.tick(1);
  2064. // request first playable segment
  2065. this.standardXHRResponse(this.requests[1]);
  2066. this.clock.tick(1);
  2067. this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
  2068. this.clock.tick(1);
  2069. // request second playable segment
  2070. this.standardXHRResponse(this.requests[2]);
  2071. assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8',
  2072. 'master playlist requested');
  2073. assert.equal(removes.length, 1, 'remove called');
  2074. // segment-loader removes at currentTime - 30
  2075. assert.deepEqual(removes[0], [0, 40],
  2076. 'remove called with the right range');
  2077. // verify stats
  2078. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 2048, '2048 bytes');
  2079. assert.equal(this.player.tech_.hls.stats.mediaRequests, 2, '2 requests');
  2080. });
  2081. QUnit.test('cleans up the buffer based on currentTime when loading a live segment ' +
  2082. 'if seekable start is after currentTime', function(assert) {
  2083. let removes = [];
  2084. let seekable = videojs.createTimeRanges([[0, 80]]);
  2085. this.player.src({
  2086. src: 'liveStart30sBefore.m3u8',
  2087. type: 'application/vnd.apple.mpegurl'
  2088. });
  2089. openMediaSource(this.player, this.clock);
  2090. this.player.tech_.hls.masterPlaylistController_.seekable = function() {
  2091. return seekable;
  2092. };
  2093. // This is so we do not track first call to remove during segment loader init
  2094. this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
  2095. this.resetLoader();
  2096. };
  2097. this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
  2098. let buffer = new (videojs.extend(videojs.EventTarget, {
  2099. constructor() {},
  2100. abort() {},
  2101. buffered: videojs.createTimeRange(),
  2102. appendBuffer() {},
  2103. remove(start, end) {
  2104. removes.push([start, end]);
  2105. }
  2106. }))();
  2107. this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
  2108. return buffer;
  2109. };
  2110. this.player.tech_.hls.bandwidth = 20e10;
  2111. this.player.tech_.triggerReady();
  2112. this.standardXHRResponse(this.requests[0]);
  2113. this.player.tech_.hls.playlists.trigger('loadedmetadata');
  2114. this.player.tech_.trigger('canplay');
  2115. this.player.tech_.paused = function() {
  2116. return false;
  2117. };
  2118. this.player.tech_.trigger('play');
  2119. this.clock.tick(1);
  2120. // request first playable segment
  2121. this.standardXHRResponse(this.requests[1]);
  2122. this.clock.tick(1);
  2123. this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
  2124. // Change seekable so that it starts *after* the currentTime which was set
  2125. // based on the previous seekable range (the end of 80)
  2126. seekable = videojs.createTimeRanges([[100, 120]]);
  2127. this.clock.tick(1);
  2128. // request second playable segment
  2129. this.standardXHRResponse(this.requests[2]);
  2130. assert.strictEqual(this.requests[0].url, 'liveStart30sBefore.m3u8', 'master playlist requested');
  2131. assert.equal(removes.length, 1, 'remove called');
  2132. assert.deepEqual(removes[0], [0, 80 - 30], 'remove called with the right range');
  2133. });
  2134. QUnit.test('cleans up the buffer when loading VOD segments', function(assert) {
  2135. let removes = [];
  2136. this.player.src({
  2137. src: 'manifest/master.m3u8',
  2138. type: 'application/vnd.apple.mpegurl'
  2139. });
  2140. this.clock.tick(1);
  2141. openMediaSource(this.player, this.clock);
  2142. // This is so we do not track first call to remove during segment loader init
  2143. this.player.tech_.hls.masterPlaylistController_.mainSegmentLoader_.resetEverything = function() {
  2144. this.resetLoader();
  2145. };
  2146. this.player.tech_.hls.mediaSource.addSourceBuffer = () => {
  2147. let buffer = new (videojs.extend(videojs.EventTarget, {
  2148. constructor() {},
  2149. abort() {},
  2150. buffered: videojs.createTimeRange(),
  2151. appendBuffer() {},
  2152. remove(start, end) {
  2153. removes.push([start, end]);
  2154. }
  2155. }))();
  2156. this.player.tech_.hls.mediaSource.sourceBuffers = [buffer];
  2157. return buffer;
  2158. };
  2159. this.player.width(640);
  2160. this.player.height(360);
  2161. this.player.tech_.hls.bandwidth = 20e10;
  2162. this.standardXHRResponse(this.requests[0]);
  2163. this.standardXHRResponse(this.requests[1]);
  2164. this.standardXHRResponse(this.requests[2]);
  2165. this.clock.tick(1);
  2166. this.player.currentTime(120);
  2167. this.player.tech_.hls.mediaSource.sourceBuffers[0].trigger('updateend');
  2168. // This requires 2 clock ticks because after updateend monitorBuffer_ is called
  2169. // to setup fillBuffer on the next tick, but the seek also causes monitorBuffer_ to be
  2170. // called, which cancels the previously set timeout and sets a new one for the following
  2171. // tick.
  2172. this.clock.tick(2);
  2173. this.standardXHRResponse(this.requests[3]);
  2174. assert.strictEqual(this.requests[0].url, 'manifest/master.m3u8',
  2175. 'master playlist requested');
  2176. assert.strictEqual(this.requests[1].url, absoluteUrl('manifest/media3.m3u8'),
  2177. 'media playlist requested');
  2178. assert.equal(removes.length, 1, 'remove called');
  2179. assert.deepEqual(removes[0], [0, 120 - 30], 'remove called with the right range');
  2180. });
  2181. QUnit.test('when mediaGroup changes enabled track should not change', function(assert) {
  2182. let hlsAudioChangeEvents = 0;
  2183. this.player.src({
  2184. src: 'manifest/multipleAudioGroups.m3u8',
  2185. type: 'application/vnd.apple.mpegurl'
  2186. });
  2187. this.clock.tick(1);
  2188. openMediaSource(this.player, this.clock);
  2189. this.player.tech_.on('usage', (event) => {
  2190. if (event.name === 'hls-audio-change') {
  2191. hlsAudioChangeEvents++;
  2192. }
  2193. });
  2194. // master
  2195. this.standardXHRResponse(this.requests.shift());
  2196. // video media
  2197. this.standardXHRResponse(this.requests.shift());
  2198. let hls = this.player.tech_.hls;
  2199. let mpc = hls.masterPlaylistController_;
  2200. let audioTracks = this.player.audioTracks();
  2201. assert.equal(hlsAudioChangeEvents, 0, 'no hls-audio-change event was fired');
  2202. assert.equal(audioTracks.length, 3, 'three audio tracks after load');
  2203. assert.equal(audioTracks[0].enabled, true, 'track one enabled after load');
  2204. let oldMediaGroup = hls.playlists.media().attributes.AUDIO;
  2205. // clear out any outstanding requests
  2206. this.requests.length = 0;
  2207. // force mpc to select a playlist from a new media group
  2208. mpc.masterPlaylistLoader_.media(mpc.master().playlists[0]);
  2209. this.clock.tick(1);
  2210. // video media
  2211. this.standardXHRResponse(this.requests.shift());
  2212. assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
  2213. audioTracks = this.player.audioTracks();
  2214. let activeGroup = mpc.mediaTypes_.AUDIO.activeGroup(audioTracks[0]);
  2215. assert.equal(audioTracks.length, 3, 'three audio tracks after changing mediaGroup');
  2216. assert.ok(activeGroup.default, 'track one should be the default');
  2217. assert.ok(audioTracks[0].enabled, 'enabled the default track');
  2218. assert.notOk(audioTracks[1].enabled, 'disabled track two');
  2219. assert.notOk(audioTracks[2].enabled, 'disabled track three');
  2220. audioTracks[1].enabled = true;
  2221. assert.notOk(audioTracks[0].enabled, 'disabled track one');
  2222. assert.ok(audioTracks[1].enabled, 'enabled track two');
  2223. assert.notOk(audioTracks[2].enabled, 'disabled track three');
  2224. oldMediaGroup = hls.playlists.media().attributes.AUDIO;
  2225. // clear out any outstanding requests
  2226. this.requests.length = 0;
  2227. // swap back to the old media group
  2228. // this playlist is already loaded so no new requests are made
  2229. mpc.masterPlaylistLoader_.media(mpc.master().playlists[3]);
  2230. this.clock.tick(1);
  2231. assert.notEqual(oldMediaGroup, hls.playlists.media().attributes.AUDIO, 'selected a new playlist');
  2232. audioTracks = this.player.audioTracks();
  2233. assert.equal(hlsAudioChangeEvents, 1, 'an hls-audio-change event was fired');
  2234. assert.equal(audioTracks.length, 3, 'three audio tracks after reverting mediaGroup');
  2235. assert.notOk(audioTracks[0].enabled, 'the default track is still disabled');
  2236. assert.ok(audioTracks[1].enabled, 'track two is still enabled');
  2237. assert.notOk(audioTracks[2].enabled, 'track three is still disabled');
  2238. });
  2239. QUnit.test('Allows specifying the beforeRequest function on the player', function(assert) {
  2240. let beforeRequestCalled = false;
  2241. this.player.src({
  2242. src: 'master.m3u8',
  2243. type: 'application/vnd.apple.mpegurl'
  2244. });
  2245. this.clock.tick(1);
  2246. openMediaSource(this.player, this.clock);
  2247. this.player.tech_.hls.xhr.beforeRequest = function() {
  2248. beforeRequestCalled = true;
  2249. };
  2250. // master
  2251. this.standardXHRResponse(this.requests.shift());
  2252. // media
  2253. this.standardXHRResponse(this.requests.shift());
  2254. assert.ok(beforeRequestCalled, 'beforeRequest was called');
  2255. // verify stats
  2256. assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
  2257. });
  2258. QUnit.test('Allows specifying the beforeRequest function globally', function(assert) {
  2259. let beforeRequestCalled = false;
  2260. videojs.Hls.xhr.beforeRequest = function() {
  2261. beforeRequestCalled = true;
  2262. };
  2263. this.player.src({
  2264. src: 'master.m3u8',
  2265. type: 'application/vnd.apple.mpegurl'
  2266. });
  2267. this.clock.tick(1);
  2268. openMediaSource(this.player, this.clock);
  2269. // master
  2270. this.standardXHRResponse(this.requests.shift());
  2271. assert.ok(beforeRequestCalled, 'beforeRequest was called');
  2272. delete videojs.Hls.xhr.beforeRequest;
  2273. // verify stats
  2274. assert.equal(this.player.tech_.hls.stats.bandwidth, 4194304, 'default');
  2275. });
  2276. QUnit.test('Allows overriding the global beforeRequest function', function(assert) {
  2277. let beforeGlobalRequestCalled = 0;
  2278. let beforeLocalRequestCalled = 0;
  2279. videojs.Hls.xhr.beforeRequest = function() {
  2280. beforeGlobalRequestCalled++;
  2281. };
  2282. this.player.src({
  2283. src: 'master.m3u8',
  2284. type: 'application/vnd.apple.mpegurl'
  2285. });
  2286. this.clock.tick(1);
  2287. openMediaSource(this.player, this.clock);
  2288. this.player.tech_.hls.xhr.beforeRequest = function() {
  2289. beforeLocalRequestCalled++;
  2290. };
  2291. // master
  2292. this.standardXHRResponse(this.requests.shift());
  2293. // media
  2294. this.standardXHRResponse(this.requests.shift());
  2295. // ts
  2296. this.standardXHRResponse(this.requests.shift());
  2297. assert.equal(beforeLocalRequestCalled, 2, 'local beforeRequest was called twice ' +
  2298. 'for the media playlist and media');
  2299. assert.equal(beforeGlobalRequestCalled, 1, 'global beforeRequest was called once ' +
  2300. 'for the master playlist');
  2301. delete videojs.Hls.xhr.beforeRequest;
  2302. // verify stats
  2303. assert.equal(this.player.tech_.hls.stats.mediaBytesTransferred, 1024, 'seen above');
  2304. assert.equal(this.player.tech_.hls.stats.mediaRequests, 1, 'one segment request');
  2305. });
  2306. QUnit.test('passes useCueTags hls option to master playlist controller', function(assert) {
  2307. this.player.src({
  2308. src: 'master.m3u8',
  2309. type: 'application/vnd.apple.mpegurl'
  2310. });
  2311. this.clock.tick(1);
  2312. assert.ok(!this.player.tech_.hls.masterPlaylistController_.useCueTags_,
  2313. 'useCueTags is falsy by default');
  2314. let origHlsOptions = videojs.options.hls;
  2315. videojs.options.hls = {
  2316. useCueTags: true
  2317. };
  2318. this.player.dispose();
  2319. this.player = createPlayer();
  2320. this.player.src({
  2321. src: 'http://example.com/media.m3u8',
  2322. type: 'application/vnd.apple.mpegurl'
  2323. });
  2324. this.clock.tick(1);
  2325. assert.ok(this.player.tech_.hls.masterPlaylistController_.useCueTags_,
  2326. 'useCueTags passed to master playlist controller');
  2327. videojs.options.hls = origHlsOptions;
  2328. });
  2329. QUnit.skip('populates quality levels list when available', function(assert) {
  2330. this.player.src({
  2331. src: 'manifest/master.m3u8',
  2332. type: 'application/vnd.apple.mpegurl'
  2333. });
  2334. this.clock.tick(1);
  2335. openMediaSource(this.player, this.clock);
  2336. assert.ok(this.player.tech_.hls.qualityLevels_, 'added quality levels');
  2337. let qualityLevels = this.player.qualityLevels();
  2338. let addCount = 0;
  2339. let changeCount = 0;
  2340. qualityLevels.on('addqualitylevel', () => {
  2341. addCount++;
  2342. });
  2343. qualityLevels.on('change', () => {
  2344. changeCount++;
  2345. });
  2346. // master
  2347. this.standardXHRResponse(this.requests.shift());
  2348. // media
  2349. this.standardXHRResponse(this.requests.shift());
  2350. assert.equal(addCount, 4, 'four levels added from master');
  2351. assert.equal(changeCount, 1, 'selected initial quality level');
  2352. this.player.dispose();
  2353. this.player = createPlayer({}, {
  2354. src: 'http://example.com/media.m3u8',
  2355. type: 'application/vnd.apple.mpegurl'
  2356. }, this.clock);
  2357. openMediaSource(this.player, this.clock);
  2358. assert.ok(this.player.tech_.hls.qualityLevels_, 'added quality levels from video with source');
  2359. });
  2360. QUnit.module('HLS Integration', {
  2361. beforeEach(assert) {
  2362. this.env = useFakeEnvironment(assert);
  2363. this.requests = this.env.requests;
  2364. this.mse = useFakeMediaSource();
  2365. this.tech = new (videojs.getTech('Html5'))({});
  2366. this.clock = this.env.clock;
  2367. this.standardXHRResponse = (request, data) => {
  2368. standardXHRResponse(request, data);
  2369. // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
  2370. // we have to use clock.tick to get the expected side effects of
  2371. // SegmentLoader#handleUpdateEnd_
  2372. this.clock.tick(1);
  2373. };
  2374. videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
  2375. },
  2376. afterEach() {
  2377. this.env.restore();
  2378. this.mse.restore();
  2379. videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
  2380. }
  2381. });
  2382. QUnit.test('does not error when MediaSource is not defined', function(assert) {
  2383. window.MediaSource = null;
  2384. let hls = HlsSourceHandler('html5').handleSource({
  2385. src: 'manifest/alternateAudio.m3u8',
  2386. type: 'application/vnd.apple.mpegurl'
  2387. }, this.tech);
  2388. hls.mediaSource.trigger('sourceopen');
  2389. // master
  2390. this.standardXHRResponse(this.requests.shift());
  2391. // media
  2392. this.standardXHRResponse(this.requests.shift());
  2393. assert.ok(true, 'did not throw an exception');
  2394. });
  2395. QUnit.test('aborts all in-flight work when disposed', function(assert) {
  2396. let hls = HlsSourceHandler('html5').handleSource({
  2397. src: 'manifest/master.m3u8',
  2398. type: 'application/vnd.apple.mpegurl'
  2399. }, this.tech);
  2400. hls.mediaSource.trigger('sourceopen');
  2401. // master
  2402. this.standardXHRResponse(this.requests.shift());
  2403. // media
  2404. this.standardXHRResponse(this.requests.shift());
  2405. hls.dispose();
  2406. assert.ok(this.requests[0].aborted, 'aborted the old segment request');
  2407. hls.mediaSource.sourceBuffers.forEach(sourceBuffer => {
  2408. let lastUpdate = sourceBuffer.updates_[sourceBuffer.updates_.length - 1];
  2409. assert.ok(lastUpdate.abort, 'aborted the source buffer');
  2410. });
  2411. });
  2412. QUnit.test('stats are reset on dispose', function(assert) {
  2413. let hls = HlsSourceHandler('html5').handleSource({
  2414. src: 'manifest/master.m3u8',
  2415. type: 'application/vnd.apple.mpegurl'
  2416. }, this.tech);
  2417. hls.mediaSource.trigger('sourceopen');
  2418. // master
  2419. this.standardXHRResponse(this.requests.shift());
  2420. // media
  2421. this.standardXHRResponse(this.requests.shift());
  2422. // media
  2423. this.standardXHRResponse(this.requests.shift());
  2424. assert.equal(hls.stats.mediaBytesTransferred, 1024, 'stat is set');
  2425. hls.dispose();
  2426. assert.equal(hls.stats.mediaBytesTransferred, 0, 'stat is reset');
  2427. });
  2428. QUnit.test('detects fullscreen and triggers a quality change', function(assert) {
  2429. let qualityChanges = 0;
  2430. let hls = HlsSourceHandler('html5').handleSource({
  2431. src: 'manifest/master.m3u8',
  2432. type: 'application/vnd.apple.mpegurl'
  2433. }, this.tech);
  2434. let fullscreenElementName;
  2435. ['fullscreenElement', 'webkitFullscreenElement',
  2436. 'mozFullScreenElement', 'msFullscreenElement'
  2437. ].forEach((name) => {
  2438. if (!fullscreenElementName && !document.hasOwnProperty(name)) {
  2439. fullscreenElementName = name;
  2440. }
  2441. });
  2442. hls.masterPlaylistController_.fastQualityChange_ = function() {
  2443. qualityChanges++;
  2444. };
  2445. // take advantage of capability detection to mock fullscreen activation
  2446. document[fullscreenElementName] = this.tech.el();
  2447. Events.trigger(document, 'fullscreenchange');
  2448. assert.equal(qualityChanges, 1, 'made a fast quality change');
  2449. // don't do a fast quality change when returning from fullscreen;
  2450. // allow the video element to rescale the already buffered video
  2451. document[fullscreenElementName] = null;
  2452. Events.trigger(document, 'fullscreenchange');
  2453. assert.equal(qualityChanges, 1, 'did not make another quality change');
  2454. });
  2455. QUnit.test('downloads additional playlists if required', function(assert) {
  2456. let originalPlaylist;
  2457. let hls = HlsSourceHandler('html5').handleSource({
  2458. src: 'manifest/master.m3u8',
  2459. type: 'application/vnd.apple.mpegurl'
  2460. }, this.tech);
  2461. // Make segment metadata noop since most test segments dont have real data
  2462. hls.masterPlaylistController_.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
  2463. hls.mediaSource.trigger('sourceopen');
  2464. hls.bandwidth = 1;
  2465. // master
  2466. this.standardXHRResponse(this.requests[0]);
  2467. // media
  2468. this.standardXHRResponse(this.requests[1]);
  2469. originalPlaylist = hls.playlists.media();
  2470. hls.masterPlaylistController_.mainSegmentLoader_.mediaIndex = 0;
  2471. // the playlist selection is revisited after a new segment is downloaded
  2472. this.requests[2].bandwidth = 3000000;
  2473. // segment
  2474. this.standardXHRResponse(this.requests[2]);
  2475. // update the buffer to reflect the appended segment, and have enough buffer to
  2476. // change playlist
  2477. this.tech.buffered = () => videojs.createTimeRanges([[0, 30]]);
  2478. hls.mediaSource.sourceBuffers[0].trigger('updateend');
  2479. // new media
  2480. this.standardXHRResponse(this.requests[3]);
  2481. assert.ok((/manifest\/media\d+.m3u8$/).test(this.requests[3].url),
  2482. 'made a playlist request');
  2483. assert.notEqual(originalPlaylist.resolvedUri,
  2484. hls.playlists.media().resolvedUri,
  2485. 'a new playlists was selected');
  2486. assert.ok(hls.playlists.media().segments, 'segments are now available');
  2487. // verify stats
  2488. assert.equal(hls.stats.bandwidth, 3000000, 'default');
  2489. assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  2490. assert.equal(hls.stats.mediaRequests, 1, '1 request');
  2491. });
  2492. QUnit.test('waits to download new segments until the media playlist is stable', function(assert) {
  2493. let sourceBuffer;
  2494. let hls = HlsSourceHandler('html5').handleSource({
  2495. src: 'manifest/master.m3u8',
  2496. type: 'application/vnd.apple.mpegurl'
  2497. }, this.tech);
  2498. hls.masterPlaylistController_.mainSegmentLoader_.addSegmentMetadataCue_ = () => {};
  2499. hls.mediaSource.trigger('sourceopen');
  2500. // make sure we stay on the lowest variant
  2501. hls.bandwidth = 1;
  2502. // master
  2503. this.standardXHRResponse(this.requests.shift());
  2504. // media1
  2505. this.standardXHRResponse(this.requests.shift());
  2506. // source buffer created after media source is open and first media playlist is selected
  2507. sourceBuffer = hls.mediaSource.sourceBuffers[0];
  2508. hls.masterPlaylistController_.mainSegmentLoader_.mediaIndex = 0;
  2509. // segment 0
  2510. this.standardXHRResponse(this.requests.shift());
  2511. // update the buffer to reflect the appended segment, and have enough buffer to
  2512. // change playlist
  2513. this.tech.buffered = () => videojs.createTimeRanges([[0, 30]]);
  2514. // no time has elapsed, so bandwidth is really high and we'll switch
  2515. // playlists
  2516. sourceBuffer.trigger('updateend');
  2517. assert.equal(this.requests.length, 1, 'only the playlist request outstanding');
  2518. this.clock.tick(10 * 1000);
  2519. assert.equal(this.requests.length, 1, 'delays segment fetching');
  2520. // another media playlist
  2521. this.standardXHRResponse(this.requests.shift());
  2522. this.clock.tick(10 * 1000);
  2523. assert.equal(this.requests.length, 1, 'resumes segment fetching');
  2524. // verify stats
  2525. assert.equal(hls.stats.bandwidth, Infinity, 'default');
  2526. assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  2527. assert.equal(hls.stats.mediaRequests, 1, '1 request');
  2528. });
  2529. QUnit.test('live playlist starts three target durations before live', function(assert) {
  2530. let hls = HlsSourceHandler('html5').handleSource({
  2531. src: 'manifest/master.m3u8',
  2532. type: 'application/vnd.apple.mpegurl'
  2533. }, this.tech);
  2534. hls.mediaSource.trigger('sourceopen');
  2535. this.requests.shift().respond(200, null,
  2536. '#EXTM3U\n' +
  2537. '#EXT-X-MEDIA-SEQUENCE:101\n' +
  2538. '#EXTINF:10,\n' +
  2539. '0.ts\n' +
  2540. '#EXTINF:10,\n' +
  2541. '1.ts\n' +
  2542. '#EXTINF:10,\n' +
  2543. '2.ts\n' +
  2544. '#EXTINF:10,\n' +
  2545. '3.ts\n' +
  2546. '#EXTINF:10,\n' +
  2547. '4.ts\n');
  2548. assert.equal(this.requests.length, 0, 'no outstanding segment request');
  2549. this.tech.paused = function() {
  2550. return false;
  2551. };
  2552. this.tech.trigger('play');
  2553. this.clock.tick(1);
  2554. assert.equal(this.tech.currentTime(),
  2555. hls.seekable().end(0),
  2556. 'seeked to the seekable end');
  2557. assert.equal(this.requests.length, 1, 'begins buffering');
  2558. });
  2559. QUnit.test('uses user defined selectPlaylist from HlsHandler if specified', function(assert) {
  2560. let origStandardPlaylistSelector = Hls.STANDARD_PLAYLIST_SELECTOR;
  2561. let defaultSelectPlaylistCount = 0;
  2562. Hls.STANDARD_PLAYLIST_SELECTOR = () => defaultSelectPlaylistCount++;
  2563. let hls = HlsSourceHandler('html5').handleSource({
  2564. src: 'manifest/master.m3u8',
  2565. type: 'application/vnd.apple.mpegurl'
  2566. }, this.tech);
  2567. hls.masterPlaylistController_.selectPlaylist();
  2568. assert.equal(defaultSelectPlaylistCount, 1, 'uses default playlist selector');
  2569. defaultSelectPlaylistCount = 0;
  2570. let newSelectPlaylistCount = 0;
  2571. let newSelectPlaylist = () => newSelectPlaylistCount++;
  2572. HlsHandler.prototype.selectPlaylist = newSelectPlaylist;
  2573. hls = HlsSourceHandler('html5').handleSource({
  2574. src: 'manifest/master.m3u8',
  2575. type: 'application/vnd.apple.mpegurl'
  2576. }, this.tech);
  2577. hls.masterPlaylistController_.selectPlaylist();
  2578. assert.equal(defaultSelectPlaylistCount, 0, 'standard playlist selector not run');
  2579. assert.equal(newSelectPlaylistCount, 1, 'uses overridden playlist selector');
  2580. newSelectPlaylistCount = 0;
  2581. let setSelectPlaylistCount = 0;
  2582. hls.selectPlaylist = () => setSelectPlaylistCount++;
  2583. hls.masterPlaylistController_.selectPlaylist();
  2584. assert.equal(defaultSelectPlaylistCount, 0, 'standard playlist selector not run');
  2585. assert.equal(newSelectPlaylistCount, 0, 'overridden playlist selector not run');
  2586. assert.equal(setSelectPlaylistCount, 1, 'uses set playlist selector');
  2587. Hls.STANDARD_PLAYLIST_SELECTOR = origStandardPlaylistSelector;
  2588. delete HlsHandler.prototype.selectPlaylist;
  2589. });
  2590. QUnit.module('HLS - Encryption', {
  2591. beforeEach(assert) {
  2592. this.env = useFakeEnvironment(assert);
  2593. this.requests = this.env.requests;
  2594. this.mse = useFakeMediaSource();
  2595. this.tech = new (videojs.getTech('Html5'))({});
  2596. this.clock = this.env.clock;
  2597. this.standardXHRResponse = (request, data) => {
  2598. standardXHRResponse(request, data);
  2599. // Because SegmentLoader#fillBuffer_ is now scheduled asynchronously
  2600. // we have to use clock.tick to get the expected side effects of
  2601. // SegmentLoader#handleUpdateEnd_
  2602. this.clock.tick(1);
  2603. };
  2604. videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
  2605. },
  2606. afterEach() {
  2607. this.env.restore();
  2608. this.mse.restore();
  2609. videojs.HlsHandler.prototype.setupQualityLevels_ = ogHlsHandlerSetupQualityLevels;
  2610. }
  2611. });
  2612. QUnit.test('blacklists playlist if key requests fail', function(assert) {
  2613. let hls = HlsSourceHandler('html5').handleSource({
  2614. src: 'manifest/encrypted-master.m3u8',
  2615. type: 'application/vnd.apple.mpegurl'
  2616. }, this.tech);
  2617. hls.mediaSource.trigger('sourceopen');
  2618. this.requests.shift()
  2619. .respond(200, null,
  2620. '#EXTM3U\n' +
  2621. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  2622. 'media.m3u8\n' +
  2623. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  2624. 'media1.m3u8\n');
  2625. this.requests.shift()
  2626. .respond(200, null,
  2627. '#EXTM3U\n' +
  2628. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' +
  2629. '#EXTINF:2.833,\n' +
  2630. 'http://media.example.com/fileSequence52-A.ts\n' +
  2631. '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
  2632. '#EXTINF:15.0,\n' +
  2633. 'http://media.example.com/fileSequence53-A.ts\n' +
  2634. '#EXT-X-ENDLIST\n');
  2635. this.clock.tick(1);
  2636. // segment 1
  2637. if (/key\.php/i.test(this.requests[0].url)) {
  2638. this.standardXHRResponse(this.requests.pop());
  2639. } else {
  2640. this.standardXHRResponse(this.requests.shift());
  2641. }
  2642. // fail key
  2643. this.requests.shift().respond(404);
  2644. assert.ok(hls.playlists.media().excludeUntil > 0,
  2645. 'playlist blacklisted');
  2646. assert.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
  2647. });
  2648. QUnit.test('treats invalid keys as a key request failure and blacklists playlist', function(assert) {
  2649. let hls = HlsSourceHandler('html5').handleSource({
  2650. src: 'manifest/encrypted-master.m3u8',
  2651. type: 'application/vnd.apple.mpegurl'
  2652. }, this.tech);
  2653. hls.mediaSource.trigger('sourceopen');
  2654. this.requests.shift()
  2655. .respond(200, null,
  2656. '#EXTM3U\n' +
  2657. '#EXT-X-STREAM-INF:BANDWIDTH=1000\n' +
  2658. 'media.m3u8\n' +
  2659. '#EXT-X-STREAM-INF:BANDWIDTH=1\n' +
  2660. 'media1.m3u8\n');
  2661. this.requests.shift()
  2662. .respond(200, null,
  2663. '#EXTM3U\n' +
  2664. '#EXT-X-MEDIA-SEQUENCE:5\n' +
  2665. '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' +
  2666. '#EXTINF:2.833,\n' +
  2667. 'http://media.example.com/fileSequence52-A.ts\n' +
  2668. '#EXT-X-KEY:METHOD=NONE\n' +
  2669. '#EXTINF:15.0,\n' +
  2670. 'http://media.example.com/fileSequence52-B.ts\n' +
  2671. '#EXT-X-ENDLIST\n');
  2672. this.clock.tick(1);
  2673. // segment request
  2674. this.standardXHRResponse(this.requests.pop());
  2675. assert.equal(this.requests[0].url,
  2676. 'https://priv.example.com/key.php?r=52',
  2677. 'requested the key');
  2678. // keys *should* be 16 bytes long -- this one is too small
  2679. this.requests[0].response = new Uint8Array(1).buffer;
  2680. this.requests.shift().respond(200, null, '');
  2681. this.clock.tick(1);
  2682. // blacklist this playlist
  2683. assert.ok(hls.playlists.media().excludeUntil > 0,
  2684. 'blacklisted playlist');
  2685. assert.equal(this.env.log.warn.calls, 1, 'logged warning for blacklist');
  2686. // verify stats
  2687. assert.equal(hls.stats.mediaBytesTransferred, 1024, '1024 bytes');
  2688. assert.equal(hls.stats.mediaRequests, 1, '1 request');
  2689. });