playlist-controller.js 66 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031
  1. /**
  2. * @file playlist-controller.js
  3. */
  4. import window from 'global/window';
  5. import PlaylistLoader from './playlist-loader';
  6. import DashPlaylistLoader from './dash-playlist-loader';
  7. import { isEnabled, isLowestEnabledRendition } from './playlist.js';
  8. import SegmentLoader from './segment-loader';
  9. import SourceUpdater from './source-updater';
  10. import VTTSegmentLoader from './vtt-segment-loader';
  11. import * as Ranges from './ranges';
  12. import videojs from 'video.js';
  13. import { updateAdCues } from './ad-cue-tags';
  14. import SyncController from './sync-controller';
  15. import TimelineChangeController from './timeline-change-controller';
  16. import Decrypter from 'worker!./decrypter-worker.js';
  17. import Config from './config';
  18. import {
  19. parseCodecs,
  20. browserSupportsCodec,
  21. muxerSupportsCodec,
  22. DEFAULT_AUDIO_CODEC,
  23. DEFAULT_VIDEO_CODEC
  24. } from '@videojs/vhs-utils/es/codecs.js';
  25. import { codecsForPlaylist, unwrapCodecList, codecCount } from './util/codecs.js';
  26. import { createMediaTypes, setupMediaGroups } from './media-groups';
  27. import logger from './util/logger';
  28. import {merge, createTimeRanges} from './util/vjs-compat';
  29. const ABORT_EARLY_EXCLUSION_SECONDS = 60 * 2;
  30. let Vhs;
  31. // SegmentLoader stats that need to have each loader's
  32. // values summed to calculate the final value
  33. const loaderStats = [
  34. 'mediaRequests',
  35. 'mediaRequestsAborted',
  36. 'mediaRequestsTimedout',
  37. 'mediaRequestsErrored',
  38. 'mediaTransferDuration',
  39. 'mediaBytesTransferred',
  40. 'mediaAppends'
  41. ];
  42. const sumLoaderStat = function(stat) {
  43. return this.audioSegmentLoader_[stat] +
  44. this.mainSegmentLoader_[stat];
  45. };
  46. const shouldSwitchToMedia = function({
  47. currentPlaylist,
  48. buffered,
  49. currentTime,
  50. nextPlaylist,
  51. bufferLowWaterLine,
  52. bufferHighWaterLine,
  53. duration,
  54. bufferBasedABR,
  55. log
  56. }) {
  57. // we have no other playlist to switch to
  58. if (!nextPlaylist) {
  59. videojs.log.warn('We received no playlist to switch to. Please check your stream.');
  60. return false;
  61. }
  62. const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`;
  63. if (!currentPlaylist) {
  64. log(`${sharedLogLine} as current playlist is not set`);
  65. return true;
  66. }
  67. // no need to switch if playlist is the same
  68. if (nextPlaylist.id === currentPlaylist.id) {
  69. return false;
  70. }
  71. // determine if current time is in a buffered range.
  72. const isBuffered = Boolean(Ranges.findRange(buffered, currentTime).length);
  73. // If the playlist is live, then we want to not take low water line into account.
  74. // This is because in LIVE, the player plays 3 segments from the end of the
  75. // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble
  76. // in those segments, a viewer will never experience a rendition upswitch.
  77. if (!currentPlaylist.endList) {
  78. // For LLHLS live streams, don't switch renditions before playback has started, as it almost
  79. // doubles the time to first playback.
  80. if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') {
  81. log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`);
  82. return false;
  83. }
  84. log(`${sharedLogLine} as current playlist is live`);
  85. return true;
  86. }
  87. const forwardBuffer = Ranges.timeAheadOf(buffered, currentTime);
  88. const maxBufferLowWaterLine = bufferBasedABR ?
  89. Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE;
  90. // For the same reason as LIVE, we ignore the low water line when the VOD
  91. // duration is below the max potential low water line
  92. if (duration < maxBufferLowWaterLine) {
  93. log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`);
  94. return true;
  95. }
  96. const nextBandwidth = nextPlaylist.attributes.BANDWIDTH;
  97. const currBandwidth = currentPlaylist.attributes.BANDWIDTH;
  98. // when switching down, if our buffer is lower than the high water line,
  99. // we can switch down
  100. if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) {
  101. let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`;
  102. if (bufferBasedABR) {
  103. logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`;
  104. }
  105. log(logLine);
  106. return true;
  107. }
  108. // and if our buffer is higher than the low water line,
  109. // we can switch up
  110. if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) {
  111. let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`;
  112. if (bufferBasedABR) {
  113. logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`;
  114. }
  115. log(logLine);
  116. return true;
  117. }
  118. log(`not ${sharedLogLine} as no switching criteria met`);
  119. return false;
  120. };
  121. /**
  122. * the main playlist controller controller all interactons
  123. * between playlists and segmentloaders. At this time this mainly
  124. * involves a main playlist and a series of audio playlists
  125. * if they are available
  126. *
  127. * @class PlaylistController
  128. * @extends videojs.EventTarget
  129. */
  130. export class PlaylistController extends videojs.EventTarget {
  131. constructor(options) {
  132. super();
  133. const {
  134. src,
  135. withCredentials,
  136. tech,
  137. bandwidth,
  138. externVhs,
  139. useCueTags,
  140. playlistExclusionDuration,
  141. enableLowInitialPlaylist,
  142. sourceType,
  143. cacheEncryptionKeys,
  144. bufferBasedABR,
  145. leastPixelDiffSelector,
  146. captionServices
  147. } = options;
  148. if (!src) {
  149. throw new Error('A non-empty playlist URL or JSON manifest string is required');
  150. }
  151. let { maxPlaylistRetries } = options;
  152. if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') {
  153. maxPlaylistRetries = Infinity;
  154. }
  155. Vhs = externVhs;
  156. this.bufferBasedABR = Boolean(bufferBasedABR);
  157. this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector);
  158. this.withCredentials = withCredentials;
  159. this.tech_ = tech;
  160. this.vhs_ = tech.vhs;
  161. this.sourceType_ = sourceType;
  162. this.useCueTags_ = useCueTags;
  163. this.playlistExclusionDuration = playlistExclusionDuration;
  164. this.maxPlaylistRetries = maxPlaylistRetries;
  165. this.enableLowInitialPlaylist = enableLowInitialPlaylist;
  166. if (this.useCueTags_) {
  167. this.cueTagsTrack_ = this.tech_.addTextTrack(
  168. 'metadata',
  169. 'ad-cues'
  170. );
  171. this.cueTagsTrack_.inBandMetadataTrackDispatchType = '';
  172. }
  173. this.requestOptions_ = {
  174. withCredentials,
  175. maxPlaylistRetries,
  176. timeout: null
  177. };
  178. this.on('error', this.pauseLoading);
  179. this.mediaTypes_ = createMediaTypes();
  180. this.mediaSource = new window.MediaSource();
  181. this.handleDurationChange_ = this.handleDurationChange_.bind(this);
  182. this.handleSourceOpen_ = this.handleSourceOpen_.bind(this);
  183. this.handleSourceEnded_ = this.handleSourceEnded_.bind(this);
  184. this.mediaSource.addEventListener('durationchange', this.handleDurationChange_);
  185. // load the media source into the player
  186. this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_);
  187. this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_);
  188. // we don't have to handle sourceclose since dispose will handle termination of
  189. // everything, and the MediaSource should not be detached without a proper disposal
  190. this.seekable_ = createTimeRanges();
  191. this.hasPlayed_ = false;
  192. this.syncController_ = new SyncController(options);
  193. this.segmentMetadataTrack_ = tech.addRemoteTextTrack({
  194. kind: 'metadata',
  195. label: 'segment-metadata'
  196. }, false).track;
  197. this.decrypter_ = new Decrypter();
  198. this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
  199. this.inbandTextTracks_ = {};
  200. this.timelineChangeController_ = new TimelineChangeController();
  201. const segmentLoaderSettings = {
  202. vhs: this.vhs_,
  203. parse708captions: options.parse708captions,
  204. useDtsForTimestampOffset: options.useDtsForTimestampOffset,
  205. captionServices,
  206. mediaSource: this.mediaSource,
  207. currentTime: this.tech_.currentTime.bind(this.tech_),
  208. seekable: () => this.seekable(),
  209. seeking: () => this.tech_.seeking(),
  210. duration: () => this.duration(),
  211. hasPlayed: () => this.hasPlayed_,
  212. goalBufferLength: () => this.goalBufferLength(),
  213. bandwidth,
  214. syncController: this.syncController_,
  215. decrypter: this.decrypter_,
  216. sourceType: this.sourceType_,
  217. inbandTextTracks: this.inbandTextTracks_,
  218. cacheEncryptionKeys,
  219. sourceUpdater: this.sourceUpdater_,
  220. timelineChangeController: this.timelineChangeController_,
  221. exactManifestTimings: options.exactManifestTimings
  222. };
  223. // The source type check not only determines whether a special DASH playlist loader
  224. // should be used, but also covers the case where the provided src is a vhs-json
  225. // manifest object (instead of a URL). In the case of vhs-json, the default
  226. // PlaylistLoader should be used.
  227. this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ?
  228. new DashPlaylistLoader(src, this.vhs_, this.requestOptions_) :
  229. new PlaylistLoader(src, this.vhs_, this.requestOptions_);
  230. this.setupMainPlaylistLoaderListeners_();
  231. // setup segment loaders
  232. // combined audio/video or just video when alternate audio track is selected
  233. this.mainSegmentLoader_ =
  234. new SegmentLoader(merge(segmentLoaderSettings, {
  235. segmentMetadataTrack: this.segmentMetadataTrack_,
  236. loaderType: 'main'
  237. }), options);
  238. // alternate audio track
  239. this.audioSegmentLoader_ =
  240. new SegmentLoader(merge(segmentLoaderSettings, {
  241. loaderType: 'audio'
  242. }), options);
  243. this.subtitleSegmentLoader_ =
  244. new VTTSegmentLoader(merge(segmentLoaderSettings, {
  245. loaderType: 'vtt',
  246. featuresNativeTextTracks: this.tech_.featuresNativeTextTracks,
  247. loadVttJs: () => new Promise((resolve, reject) => {
  248. function onLoad() {
  249. tech.off('vttjserror', onError);
  250. resolve();
  251. }
  252. function onError() {
  253. tech.off('vttjsloaded', onLoad);
  254. reject();
  255. }
  256. tech.one('vttjsloaded', onLoad);
  257. tech.one('vttjserror', onError);
  258. // safe to call multiple times, script will be loaded only once:
  259. tech.addWebVttScript_();
  260. })
  261. }), options);
  262. this.setupSegmentLoaderListeners_();
  263. if (this.bufferBasedABR) {
  264. this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_());
  265. this.tech_.on('pause', () => this.stopABRTimer_());
  266. this.tech_.on('play', () => this.startABRTimer_());
  267. }
  268. // Create SegmentLoader stat-getters
  269. // mediaRequests_
  270. // mediaRequestsAborted_
  271. // mediaRequestsTimedout_
  272. // mediaRequestsErrored_
  273. // mediaTransferDuration_
  274. // mediaBytesTransferred_
  275. // mediaAppends_
  276. loaderStats.forEach((stat) => {
  277. this[stat + '_'] = sumLoaderStat.bind(this, stat);
  278. });
  279. this.logger_ = logger('pc');
  280. this.triggeredFmp4Usage = false;
  281. if (this.tech_.preload() === 'none') {
  282. this.loadOnPlay_ = () => {
  283. this.loadOnPlay_ = null;
  284. this.mainPlaylistLoader_.load();
  285. };
  286. this.tech_.one('play', this.loadOnPlay_);
  287. } else {
  288. this.mainPlaylistLoader_.load();
  289. }
  290. this.timeToLoadedData__ = -1;
  291. this.mainAppendsToLoadedData__ = -1;
  292. this.audioAppendsToLoadedData__ = -1;
  293. const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart';
  294. // start the first frame timer on loadstart or play (for preload none)
  295. this.tech_.one(event, () => {
  296. const timeToLoadedDataStart = Date.now();
  297. this.tech_.one('loadeddata', () => {
  298. this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart;
  299. this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends;
  300. this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends;
  301. });
  302. });
  303. }
  304. mainAppendsToLoadedData_() {
  305. return this.mainAppendsToLoadedData__;
  306. }
  307. audioAppendsToLoadedData_() {
  308. return this.audioAppendsToLoadedData__;
  309. }
  310. appendsToLoadedData_() {
  311. const main = this.mainAppendsToLoadedData_();
  312. const audio = this.audioAppendsToLoadedData_();
  313. if (main === -1 || audio === -1) {
  314. return -1;
  315. }
  316. return main + audio;
  317. }
  318. timeToLoadedData_() {
  319. return this.timeToLoadedData__;
  320. }
  321. /**
  322. * Run selectPlaylist and switch to the new playlist if we should
  323. *
  324. * @param {string} [reason=abr] a reason for why the ABR check is made
  325. * @private
  326. */
  327. checkABR_(reason = 'abr') {
  328. const nextPlaylist = this.selectPlaylist();
  329. if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) {
  330. this.switchMedia_(nextPlaylist, reason);
  331. }
  332. }
  333. switchMedia_(playlist, cause, delay) {
  334. const oldMedia = this.media();
  335. const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
  336. const newId = playlist.id || playlist.uri;
  337. if (oldId && oldId !== newId) {
  338. this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
  339. this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`});
  340. }
  341. this.mainPlaylistLoader_.media(playlist, delay);
  342. }
  343. /**
  344. * Start a timer that periodically calls checkABR_
  345. *
  346. * @private
  347. */
  348. startABRTimer_() {
  349. this.stopABRTimer_();
  350. this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250);
  351. }
  352. /**
  353. * Stop the timer that periodically calls checkABR_
  354. *
  355. * @private
  356. */
  357. stopABRTimer_() {
  358. // if we're scrubbing, we don't need to pause.
  359. // This getter will be added to Video.js in version 7.11.
  360. if (this.tech_.scrubbing && this.tech_.scrubbing()) {
  361. return;
  362. }
  363. window.clearInterval(this.abrTimer_);
  364. this.abrTimer_ = null;
  365. }
  366. /**
  367. * Get a list of playlists for the currently selected audio playlist
  368. *
  369. * @return {Array} the array of audio playlists
  370. */
  371. getAudioTrackPlaylists_() {
  372. const main = this.main();
  373. const defaultPlaylists = main && main.playlists || [];
  374. // if we don't have any audio groups then we can only
  375. // assume that the audio tracks are contained in main
  376. // playlist array, use that or an empty array.
  377. if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) {
  378. return defaultPlaylists;
  379. }
  380. const AUDIO = main.mediaGroups.AUDIO;
  381. const groupKeys = Object.keys(AUDIO);
  382. let track;
  383. // get the current active track
  384. if (Object.keys(this.mediaTypes_.AUDIO.groups).length) {
  385. track = this.mediaTypes_.AUDIO.activeTrack();
  386. // or get the default track from main if mediaTypes_ isn't setup yet
  387. } else {
  388. // default group is `main` or just the first group.
  389. const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]];
  390. for (const label in defaultGroup) {
  391. if (defaultGroup[label].default) {
  392. track = {label};
  393. break;
  394. }
  395. }
  396. }
  397. // no active track no playlists.
  398. if (!track) {
  399. return defaultPlaylists;
  400. }
  401. const playlists = [];
  402. // get all of the playlists that are possible for the
  403. // active track.
  404. for (const group in AUDIO) {
  405. if (AUDIO[group][track.label]) {
  406. const properties = AUDIO[group][track.label];
  407. if (properties.playlists && properties.playlists.length) {
  408. playlists.push.apply(playlists, properties.playlists);
  409. } else if (properties.uri) {
  410. playlists.push(properties);
  411. } else if (main.playlists.length) {
  412. // if an audio group does not have a uri
  413. // see if we have main playlists that use it as a group.
  414. // if we do then add those to the playlists list.
  415. for (let i = 0; i < main.playlists.length; i++) {
  416. const playlist = main.playlists[i];
  417. if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) {
  418. playlists.push(playlist);
  419. }
  420. }
  421. }
  422. }
  423. }
  424. if (!playlists.length) {
  425. return defaultPlaylists;
  426. }
  427. return playlists;
  428. }
  429. /**
  430. * Register event handlers on the main playlist loader. A helper
  431. * function for construction time.
  432. *
  433. * @private
  434. */
  435. setupMainPlaylistLoaderListeners_() {
  436. this.mainPlaylistLoader_.on('loadedmetadata', () => {
  437. const media = this.mainPlaylistLoader_.media();
  438. const requestTimeout = (media.targetDuration * 1.5) * 1000;
  439. // If we don't have any more available playlists, we don't want to
  440. // timeout the request.
  441. if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
  442. this.requestOptions_.timeout = 0;
  443. } else {
  444. this.requestOptions_.timeout = requestTimeout;
  445. }
  446. // if this isn't a live video and preload permits, start
  447. // downloading segments
  448. if (media.endList && this.tech_.preload() !== 'none') {
  449. this.mainSegmentLoader_.playlist(media, this.requestOptions_);
  450. this.mainSegmentLoader_.load();
  451. }
  452. setupMediaGroups({
  453. sourceType: this.sourceType_,
  454. segmentLoaders: {
  455. AUDIO: this.audioSegmentLoader_,
  456. SUBTITLES: this.subtitleSegmentLoader_,
  457. main: this.mainSegmentLoader_
  458. },
  459. tech: this.tech_,
  460. requestOptions: this.requestOptions_,
  461. mainPlaylistLoader: this.mainPlaylistLoader_,
  462. vhs: this.vhs_,
  463. main: this.main(),
  464. mediaTypes: this.mediaTypes_,
  465. excludePlaylist: this.excludePlaylist.bind(this)
  466. });
  467. this.triggerPresenceUsage_(this.main(), media);
  468. this.setupFirstPlay();
  469. if (!this.mediaTypes_.AUDIO.activePlaylistLoader ||
  470. this.mediaTypes_.AUDIO.activePlaylistLoader.media()) {
  471. this.trigger('selectedinitialmedia');
  472. } else {
  473. // We must wait for the active audio playlist loader to
  474. // finish setting up before triggering this event so the
  475. // representations API and EME setup is correct
  476. this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => {
  477. this.trigger('selectedinitialmedia');
  478. });
  479. }
  480. });
  481. this.mainPlaylistLoader_.on('loadedplaylist', () => {
  482. if (this.loadOnPlay_) {
  483. this.tech_.off('play', this.loadOnPlay_);
  484. }
  485. let updatedPlaylist = this.mainPlaylistLoader_.media();
  486. if (!updatedPlaylist) {
  487. // exclude any variants that are not supported by the browser before selecting
  488. // an initial media as the playlist selectors do not consider browser support
  489. this.excludeUnsupportedVariants_();
  490. let selectedMedia;
  491. if (this.enableLowInitialPlaylist) {
  492. selectedMedia = this.selectInitialPlaylist();
  493. }
  494. if (!selectedMedia) {
  495. selectedMedia = this.selectPlaylist();
  496. }
  497. if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) {
  498. return;
  499. }
  500. this.initialMedia_ = selectedMedia;
  501. this.switchMedia_(this.initialMedia_, 'initial');
  502. // Under the standard case where a source URL is provided, loadedplaylist will
  503. // fire again since the playlist will be requested. In the case of vhs-json
  504. // (where the manifest object is provided as the source), when the media
  505. // playlist's `segments` list is already available, a media playlist won't be
  506. // requested, and loadedplaylist won't fire again, so the playlist handler must be
  507. // called on its own here.
  508. const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments;
  509. if (!haveJsonSource) {
  510. return;
  511. }
  512. updatedPlaylist = this.initialMedia_;
  513. }
  514. this.handleUpdatedMediaPlaylist(updatedPlaylist);
  515. });
  516. this.mainPlaylistLoader_.on('error', () => {
  517. const error = this.mainPlaylistLoader_.error;
  518. this.excludePlaylist({ playlistToExclude: error.playlist, error });
  519. });
  520. this.mainPlaylistLoader_.on('mediachanging', () => {
  521. this.mainSegmentLoader_.abort();
  522. this.mainSegmentLoader_.pause();
  523. });
  524. this.mainPlaylistLoader_.on('mediachange', () => {
  525. const media = this.mainPlaylistLoader_.media();
  526. const requestTimeout = (media.targetDuration * 1.5) * 1000;
  527. // If we don't have any more available playlists, we don't want to
  528. // timeout the request.
  529. if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) {
  530. this.requestOptions_.timeout = 0;
  531. } else {
  532. this.requestOptions_.timeout = requestTimeout;
  533. }
  534. this.mainPlaylistLoader_.load();
  535. // TODO: Create a new event on the PlaylistLoader that signals
  536. // that the segments have changed in some way and use that to
  537. // update the SegmentLoader instead of doing it twice here and
  538. // on `loadedplaylist`
  539. this.mainSegmentLoader_.playlist(media, this.requestOptions_);
  540. this.mainSegmentLoader_.load();
  541. this.tech_.trigger({
  542. type: 'mediachange',
  543. bubbles: true
  544. });
  545. });
  546. this.mainPlaylistLoader_.on('playlistunchanged', () => {
  547. const updatedPlaylist = this.mainPlaylistLoader_.media();
  548. // ignore unchanged playlists that have already been
  549. // excluded for not-changing. We likely just have a really slowly updating
  550. // playlist.
  551. if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') {
  552. return;
  553. }
  554. const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist);
  555. if (playlistOutdated) {
  556. // Playlist has stopped updating and we're stuck at its end. Try to
  557. // exclude it and switch to another playlist in the hope that that
  558. // one is updating (and give the player a chance to re-adjust to the
  559. // safe live point).
  560. this.excludePlaylist({
  561. error: {
  562. message: 'Playlist no longer updating.',
  563. reason: 'playlist-unchanged'
  564. }
  565. });
  566. // useful for monitoring QoS
  567. this.tech_.trigger('playliststuck');
  568. }
  569. });
  570. this.mainPlaylistLoader_.on('renditiondisabled', () => {
  571. this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled'});
  572. });
  573. this.mainPlaylistLoader_.on('renditionenabled', () => {
  574. this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled'});
  575. });
  576. }
  577. /**
  578. * Given an updated media playlist (whether it was loaded for the first time, or
  579. * refreshed for live playlists), update any relevant properties and state to reflect
  580. * changes in the media that should be accounted for (e.g., cues and duration).
  581. *
  582. * @param {Object} updatedPlaylist the updated media playlist object
  583. *
  584. * @private
  585. */
  586. handleUpdatedMediaPlaylist(updatedPlaylist) {
  587. if (this.useCueTags_) {
  588. this.updateAdCues_(updatedPlaylist);
  589. }
  590. // TODO: Create a new event on the PlaylistLoader that signals
  591. // that the segments have changed in some way and use that to
  592. // update the SegmentLoader instead of doing it twice here and
  593. // on `mediachange`
  594. this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
  595. this.updateDuration(!updatedPlaylist.endList);
  596. // If the player isn't paused, ensure that the segment loader is running,
  597. // as it is possible that it was temporarily stopped while waiting for
  598. // a playlist (e.g., in case the playlist errored and we re-requested it).
  599. if (!this.tech_.paused()) {
  600. this.mainSegmentLoader_.load();
  601. if (this.audioSegmentLoader_) {
  602. this.audioSegmentLoader_.load();
  603. }
  604. }
  605. }
  606. /**
  607. * A helper function for triggerring presence usage events once per source
  608. *
  609. * @private
  610. */
  611. triggerPresenceUsage_(main, media) {
  612. const mediaGroups = main.mediaGroups || {};
  613. let defaultDemuxed = true;
  614. const audioGroupKeys = Object.keys(mediaGroups.AUDIO);
  615. for (const mediaGroup in mediaGroups.AUDIO) {
  616. for (const label in mediaGroups.AUDIO[mediaGroup]) {
  617. const properties = mediaGroups.AUDIO[mediaGroup][label];
  618. if (!properties.uri) {
  619. defaultDemuxed = false;
  620. }
  621. }
  622. }
  623. if (defaultDemuxed) {
  624. this.tech_.trigger({type: 'usage', name: 'vhs-demuxed'});
  625. }
  626. if (Object.keys(mediaGroups.SUBTITLES).length) {
  627. this.tech_.trigger({type: 'usage', name: 'vhs-webvtt'});
  628. }
  629. if (Vhs.Playlist.isAes(media)) {
  630. this.tech_.trigger({type: 'usage', name: 'vhs-aes'});
  631. }
  632. if (audioGroupKeys.length &&
  633. Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) {
  634. this.tech_.trigger({type: 'usage', name: 'vhs-alternate-audio'});
  635. }
  636. if (this.useCueTags_) {
  637. this.tech_.trigger({type: 'usage', name: 'vhs-playlist-cue-tags'});
  638. }
  639. }
  640. shouldSwitchToMedia_(nextPlaylist) {
  641. const currentPlaylist = this.mainPlaylistLoader_.media() ||
  642. this.mainPlaylistLoader_.pendingMedia_;
  643. const currentTime = this.tech_.currentTime();
  644. const bufferLowWaterLine = this.bufferLowWaterLine();
  645. const bufferHighWaterLine = this.bufferHighWaterLine();
  646. const buffered = this.tech_.buffered();
  647. return shouldSwitchToMedia({
  648. buffered,
  649. currentTime,
  650. currentPlaylist,
  651. nextPlaylist,
  652. bufferLowWaterLine,
  653. bufferHighWaterLine,
  654. duration: this.duration(),
  655. bufferBasedABR: this.bufferBasedABR,
  656. log: this.logger_
  657. });
  658. }
  659. /**
  660. * Register event handlers on the segment loaders. A helper function
  661. * for construction time.
  662. *
  663. * @private
  664. */
  665. setupSegmentLoaderListeners_() {
  666. this.mainSegmentLoader_.on('bandwidthupdate', () => {
  667. // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's
  668. // useful to check to see if a rendition switch should be made.
  669. this.checkABR_('bandwidthupdate');
  670. this.tech_.trigger('bandwidthupdate');
  671. });
  672. this.mainSegmentLoader_.on('timeout', () => {
  673. if (this.bufferBasedABR) {
  674. // If a rendition change is needed, then it would've be done on `bandwidthupdate`.
  675. // Here the only consideration is that for buffer based ABR there's no guarantee
  676. // of an immediate switch (since the bandwidth is averaged with a timeout
  677. // bandwidth value of 1), so force a load on the segment loader to keep it going.
  678. this.mainSegmentLoader_.load();
  679. }
  680. });
  681. // `progress` events are not reliable enough of a bandwidth measure to trigger buffer
  682. // based ABR.
  683. if (!this.bufferBasedABR) {
  684. this.mainSegmentLoader_.on('progress', () => {
  685. this.trigger('progress');
  686. });
  687. }
  688. this.mainSegmentLoader_.on('error', () => {
  689. const error = this.mainSegmentLoader_.error();
  690. this.excludePlaylist({ playlistToExclude: error.playlist, error });
  691. });
  692. this.mainSegmentLoader_.on('appenderror', () => {
  693. this.error = this.mainSegmentLoader_.error_;
  694. this.trigger('error');
  695. });
  696. this.mainSegmentLoader_.on('syncinfoupdate', () => {
  697. this.onSyncInfoUpdate_();
  698. });
  699. this.mainSegmentLoader_.on('timestampoffset', () => {
  700. this.tech_.trigger({type: 'usage', name: 'vhs-timestamp-offset'});
  701. });
  702. this.audioSegmentLoader_.on('syncinfoupdate', () => {
  703. this.onSyncInfoUpdate_();
  704. });
  705. this.audioSegmentLoader_.on('appenderror', () => {
  706. this.error = this.audioSegmentLoader_.error_;
  707. this.trigger('error');
  708. });
  709. this.mainSegmentLoader_.on('ended', () => {
  710. this.logger_('main segment loader ended');
  711. this.onEndOfStream();
  712. });
  713. this.mainSegmentLoader_.on('earlyabort', (event) => {
  714. // never try to early abort with the new ABR algorithm
  715. if (this.bufferBasedABR) {
  716. return;
  717. }
  718. this.delegateLoaders_('all', ['abort']);
  719. this.excludePlaylist({
  720. error: {
  721. message: 'Aborted early because there isn\'t enough bandwidth to complete ' +
  722. 'the request without rebuffering.'
  723. },
  724. playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS
  725. });
  726. });
  727. const updateCodecs = () => {
  728. if (!this.sourceUpdater_.hasCreatedSourceBuffers()) {
  729. return this.tryToCreateSourceBuffers_();
  730. }
  731. const codecs = this.getCodecsOrExclude_();
  732. // no codecs means that the playlist was excluded
  733. if (!codecs) {
  734. return;
  735. }
  736. this.sourceUpdater_.addOrChangeSourceBuffers(codecs);
  737. };
  738. this.mainSegmentLoader_.on('trackinfo', updateCodecs);
  739. this.audioSegmentLoader_.on('trackinfo', updateCodecs);
  740. this.mainSegmentLoader_.on('fmp4', () => {
  741. if (!this.triggeredFmp4Usage) {
  742. this.tech_.trigger({type: 'usage', name: 'vhs-fmp4'});
  743. this.triggeredFmp4Usage = true;
  744. }
  745. });
  746. this.audioSegmentLoader_.on('fmp4', () => {
  747. if (!this.triggeredFmp4Usage) {
  748. this.tech_.trigger({type: 'usage', name: 'vhs-fmp4'});
  749. this.triggeredFmp4Usage = true;
  750. }
  751. });
  752. this.audioSegmentLoader_.on('ended', () => {
  753. this.logger_('audioSegmentLoader ended');
  754. this.onEndOfStream();
  755. });
  756. }
  757. mediaSecondsLoaded_() {
  758. return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded +
  759. this.mainSegmentLoader_.mediaSecondsLoaded);
  760. }
  761. /**
  762. * Call load on our SegmentLoaders
  763. */
  764. load() {
  765. this.mainSegmentLoader_.load();
  766. if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
  767. this.audioSegmentLoader_.load();
  768. }
  769. if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
  770. this.subtitleSegmentLoader_.load();
  771. }
  772. }
  773. /**
  774. * Re-tune playback quality level for the current player
  775. * conditions. This method will perform destructive actions like removing
  776. * already buffered content in order to readjust the currently active
  777. * playlist quickly. This is good for manual quality changes
  778. *
  779. * @private
  780. */
  781. fastQualityChange_(media = this.selectPlaylist()) {
  782. if (media === this.mainPlaylistLoader_.media()) {
  783. this.logger_('skipping fastQualityChange because new media is same as old');
  784. return;
  785. }
  786. this.switchMedia_(media, 'fast-quality');
  787. // Delete all buffered data to allow an immediate quality switch, then seek to give
  788. // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds
  789. // ahead is roughly the minimum that will accomplish this across a variety of content
  790. // in IE and Edge, but seeking in place is sufficient on all other browsers)
  791. // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/
  792. // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904
  793. this.mainSegmentLoader_.resetEverything(() => {
  794. // Since this is not a typical seek, we avoid the seekTo method which can cause segments
  795. // from the previously enabled rendition to load before the new playlist has finished loading
  796. if (videojs.browser.IE_VERSION || videojs.browser.IS_EDGE) {
  797. this.tech_.setCurrentTime(this.tech_.currentTime() + 0.04);
  798. } else {
  799. this.tech_.setCurrentTime(this.tech_.currentTime());
  800. }
  801. });
  802. // don't need to reset audio as it is reset when media changes
  803. }
  804. /**
  805. * Begin playback.
  806. */
  807. play() {
  808. if (this.setupFirstPlay()) {
  809. return;
  810. }
  811. if (this.tech_.ended()) {
  812. this.tech_.setCurrentTime(0);
  813. }
  814. if (this.hasPlayed_) {
  815. this.load();
  816. }
  817. const seekable = this.tech_.seekable();
  818. // if the viewer has paused and we fell out of the live window,
  819. // seek forward to the live point
  820. if (this.tech_.duration() === Infinity) {
  821. if (this.tech_.currentTime() < seekable.start(0)) {
  822. return this.tech_.setCurrentTime(seekable.end(seekable.length - 1));
  823. }
  824. }
  825. }
  826. /**
  827. * Seek to the latest media position if this is a live video and the
  828. * player and video are loaded and initialized.
  829. */
  830. setupFirstPlay() {
  831. const media = this.mainPlaylistLoader_.media();
  832. // Check that everything is ready to begin buffering for the first call to play
  833. // If 1) there is no active media
  834. // 2) the player is paused
  835. // 3) the first play has already been setup
  836. // then exit early
  837. if (!media || this.tech_.paused() || this.hasPlayed_) {
  838. return false;
  839. }
  840. // when the video is a live stream
  841. if (!media.endList) {
  842. const seekable = this.seekable();
  843. if (!seekable.length) {
  844. // without a seekable range, the player cannot seek to begin buffering at the live
  845. // point
  846. return false;
  847. }
  848. if (videojs.browser.IE_VERSION &&
  849. this.tech_.readyState() === 0) {
  850. // IE11 throws an InvalidStateError if you try to set currentTime while the
  851. // readyState is 0, so it must be delayed until the tech fires loadedmetadata.
  852. this.tech_.one('loadedmetadata', () => {
  853. this.trigger('firstplay');
  854. this.tech_.setCurrentTime(seekable.end(0));
  855. this.hasPlayed_ = true;
  856. });
  857. return false;
  858. }
  859. // trigger firstplay to inform the source handler to ignore the next seek event
  860. this.trigger('firstplay');
  861. // seek to the live point
  862. this.tech_.setCurrentTime(seekable.end(0));
  863. }
  864. this.hasPlayed_ = true;
  865. // we can begin loading now that everything is ready
  866. this.load();
  867. return true;
  868. }
  869. /**
  870. * handle the sourceopen event on the MediaSource
  871. *
  872. * @private
  873. */
  874. handleSourceOpen_() {
  875. // Only attempt to create the source buffer if none already exist.
  876. // handleSourceOpen is also called when we are "re-opening" a source buffer
  877. // after `endOfStream` has been called (in response to a seek for instance)
  878. this.tryToCreateSourceBuffers_();
  879. // if autoplay is enabled, begin playback. This is duplicative of
  880. // code in video.js but is required because play() must be invoked
  881. // *after* the media source has opened.
  882. if (this.tech_.autoplay()) {
  883. const playPromise = this.tech_.play();
  884. // Catch/silence error when a pause interrupts a play request
  885. // on browsers which return a promise
  886. if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') {
  887. playPromise.then(null, (e) => {});
  888. }
  889. }
  890. this.trigger('sourceopen');
  891. }
  892. /**
  893. * handle the sourceended event on the MediaSource
  894. *
  895. * @private
  896. */
  897. handleSourceEnded_() {
  898. if (!this.inbandTextTracks_.metadataTrack_) {
  899. return;
  900. }
  901. const cues = this.inbandTextTracks_.metadataTrack_.cues;
  902. if (!cues || !cues.length) {
  903. return;
  904. }
  905. const duration = this.duration();
  906. cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ?
  907. Number.MAX_VALUE : duration;
  908. }
  909. /**
  910. * handle the durationchange event on the MediaSource
  911. *
  912. * @private
  913. */
  914. handleDurationChange_() {
  915. this.tech_.trigger('durationchange');
  916. }
  917. /**
  918. * Calls endOfStream on the media source when all active stream types have called
  919. * endOfStream
  920. *
  921. * @param {string} streamType
  922. * Stream type of the segment loader that called endOfStream
  923. * @private
  924. */
  925. onEndOfStream() {
  926. let isEndOfStream = this.mainSegmentLoader_.ended_;
  927. if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
  928. const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_();
  929. // if the audio playlist loader exists, then alternate audio is active
  930. if (!mainMediaInfo || mainMediaInfo.hasVideo) {
  931. // if we do not know if the main segment loader contains video yet or if we
  932. // definitively know the main segment loader contains video, then we need to wait
  933. // for both main and audio segment loaders to call endOfStream
  934. isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_;
  935. } else {
  936. // otherwise just rely on the audio loader
  937. isEndOfStream = this.audioSegmentLoader_.ended_;
  938. }
  939. }
  940. if (!isEndOfStream) {
  941. return;
  942. }
  943. this.stopABRTimer_();
  944. this.sourceUpdater_.endOfStream();
  945. }
  946. /**
  947. * Check if a playlist has stopped being updated
  948. *
  949. * @param {Object} playlist the media playlist object
  950. * @return {boolean} whether the playlist has stopped being updated or not
  951. */
  952. stuckAtPlaylistEnd_(playlist) {
  953. const seekable = this.seekable();
  954. if (!seekable.length) {
  955. // playlist doesn't have enough information to determine whether we are stuck
  956. return false;
  957. }
  958. const expired =
  959. this.syncController_.getExpiredTime(playlist, this.duration());
  960. if (expired === null) {
  961. return false;
  962. }
  963. // does not use the safe live end to calculate playlist end, since we
  964. // don't want to say we are stuck while there is still content
  965. const absolutePlaylistEnd = Vhs.Playlist.playlistEnd(playlist, expired);
  966. const currentTime = this.tech_.currentTime();
  967. const buffered = this.tech_.buffered();
  968. if (!buffered.length) {
  969. // return true if the playhead reached the absolute end of the playlist
  970. return absolutePlaylistEnd - currentTime <= Ranges.SAFE_TIME_DELTA;
  971. }
  972. const bufferedEnd = buffered.end(buffered.length - 1);
  973. // return true if there is too little buffer left and buffer has reached absolute
  974. // end of playlist
  975. return bufferedEnd - currentTime <= Ranges.SAFE_TIME_DELTA &&
  976. absolutePlaylistEnd - bufferedEnd <= Ranges.SAFE_TIME_DELTA;
  977. }
  978. /**
  979. * Exclude a playlist for a set amount of time, making it unavailable for selection by
  980. * the rendition selection algorithm, then force a new playlist (rendition) selection.
  981. *
  982. * @param {Object=} playlistToExclude
  983. * the playlist to exclude, defaults to the currently selected playlist
  984. * @param {Object=} error
  985. * an optional error
  986. * @param {number=} playlistExclusionDuration
  987. * an optional number of seconds to exclude the playlist
  988. */
  989. excludePlaylist({
  990. playlistToExclude = this.mainPlaylistLoader_.media(),
  991. error = {},
  992. playlistExclusionDuration
  993. }) {
  994. // If the `error` was generated by the playlist loader, it will contain
  995. // the playlist we were trying to load (but failed) and that should be
  996. // excluded instead of the currently selected playlist which is likely
  997. // out-of-date in this scenario
  998. playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media();
  999. playlistExclusionDuration = playlistExclusionDuration ||
  1000. error.playlistExclusionDuration ||
  1001. this.playlistExclusionDuration;
  1002. // If there is no current playlist, then an error occurred while we were
  1003. // trying to load the main OR while we were disposing of the tech
  1004. if (!playlistToExclude) {
  1005. this.error = error;
  1006. if (this.mediaSource.readyState !== 'open') {
  1007. this.trigger('error');
  1008. } else {
  1009. this.sourceUpdater_.endOfStream('network');
  1010. }
  1011. return;
  1012. }
  1013. playlistToExclude.playlistErrors_++;
  1014. const playlists = this.mainPlaylistLoader_.main.playlists;
  1015. const enabledPlaylists = playlists.filter(isEnabled);
  1016. const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude;
  1017. // Don't exclude the only playlist unless it was excluded
  1018. // forever
  1019. if (playlists.length === 1 && playlistExclusionDuration !== Infinity) {
  1020. videojs.log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` +
  1021. 'Trying again since it is the only playlist.');
  1022. this.tech_.trigger('retryplaylist');
  1023. // if this is a final rendition, we should delay
  1024. return this.mainPlaylistLoader_.load(isFinalRendition);
  1025. }
  1026. if (isFinalRendition) {
  1027. // Since we're on the final non-excluded playlist, and we're about to exclude
  1028. // it, instead of erring the player or retrying this playlist, clear out the current
  1029. // exclusion list. This allows other playlists to be attempted in case any have been
  1030. // fixed.
  1031. let reincluded = false;
  1032. playlists.forEach((playlist) => {
  1033. // skip current playlist which is about to be excluded
  1034. if (playlist === playlistToExclude) {
  1035. return;
  1036. }
  1037. const excludeUntil = playlist.excludeUntil;
  1038. // a playlist cannot be reincluded if it wasn't excluded to begin with.
  1039. if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) {
  1040. reincluded = true;
  1041. delete playlist.excludeUntil;
  1042. }
  1043. });
  1044. if (reincluded) {
  1045. videojs.log.warn('Removing other playlists from the exclusion list because the last ' +
  1046. 'rendition is about to be excluded.');
  1047. // Technically we are retrying a playlist, in that we are simply retrying a previous
  1048. // playlist. This is needed for users relying on the retryplaylist event to catch a
  1049. // case where the player might be stuck and looping through "dead" playlists.
  1050. this.tech_.trigger('retryplaylist');
  1051. }
  1052. }
  1053. // Exclude this playlist
  1054. let excludeUntil;
  1055. if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) {
  1056. excludeUntil = Infinity;
  1057. } else {
  1058. excludeUntil = Date.now() + (playlistExclusionDuration * 1000);
  1059. }
  1060. playlistToExclude.excludeUntil = excludeUntil;
  1061. if (error.reason) {
  1062. playlistToExclude.lastExcludeReason_ = error.reason;
  1063. }
  1064. this.tech_.trigger('excludeplaylist');
  1065. this.tech_.trigger({type: 'usage', name: 'vhs-rendition-excluded'});
  1066. // TODO: only load a new playlist if we're excluding the current playlist
  1067. // If this function was called with a playlist that's not the current active playlist
  1068. // (e.g., media().id !== playlistToExclude.id),
  1069. // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist.
  1070. const nextPlaylist = this.selectPlaylist();
  1071. if (!nextPlaylist) {
  1072. this.error = 'Playback cannot continue. No available working or supported playlists.';
  1073. this.trigger('error');
  1074. return;
  1075. }
  1076. const logFn = error.internal ? this.logger_ : videojs.log.warn;
  1077. const errorMessage = error.message ? (' ' + error.message) : '';
  1078. logFn(`${(error.internal ? 'Internal problem' : 'Problem')} encountered with playlist ${playlistToExclude.id}.` +
  1079. `${errorMessage} Switching to playlist ${nextPlaylist.id}.`);
  1080. // if audio group changed reset audio loaders
  1081. if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) {
  1082. this.delegateLoaders_('audio', ['abort', 'pause']);
  1083. }
  1084. // if subtitle group changed reset subtitle loaders
  1085. if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) {
  1086. this.delegateLoaders_('subtitle', ['abort', 'pause']);
  1087. }
  1088. this.delegateLoaders_('main', ['abort', 'pause']);
  1089. const delayDuration = (nextPlaylist.targetDuration / 2) * 1000 || 5 * 1000;
  1090. const shouldDelay = typeof nextPlaylist.lastRequest === 'number' &&
  1091. (Date.now() - nextPlaylist.lastRequest) <= delayDuration;
  1092. // delay if it's a final rendition or if the last refresh is sooner than half targetDuration
  1093. return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay);
  1094. }
  1095. /**
  1096. * Pause all segment/playlist loaders
  1097. */
  1098. pauseLoading() {
  1099. this.delegateLoaders_('all', ['abort', 'pause']);
  1100. this.stopABRTimer_();
  1101. }
  1102. /**
  1103. * Call a set of functions in order on playlist loaders, segment loaders,
  1104. * or both types of loaders.
  1105. *
  1106. * @param {string} filter
  1107. * Filter loaders that should call fnNames using a string. Can be:
  1108. * * all - run on all loaders
  1109. * * audio - run on all audio loaders
  1110. * * subtitle - run on all subtitle loaders
  1111. * * main - run on the main loaders
  1112. *
  1113. * @param {Array|string} fnNames
  1114. * A string or array of function names to call.
  1115. */
  1116. delegateLoaders_(filter, fnNames) {
  1117. const loaders = [];
  1118. const dontFilterPlaylist = filter === 'all';
  1119. if (dontFilterPlaylist || filter === 'main') {
  1120. loaders.push(this.mainPlaylistLoader_);
  1121. }
  1122. const mediaTypes = [];
  1123. if (dontFilterPlaylist || filter === 'audio') {
  1124. mediaTypes.push('AUDIO');
  1125. }
  1126. if (dontFilterPlaylist || filter === 'subtitle') {
  1127. mediaTypes.push('CLOSED-CAPTIONS');
  1128. mediaTypes.push('SUBTITLES');
  1129. }
  1130. mediaTypes.forEach((mediaType) => {
  1131. const loader = this.mediaTypes_[mediaType] &&
  1132. this.mediaTypes_[mediaType].activePlaylistLoader;
  1133. if (loader) {
  1134. loaders.push(loader);
  1135. }
  1136. });
  1137. ['main', 'audio', 'subtitle'].forEach((name) => {
  1138. const loader = this[`${name}SegmentLoader_`];
  1139. if (loader && (filter === name || filter === 'all')) {
  1140. loaders.push(loader);
  1141. }
  1142. });
  1143. loaders.forEach((loader) => fnNames.forEach((fnName) => {
  1144. if (typeof loader[fnName] === 'function') {
  1145. loader[fnName]();
  1146. }
  1147. }));
  1148. }
  1149. /**
  1150. * set the current time on all segment loaders
  1151. *
  1152. * @param {TimeRange} currentTime the current time to set
  1153. * @return {TimeRange} the current time
  1154. */
  1155. setCurrentTime(currentTime) {
  1156. const buffered = Ranges.findRange(this.tech_.buffered(), currentTime);
  1157. if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) {
  1158. // return immediately if the metadata is not ready yet
  1159. return 0;
  1160. }
  1161. // it's clearly an edge-case but don't thrown an error if asked to
  1162. // seek within an empty playlist
  1163. if (!this.mainPlaylistLoader_.media().segments) {
  1164. return 0;
  1165. }
  1166. // if the seek location is already buffered, continue buffering as usual
  1167. if (buffered && buffered.length) {
  1168. return currentTime;
  1169. }
  1170. // cancel outstanding requests so we begin buffering at the new
  1171. // location
  1172. this.mainSegmentLoader_.resetEverything();
  1173. this.mainSegmentLoader_.abort();
  1174. if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
  1175. this.audioSegmentLoader_.resetEverything();
  1176. this.audioSegmentLoader_.abort();
  1177. }
  1178. if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
  1179. this.subtitleSegmentLoader_.resetEverything();
  1180. this.subtitleSegmentLoader_.abort();
  1181. }
  1182. // start segment loader loading in case they are paused
  1183. this.load();
  1184. }
  1185. /**
  1186. * get the current duration
  1187. *
  1188. * @return {TimeRange} the duration
  1189. */
  1190. duration() {
  1191. if (!this.mainPlaylistLoader_) {
  1192. return 0;
  1193. }
  1194. const media = this.mainPlaylistLoader_.media();
  1195. if (!media) {
  1196. // no playlists loaded yet, so can't determine a duration
  1197. return 0;
  1198. }
  1199. // Don't rely on the media source for duration in the case of a live playlist since
  1200. // setting the native MediaSource's duration to infinity ends up with consequences to
  1201. // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
  1202. //
  1203. // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
  1204. // however, few browsers have support for setLiveSeekableRange()
  1205. // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
  1206. //
  1207. // Until a time when the duration of the media source can be set to infinity, and a
  1208. // seekable range specified across browsers, just return Infinity.
  1209. if (!media.endList) {
  1210. return Infinity;
  1211. }
  1212. // Since this is a VOD video, it is safe to rely on the media source's duration (if
  1213. // available). If it's not available, fall back to a playlist-calculated estimate.
  1214. if (this.mediaSource) {
  1215. return this.mediaSource.duration;
  1216. }
  1217. return Vhs.Playlist.duration(media);
  1218. }
  1219. /**
  1220. * check the seekable range
  1221. *
  1222. * @return {TimeRange} the seekable range
  1223. */
  1224. seekable() {
  1225. return this.seekable_;
  1226. }
  1227. onSyncInfoUpdate_() {
  1228. let audioSeekable;
  1229. // TODO check for creation of both source buffers before updating seekable
  1230. //
  1231. // A fix was made to this function where a check for
  1232. // this.sourceUpdater_.hasCreatedSourceBuffers
  1233. // was added to ensure that both source buffers were created before seekable was
  1234. // updated. However, it originally had a bug where it was checking for a true and
  1235. // returning early instead of checking for false. Setting it to check for false to
  1236. // return early though created other issues. A call to play() would check for seekable
  1237. // end without verifying that a seekable range was present. In addition, even checking
  1238. // for that didn't solve some issues, as handleFirstPlay is sometimes worked around
  1239. // due to a media update calling load on the segment loaders, skipping a seek to live,
  1240. // thereby starting live streams at the beginning of the stream rather than at the end.
  1241. //
  1242. // This conditional should be fixed to wait for the creation of two source buffers at
  1243. // the same time as the other sections of code are fixed to properly seek to live and
  1244. // not throw an error due to checking for a seekable end when no seekable range exists.
  1245. //
  1246. // For now, fall back to the older behavior, with the understanding that the seekable
  1247. // range may not be completely correct, leading to a suboptimal initial live point.
  1248. if (!this.mainPlaylistLoader_) {
  1249. return;
  1250. }
  1251. let media = this.mainPlaylistLoader_.media();
  1252. if (!media) {
  1253. return;
  1254. }
  1255. let expired = this.syncController_.getExpiredTime(media, this.duration());
  1256. if (expired === null) {
  1257. // not enough information to update seekable
  1258. return;
  1259. }
  1260. const main = this.mainPlaylistLoader_.main;
  1261. const mainSeekable = Vhs.Playlist.seekable(
  1262. media,
  1263. expired,
  1264. Vhs.Playlist.liveEdgeDelay(main, media)
  1265. );
  1266. if (mainSeekable.length === 0) {
  1267. return;
  1268. }
  1269. if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
  1270. media = this.mediaTypes_.AUDIO.activePlaylistLoader.media();
  1271. expired = this.syncController_.getExpiredTime(media, this.duration());
  1272. if (expired === null) {
  1273. return;
  1274. }
  1275. audioSeekable = Vhs.Playlist.seekable(
  1276. media,
  1277. expired,
  1278. Vhs.Playlist.liveEdgeDelay(main, media)
  1279. );
  1280. if (audioSeekable.length === 0) {
  1281. return;
  1282. }
  1283. }
  1284. let oldEnd;
  1285. let oldStart;
  1286. if (this.seekable_ && this.seekable_.length) {
  1287. oldEnd = this.seekable_.end(0);
  1288. oldStart = this.seekable_.start(0);
  1289. }
  1290. if (!audioSeekable) {
  1291. // seekable has been calculated based on buffering video data so it
  1292. // can be returned directly
  1293. this.seekable_ = mainSeekable;
  1294. } else if (audioSeekable.start(0) > mainSeekable.end(0) ||
  1295. mainSeekable.start(0) > audioSeekable.end(0)) {
  1296. // seekables are pretty far off, rely on main
  1297. this.seekable_ = mainSeekable;
  1298. } else {
  1299. this.seekable_ = createTimeRanges([[
  1300. (audioSeekable.start(0) > mainSeekable.start(0)) ? audioSeekable.start(0) :
  1301. mainSeekable.start(0),
  1302. (audioSeekable.end(0) < mainSeekable.end(0)) ? audioSeekable.end(0) :
  1303. mainSeekable.end(0)
  1304. ]]);
  1305. }
  1306. // seekable is the same as last time
  1307. if (this.seekable_ && this.seekable_.length) {
  1308. if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) {
  1309. return;
  1310. }
  1311. }
  1312. this.logger_(`seekable updated [${Ranges.printableRange(this.seekable_)}]`);
  1313. this.tech_.trigger('seekablechanged');
  1314. }
  1315. /**
  1316. * Update the player duration
  1317. */
  1318. updateDuration(isLive) {
  1319. if (this.updateDuration_) {
  1320. this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
  1321. this.updateDuration_ = null;
  1322. }
  1323. if (this.mediaSource.readyState !== 'open') {
  1324. this.updateDuration_ = this.updateDuration.bind(this, isLive);
  1325. this.mediaSource.addEventListener('sourceopen', this.updateDuration_);
  1326. return;
  1327. }
  1328. if (isLive) {
  1329. const seekable = this.seekable();
  1330. if (!seekable.length) {
  1331. return;
  1332. }
  1333. // Even in the case of a live playlist, the native MediaSource's duration should not
  1334. // be set to Infinity (even though this would be expected for a live playlist), since
  1335. // setting the native MediaSource's duration to infinity ends up with consequences to
  1336. // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details.
  1337. //
  1338. // This is resolved in the spec by https://github.com/w3c/media-source/pull/92,
  1339. // however, few browsers have support for setLiveSeekableRange()
  1340. // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange
  1341. //
  1342. // Until a time when the duration of the media source can be set to infinity, and a
  1343. // seekable range specified across browsers, the duration should be greater than or
  1344. // equal to the last possible seekable value.
  1345. // MediaSource duration starts as NaN
  1346. // It is possible (and probable) that this case will never be reached for many
  1347. // sources, since the MediaSource reports duration as the highest value without
  1348. // accounting for timestamp offset. For example, if the timestamp offset is -100 and
  1349. // we buffered times 0 to 100 with real times of 100 to 200, even though current
  1350. // time will be between 0 and 100, the native media source may report the duration
  1351. // as 200. However, since we report duration separate from the media source (as
  1352. // Infinity), and as long as the native media source duration value is greater than
  1353. // our reported seekable range, seeks will work as expected. The large number as
  1354. // duration for live is actually a strategy used by some players to work around the
  1355. // issue of live seekable ranges cited above.
  1356. if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) {
  1357. this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1));
  1358. }
  1359. return;
  1360. }
  1361. const buffered = this.tech_.buffered();
  1362. let duration = Vhs.Playlist.duration(this.mainPlaylistLoader_.media());
  1363. if (buffered.length > 0) {
  1364. duration = Math.max(duration, buffered.end(buffered.length - 1));
  1365. }
  1366. if (this.mediaSource.duration !== duration) {
  1367. this.sourceUpdater_.setDuration(duration);
  1368. }
  1369. }
  1370. /**
  1371. * dispose of the PlaylistController and everything
  1372. * that it controls
  1373. */
  1374. dispose() {
  1375. this.trigger('dispose');
  1376. this.decrypter_.terminate();
  1377. this.mainPlaylistLoader_.dispose();
  1378. this.mainSegmentLoader_.dispose();
  1379. if (this.loadOnPlay_) {
  1380. this.tech_.off('play', this.loadOnPlay_);
  1381. }
  1382. ['AUDIO', 'SUBTITLES'].forEach((type) => {
  1383. const groups = this.mediaTypes_[type].groups;
  1384. for (const id in groups) {
  1385. groups[id].forEach((group) => {
  1386. if (group.playlistLoader) {
  1387. group.playlistLoader.dispose();
  1388. }
  1389. });
  1390. }
  1391. });
  1392. this.audioSegmentLoader_.dispose();
  1393. this.subtitleSegmentLoader_.dispose();
  1394. this.sourceUpdater_.dispose();
  1395. this.timelineChangeController_.dispose();
  1396. this.stopABRTimer_();
  1397. if (this.updateDuration_) {
  1398. this.mediaSource.removeEventListener('sourceopen', this.updateDuration_);
  1399. }
  1400. this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_);
  1401. // load the media source into the player
  1402. this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_);
  1403. this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_);
  1404. this.off();
  1405. }
  1406. /**
  1407. * return the main playlist object if we have one
  1408. *
  1409. * @return {Object} the main playlist object that we parsed
  1410. */
  1411. main() {
  1412. return this.mainPlaylistLoader_.main;
  1413. }
  1414. /**
  1415. * return the currently selected playlist
  1416. *
  1417. * @return {Object} the currently selected playlist object that we parsed
  1418. */
  1419. media() {
  1420. // playlist loader will not return media if it has not been fully loaded
  1421. return this.mainPlaylistLoader_.media() || this.initialMedia_;
  1422. }
  1423. areMediaTypesKnown_() {
  1424. const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
  1425. const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_();
  1426. // if we are not using an audio loader, then we have audio media info
  1427. // otherwise check on the segment loader.
  1428. const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_();
  1429. // one or both loaders has not loaded sufficently to get codecs
  1430. if (!hasMainMediaInfo || !hasAudioMediaInfo) {
  1431. return false;
  1432. }
  1433. return true;
  1434. }
  1435. getCodecsOrExclude_() {
  1436. const media = {
  1437. main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {},
  1438. audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {}
  1439. };
  1440. const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media();
  1441. // set "main" media equal to video
  1442. media.video = media.main;
  1443. const playlistCodecs = codecsForPlaylist(this.main(), playlist);
  1444. const codecs = {};
  1445. const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader;
  1446. if (media.main.hasVideo) {
  1447. codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC;
  1448. }
  1449. if (media.main.isMuxed) {
  1450. codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`;
  1451. }
  1452. if ((media.main.hasAudio && !media.main.isMuxed) || media.audio.hasAudio || usingAudioLoader) {
  1453. codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC;
  1454. // set audio isFmp4 so we use the correct "supports" function below
  1455. media.audio.isFmp4 = (media.main.hasAudio && !media.main.isMuxed) ? media.main.isFmp4 : media.audio.isFmp4;
  1456. }
  1457. // no codecs, no playback.
  1458. if (!codecs.audio && !codecs.video) {
  1459. this.excludePlaylist({
  1460. playlistToExclude: playlist,
  1461. error: { message: 'Could not determine codecs for playlist.' },
  1462. playlistExclusionDuration: Infinity
  1463. });
  1464. return;
  1465. }
  1466. // fmp4 relies on browser support, while ts relies on muxer support
  1467. const supportFunction = (isFmp4, codec) => (isFmp4 ? browserSupportsCodec(codec) : muxerSupportsCodec(codec));
  1468. const unsupportedCodecs = {};
  1469. let unsupportedAudio;
  1470. ['video', 'audio'].forEach(function(type) {
  1471. if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) {
  1472. const supporter = media[type].isFmp4 ? 'browser' : 'muxer';
  1473. unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || [];
  1474. unsupportedCodecs[supporter].push(codecs[type]);
  1475. if (type === 'audio') {
  1476. unsupportedAudio = supporter;
  1477. }
  1478. }
  1479. });
  1480. if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) {
  1481. const audioGroup = playlist.attributes.AUDIO;
  1482. this.main().playlists.forEach(variant => {
  1483. const variantAudioGroup = variant.attributes && variant.attributes.AUDIO;
  1484. if (variantAudioGroup === audioGroup && variant !== playlist) {
  1485. variant.excludeUntil = Infinity;
  1486. }
  1487. });
  1488. this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`);
  1489. }
  1490. // if we have any unsupported codecs exclude this playlist.
  1491. if (Object.keys(unsupportedCodecs).length) {
  1492. const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => {
  1493. if (acc) {
  1494. acc += ', ';
  1495. }
  1496. acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`;
  1497. return acc;
  1498. }, '') + '.';
  1499. this.excludePlaylist({
  1500. playlistToExclude: playlist,
  1501. error: {
  1502. internal: true,
  1503. message
  1504. },
  1505. playlistExclusionDuration: Infinity
  1506. });
  1507. return;
  1508. }
  1509. // check if codec switching is happening
  1510. if (
  1511. this.sourceUpdater_.hasCreatedSourceBuffers() &&
  1512. !this.sourceUpdater_.canChangeType()
  1513. ) {
  1514. const switchMessages = [];
  1515. ['video', 'audio'].forEach((type) => {
  1516. const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type;
  1517. const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type;
  1518. if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) {
  1519. switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`);
  1520. }
  1521. });
  1522. if (switchMessages.length) {
  1523. this.excludePlaylist({
  1524. playlistToExclude: playlist,
  1525. error: {
  1526. message: `Codec switching not supported: ${switchMessages.join(', ')}.`,
  1527. internal: true
  1528. },
  1529. playlistExclusionDuration: Infinity
  1530. });
  1531. return;
  1532. }
  1533. }
  1534. // TODO: when using the muxer shouldn't we just return
  1535. // the codecs that the muxer outputs?
  1536. return codecs;
  1537. }
  1538. /**
  1539. * Create source buffers and exlude any incompatible renditions.
  1540. *
  1541. * @private
  1542. */
  1543. tryToCreateSourceBuffers_() {
  1544. // media source is not ready yet or sourceBuffers are already
  1545. // created.
  1546. if (
  1547. this.mediaSource.readyState !== 'open' ||
  1548. this.sourceUpdater_.hasCreatedSourceBuffers()
  1549. ) {
  1550. return;
  1551. }
  1552. if (!this.areMediaTypesKnown_()) {
  1553. return;
  1554. }
  1555. const codecs = this.getCodecsOrExclude_();
  1556. // no codecs means that the playlist was excluded
  1557. if (!codecs) {
  1558. return;
  1559. }
  1560. this.sourceUpdater_.createSourceBuffers(codecs);
  1561. const codecString = [codecs.video, codecs.audio].filter(Boolean).join(',');
  1562. this.excludeIncompatibleVariants_(codecString);
  1563. }
  1564. /**
  1565. * Excludes playlists with codecs that are unsupported by the muxer and browser.
  1566. */
  1567. excludeUnsupportedVariants_() {
  1568. const playlists = this.main().playlists;
  1569. const ids = [];
  1570. // TODO: why don't we have a property to loop through all
  1571. // playlist? Why did we ever mix indexes and keys?
  1572. Object.keys(playlists).forEach(key => {
  1573. const variant = playlists[key];
  1574. // check if we already processed this playlist.
  1575. if (ids.indexOf(variant.id) !== -1) {
  1576. return;
  1577. }
  1578. ids.push(variant.id);
  1579. const codecs = codecsForPlaylist(this.main, variant);
  1580. const unsupported = [];
  1581. if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio)) {
  1582. unsupported.push(`audio codec ${codecs.audio}`);
  1583. }
  1584. if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video)) {
  1585. unsupported.push(`video codec ${codecs.video}`);
  1586. }
  1587. if (codecs.text && codecs.text === 'stpp.ttml.im1t') {
  1588. unsupported.push(`text codec ${codecs.text}`);
  1589. }
  1590. if (unsupported.length) {
  1591. variant.excludeUntil = Infinity;
  1592. this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`);
  1593. }
  1594. });
  1595. }
  1596. /**
  1597. * Exclude playlists that are known to be codec or
  1598. * stream-incompatible with the SourceBuffer configuration. For
  1599. * instance, Media Source Extensions would cause the video element to
  1600. * stall waiting for video data if you switched from a variant with
  1601. * video and audio to an audio-only one.
  1602. *
  1603. * @param {Object} media a media playlist compatible with the current
  1604. * set of SourceBuffers. Variants in the current main playlist that
  1605. * do not appear to have compatible codec or stream configurations
  1606. * will be excluded from the default playlist selection algorithm
  1607. * indefinitely.
  1608. * @private
  1609. */
  1610. excludeIncompatibleVariants_(codecString) {
  1611. const ids = [];
  1612. const playlists = this.main().playlists;
  1613. const codecs = unwrapCodecList(parseCodecs(codecString));
  1614. const codecCount_ = codecCount(codecs);
  1615. const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null;
  1616. const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null;
  1617. Object.keys(playlists).forEach((key) => {
  1618. const variant = playlists[key];
  1619. // check if we already processed this playlist.
  1620. // or it if it is already excluded forever.
  1621. if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) {
  1622. return;
  1623. }
  1624. ids.push(variant.id);
  1625. const exclusionReasons = [];
  1626. // get codecs from the playlist for this variant
  1627. const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant);
  1628. const variantCodecCount = codecCount(variantCodecs);
  1629. // if no codecs are listed, we cannot determine that this
  1630. // variant is incompatible. Wait for mux.js to probe
  1631. if (!variantCodecs.audio && !variantCodecs.video) {
  1632. return;
  1633. }
  1634. // TODO: we can support this by removing the
  1635. // old media source and creating a new one, but it will take some work.
  1636. // The number of streams cannot change
  1637. if (variantCodecCount !== codecCount_) {
  1638. exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`);
  1639. }
  1640. // only exclude playlists by codec change, if codecs cannot switch
  1641. // during playback.
  1642. if (!this.sourceUpdater_.canChangeType()) {
  1643. const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null;
  1644. const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null;
  1645. // the video codec cannot change
  1646. if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) {
  1647. exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`);
  1648. }
  1649. // the audio codec cannot change
  1650. if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) {
  1651. exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`);
  1652. }
  1653. }
  1654. if (exclusionReasons.length) {
  1655. variant.excludeUntil = Infinity;
  1656. this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`);
  1657. }
  1658. });
  1659. }
  1660. updateAdCues_(media) {
  1661. let offset = 0;
  1662. const seekable = this.seekable();
  1663. if (seekable.length) {
  1664. offset = seekable.start(0);
  1665. }
  1666. updateAdCues(media, this.cueTagsTrack_, offset);
  1667. }
  1668. /**
  1669. * Calculates the desired forward buffer length based on current time
  1670. *
  1671. * @return {number} Desired forward buffer length in seconds
  1672. */
  1673. goalBufferLength() {
  1674. const currentTime = this.tech_.currentTime();
  1675. const initial = Config.GOAL_BUFFER_LENGTH;
  1676. const rate = Config.GOAL_BUFFER_LENGTH_RATE;
  1677. const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH);
  1678. return Math.min(initial + currentTime * rate, max);
  1679. }
  1680. /**
  1681. * Calculates the desired buffer low water line based on current time
  1682. *
  1683. * @return {number} Desired buffer low water line in seconds
  1684. */
  1685. bufferLowWaterLine() {
  1686. const currentTime = this.tech_.currentTime();
  1687. const initial = Config.BUFFER_LOW_WATER_LINE;
  1688. const rate = Config.BUFFER_LOW_WATER_LINE_RATE;
  1689. const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE);
  1690. const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE);
  1691. return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max);
  1692. }
  1693. bufferHighWaterLine() {
  1694. return Config.BUFFER_HIGH_WATER_LINE;
  1695. }
  1696. }