playlist-controller.js 83 KB

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