1. 1 : import Component from './component.js';
  2. 2 : import {merge} from './utils/obj.js';
  3. 3 : import window from 'global/window';
  4. 4 : import * as Fn from './utils/fn.js';
  5. 5 :
  6. 6 : const defaults = {
  7. 7 : trackingThreshold: 20,
  8. 8 : liveTolerance: 15
  9. 9 : };
  10. 10 :
  11. 11 : /*
  12. 12 : track when we are at the live edge, and other helpers for live playback */
  13. 13 :
  14. 14 : /**
  15. 15 : * A class for checking live current time and determining when the player
  16. 16 : * is at or behind the live edge.
  17. 17 : */
  18. 18 : class LiveTracker extends Component {
  19. 19 :
  20. 20 : /**
  21. 21 : * Creates an instance of this class.
  22. 22 : *
  23. 23 : * @param { import('./player').default } player
  24. 24 : * The `Player` that this class should be attached to.
  25. 25 : *
  26. 26 : * @param {Object} [options]
  27. 27 : * The key/value store of player options.
  28. 28 : *
  29. 29 : * @param {number} [options.trackingThreshold=20]
  30. 30 : * Number of seconds of live window (seekableEnd - seekableStart) that
  31. 31 : * media needs to have before the liveui will be shown.
  32. 32 : *
  33. 33 : * @param {number} [options.liveTolerance=15]
  34. 34 : * Number of seconds behind live that we have to be
  35. 35 : * before we will be considered non-live. Note that this will only
  36. 36 : * be used when playing at the live edge. This allows large seekable end
  37. 37 : * changes to not effect whether we are live or not.
  38. 38 : */
  39. 39 : constructor(player, options) {
  40. 40 : // LiveTracker does not need an element
  41. 41 : const options_ = merge(defaults, options, {createEl: false});
  42. 42 :
  43. 43 : super(player, options_);
  44. 44 :
  45. 45 : this.trackLiveHandler_ = () => this.trackLive_();
  46. 46 : this.handlePlay_ = (e) => this.handlePlay(e);
  47. 47 : this.handleFirstTimeupdate_ = (e) => this.handleFirstTimeupdate(e);
  48. 48 : this.handleSeeked_ = (e) => this.handleSeeked(e);
  49. 49 : this.seekToLiveEdge_ = (e) => this.seekToLiveEdge(e);
  50. 50 :
  51. 51 : this.reset_();
  52. 52 :
  53. 53 : this.on(this.player_, 'durationchange', (e) => this.handleDurationchange(e));
  54. 54 : // we should try to toggle tracking on canplay as native playback engines, like Safari
  55. 55 : // may not have the proper values for things like seekableEnd until then
  56. 56 : this.on(this.player_, 'canplay', () => this.toggleTracking());
  57. 57 : }
  58. 58 :
  59. 59 : /**
  60. 60 : * all the functionality for tracking when seek end changes
  61. 61 : * and for tracking how far past seek end we should be
  62. 62 : */
  63. 63 : trackLive_() {
  64. 64 : const seekable = this.player_.seekable();
  65. 65 :
  66. 66 : // skip undefined seekable
  67. 67 : if (!seekable || !seekable.length) {
  68. 68 : return;
  69. 69 : }
  70. 70 :
  71. 71 : const newTime = Number(window.performance.now().toFixed(4));
  72. 72 : const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
  73. 73 :
  74. 74 : this.lastTime_ = newTime;
  75. 75 :
  76. 76 : this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
  77. 77 :
  78. 78 : const liveCurrentTime = this.liveCurrentTime();
  79. 79 : const currentTime = this.player_.currentTime();
  80. 80 :
  81. 81 : // we are behind live if any are true
  82. 82 : // 1. the player is paused
  83. 83 : // 2. the user seeked to a location 2 seconds away from live
  84. 84 : // 3. the difference between live and current time is greater
  85. 85 : // liveTolerance which defaults to 15s
  86. 86 : let isBehind = this.player_.paused() || this.seekedBehindLive_ ||
  87. 87 : Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
  88. 88 :
  89. 89 : // we cannot be behind if
  90. 90 : // 1. until we have not seen a timeupdate yet
  91. 91 : // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
  92. 92 : if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
  93. 93 : isBehind = false;
  94. 94 : }
  95. 95 :
  96. 96 : if (isBehind !== this.behindLiveEdge_) {
  97. 97 : this.behindLiveEdge_ = isBehind;
  98. 98 : this.trigger('liveedgechange');
  99. 99 : }
  100. 100 : }
  101. 101 :
  102. 102 : /**
  103. 103 : * handle a durationchange event on the player
  104. 104 : * and start/stop tracking accordingly.
  105. 105 : */
  106. 106 : handleDurationchange() {
  107. 107 : this.toggleTracking();
  108. 108 : }
  109. 109 :
  110. 110 : /**
  111. 111 : * start/stop tracking
  112. 112 : */
  113. 113 : toggleTracking() {
  114. 114 : if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
  115. 115 : if (this.player_.options_.liveui) {
  116. 116 : this.player_.addClass('vjs-liveui');
  117. 117 : }
  118. 118 : this.startTracking();
  119. 119 : } else {
  120. 120 : this.player_.removeClass('vjs-liveui');
  121. 121 : this.stopTracking();
  122. 122 : }
  123. 123 : }
  124. 124 :
  125. 125 : /**
  126. 126 : * start tracking live playback
  127. 127 : */
  128. 128 : startTracking() {
  129. 129 : if (this.isTracking()) {
  130. 130 : return;
  131. 131 : }
  132. 132 :
  133. 133 : // If we haven't seen a timeupdate, we need to check whether playback
  134. 134 : // began before this component started tracking. This can happen commonly
  135. 135 : // when using autoplay.
  136. 136 : if (!this.timeupdateSeen_) {
  137. 137 : this.timeupdateSeen_ = this.player_.hasStarted();
  138. 138 : }
  139. 139 :
  140. 140 : this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, Fn.UPDATE_REFRESH_INTERVAL);
  141. 141 : this.trackLive_();
  142. 142 :
  143. 143 : this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  144. 144 :
  145. 145 : if (!this.timeupdateSeen_) {
  146. 146 : this.one(this.player_, 'play', this.handlePlay_);
  147. 147 : this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  148. 148 : } else {
  149. 149 : this.on(this.player_, 'seeked', this.handleSeeked_);
  150. 150 : }
  151. 151 : }
  152. 152 :
  153. 153 : /**
  154. 154 : * handle the first timeupdate on the player if it wasn't already playing
  155. 155 : * when live tracker started tracking.
  156. 156 : */
  157. 157 : handleFirstTimeupdate() {
  158. 158 : this.timeupdateSeen_ = true;
  159. 159 : this.on(this.player_, 'seeked', this.handleSeeked_);
  160. 160 : }
  161. 161 :
  162. 162 : /**
  163. 163 : * Keep track of what time a seek starts, and listen for seeked
  164. 164 : * to find where a seek ends.
  165. 165 : */
  166. 166 : handleSeeked() {
  167. 167 : const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
  168. 168 :
  169. 169 : this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
  170. 170 : this.nextSeekedFromUser_ = false;
  171. 171 : this.trackLive_();
  172. 172 : }
  173. 173 :
  174. 174 : /**
  175. 175 : * handle the first play on the player, and make sure that we seek
  176. 176 : * right to the live edge.
  177. 177 : */
  178. 178 : handlePlay() {
  179. 179 : this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
  180. 180 : }
  181. 181 :
  182. 182 : /**
  183. 183 : * Stop tracking, and set all internal variables to
  184. 184 : * their initial value.
  185. 185 : */
  186. 186 : reset_() {
  187. 187 : this.lastTime_ = -1;
  188. 188 : this.pastSeekEnd_ = 0;
  189. 189 : this.lastSeekEnd_ = -1;
  190. 190 : this.behindLiveEdge_ = true;
  191. 191 : this.timeupdateSeen_ = false;
  192. 192 : this.seekedBehindLive_ = false;
  193. 193 : this.nextSeekedFromUser_ = false;
  194. 194 :
  195. 195 : this.clearInterval(this.trackingInterval_);
  196. 196 : this.trackingInterval_ = null;
  197. 197 :
  198. 198 : this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
  199. 199 : this.off(this.player_, 'seeked', this.handleSeeked_);
  200. 200 : this.off(this.player_, 'play', this.handlePlay_);
  201. 201 : this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
  202. 202 : this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
  203. 203 : }
  204. 204 :
  205. 205 : /**
  206. 206 : * The next seeked event is from the user. Meaning that any seek
  207. 207 : * > 2s behind live will be considered behind live for real and
  208. 208 : * liveTolerance will be ignored.
  209. 209 : */
  210. 210 : nextSeekedFromUser() {
  211. 211 : this.nextSeekedFromUser_ = true;
  212. 212 : }
  213. 213 :
  214. 214 : /**
  215. 215 : * stop tracking live playback
  216. 216 : */
  217. 217 : stopTracking() {
  218. 218 : if (!this.isTracking()) {
  219. 219 : return;
  220. 220 : }
  221. 221 : this.reset_();
  222. 222 : this.trigger('liveedgechange');
  223. 223 : }
  224. 224 :
  225. 225 : /**
  226. 226 : * A helper to get the player seekable end
  227. 227 : * so that we don't have to null check everywhere
  228. 228 : *
  229. 229 : * @return {number}
  230. 230 : * The furthest seekable end or Infinity.
  231. 231 : */
  232. 232 : seekableEnd() {
  233. 233 : const seekable = this.player_.seekable();
  234. 234 : const seekableEnds = [];
  235. 235 : let i = seekable ? seekable.length : 0;
  236. 236 :
  237. 237 : while (i--) {
  238. 238 : seekableEnds.push(seekable.end(i));
  239. 239 : }
  240. 240 :
  241. 241 : // grab the furthest seekable end after sorting, or if there are none
  242. 242 : // default to Infinity
  243. 243 : return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
  244. 244 : }
  245. 245 :
  246. 246 : /**
  247. 247 : * A helper to get the player seekable start
  248. 248 : * so that we don't have to null check everywhere
  249. 249 : *
  250. 250 : * @return {number}
  251. 251 : * The earliest seekable start or 0.
  252. 252 : */
  253. 253 : seekableStart() {
  254. 254 : const seekable = this.player_.seekable();
  255. 255 : const seekableStarts = [];
  256. 256 : let i = seekable ? seekable.length : 0;
  257. 257 :
  258. 258 : while (i--) {
  259. 259 : seekableStarts.push(seekable.start(i));
  260. 260 : }
  261. 261 :
  262. 262 : // grab the first seekable start after sorting, or if there are none
  263. 263 : // default to 0
  264. 264 : return seekableStarts.length ? seekableStarts.sort()[0] : 0;
  265. 265 : }
  266. 266 :
  267. 267 : /**
  268. 268 : * Get the live time window aka
  269. 269 : * the amount of time between seekable start and
  270. 270 : * live current time.
  271. 271 : *
  272. 272 : * @return {number}
  273. 273 : * The amount of seconds that are seekable in
  274. 274 : * the live video.
  275. 275 : */
  276. 276 : liveWindow() {
  277. 277 : const liveCurrentTime = this.liveCurrentTime();
  278. 278 :
  279. 279 : // if liveCurrenTime is Infinity then we don't have a liveWindow at all
  280. 280 : if (liveCurrentTime === Infinity) {
  281. 281 : return 0;
  282. 282 : }
  283. 283 :
  284. 284 : return liveCurrentTime - this.seekableStart();
  285. 285 : }
  286. 286 :
  287. 287 : /**
  288. 288 : * Determines if the player is live, only checks if this component
  289. 289 : * is tracking live playback or not
  290. 290 : *
  291. 291 : * @return {boolean}
  292. 292 : * Whether liveTracker is tracking
  293. 293 : */
  294. 294 : isLive() {
  295. 295 : return this.isTracking();
  296. 296 : }
  297. 297 :
  298. 298 : /**
  299. 299 : * Determines if currentTime is at the live edge and won't fall behind
  300. 300 : * on each seekableendchange
  301. 301 : *
  302. 302 : * @return {boolean}
  303. 303 : * Whether playback is at the live edge
  304. 304 : */
  305. 305 : atLiveEdge() {
  306. 306 : return !this.behindLiveEdge();
  307. 307 : }
  308. 308 :
  309. 309 : /**
  310. 310 : * get what we expect the live current time to be
  311. 311 : *
  312. 312 : * @return {number}
  313. 313 : * The expected live current time
  314. 314 : */
  315. 315 : liveCurrentTime() {
  316. 316 : return this.pastSeekEnd() + this.seekableEnd();
  317. 317 : }
  318. 318 :
  319. 319 : /**
  320. 320 : * The number of seconds that have occurred after seekable end
  321. 321 : * changed. This will be reset to 0 once seekable end changes.
  322. 322 : *
  323. 323 : * @return {number}
  324. 324 : * Seconds past the current seekable end
  325. 325 : */
  326. 326 : pastSeekEnd() {
  327. 327 : const seekableEnd = this.seekableEnd();
  328. 328 :
  329. 329 : if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
  330. 330 : this.pastSeekEnd_ = 0;
  331. 331 : }
  332. 332 : this.lastSeekEnd_ = seekableEnd;
  333. 333 : return this.pastSeekEnd_;
  334. 334 : }
  335. 335 :
  336. 336 : /**
  337. 337 : * If we are currently behind the live edge, aka currentTime will be
  338. 338 : * behind on a seekableendchange
  339. 339 : *
  340. 340 : * @return {boolean}
  341. 341 : * If we are behind the live edge
  342. 342 : */
  343. 343 : behindLiveEdge() {
  344. 344 : return this.behindLiveEdge_;
  345. 345 : }
  346. 346 :
  347. 347 : /**
  348. 348 : * Whether live tracker is currently tracking or not.
  349. 349 : */
  350. 350 : isTracking() {
  351. 351 : return typeof this.trackingInterval_ === 'number';
  352. 352 : }
  353. 353 :
  354. 354 : /**
  355. 355 : * Seek to the live edge if we are behind the live edge
  356. 356 : */
  357. 357 : seekToLiveEdge() {
  358. 358 : this.seekedBehindLive_ = false;
  359. 359 : if (this.atLiveEdge()) {
  360. 360 : return;
  361. 361 : }
  362. 362 : this.nextSeekedFromUser_ = false;
  363. 363 : this.player_.currentTime(this.liveCurrentTime());
  364. 364 :
  365. 365 : }
  366. 366 :
  367. 367 : /**
  368. 368 : * Dispose of liveTracker
  369. 369 : */
  370. 370 : dispose() {
  371. 371 : this.stopTracking();
  372. 372 : super.dispose();
  373. 373 : }
  374. 374 : }
  375. 375 :
  376. 376 : Component.registerComponent('LiveTracker', LiveTracker);
  377. 377 : export default LiveTracker;