Blame view

node_modules/videojs-contrib-hls/src/playback-watcher.js 13.4 KB
2a09d1a4   liuqimichale   添加宜春 天水 宣化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
  /**
   * @file playback-watcher.js
   *
   * Playback starts, and now my watch begins. It shall not end until my death. I shall
   * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns
   * and win no glory. I shall live and die at my post. I am the corrector of the underflow.
   * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge
   * my life and honor to the Playback Watch, for this Player and all the Players to come.
   */
  
  import window from 'global/window';
  import Ranges from './ranges';
  import videojs from 'video.js';
  
  // Set of events that reset the playback-watcher time check logic and clear the timeout
  const timerCancelEvents = [
    'seeking',
    'seeked',
    'pause',
    'playing',
    'error'
  ];
  
  /**
   * @class PlaybackWatcher
   */
  export default class PlaybackWatcher {
    /**
     * Represents an PlaybackWatcher object.
     * @constructor
     * @param {object} options an object that includes the tech and settings
     */
    constructor(options) {
      this.tech_ = options.tech;
      this.seekable = options.seekable;
  
      this.consecutiveUpdates = 0;
      this.lastRecordedTime = null;
      this.timer_ = null;
      this.checkCurrentTimeTimeout_ = null;
  
      if (options.debug) {
        this.logger_ = videojs.log.bind(videojs, 'playback-watcher ->');
      }
      this.logger_('initialize');
  
      let canPlayHandler = () => this.monitorCurrentTime_();
      let waitingHandler = () => this.techWaiting_();
      let cancelTimerHandler = () => this.cancelTimer_();
      let fixesBadSeeksHandler = () => this.fixesBadSeeks_();
  
      this.tech_.on('seekablechanged', fixesBadSeeksHandler);
      this.tech_.on('waiting', waitingHandler);
      this.tech_.on(timerCancelEvents, cancelTimerHandler);
      this.tech_.on('canplay', canPlayHandler);
  
      // Define the dispose function to clean up our events
      this.dispose = () => {
        this.logger_('dispose');
        this.tech_.off('seekablechanged', fixesBadSeeksHandler);
        this.tech_.off('waiting', waitingHandler);
        this.tech_.off(timerCancelEvents, cancelTimerHandler);
        this.tech_.off('canplay', canPlayHandler);
        if (this.checkCurrentTimeTimeout_) {
          window.clearTimeout(this.checkCurrentTimeTimeout_);
        }
        this.cancelTimer_();
      };
    }
  
    /**
     * Periodically check current time to see if playback stopped
     *
     * @private
     */
    monitorCurrentTime_() {
      this.checkCurrentTime_();
  
      if (this.checkCurrentTimeTimeout_) {
        window.clearTimeout(this.checkCurrentTimeTimeout_);
      }
  
      // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
      this.checkCurrentTimeTimeout_ =
        window.setTimeout(this.monitorCurrentTime_.bind(this), 250);
    }
  
    /**
     * The purpose of this function is to emulate the "waiting" event on
     * browsers that do not emit it when they are waiting for more
     * data to continue playback
     *
     * @private
     */
    checkCurrentTime_() {
      if (this.tech_.seeking() && this.fixesBadSeeks_()) {
        this.consecutiveUpdates = 0;
        this.lastRecordedTime = this.tech_.currentTime();
        return;
      }
  
      if (this.tech_.paused() || this.tech_.seeking()) {
        return;
      }
  
      let currentTime = this.tech_.currentTime();
      let buffered = this.tech_.buffered();
  
      if (this.lastRecordedTime === currentTime &&
          (!buffered.length ||
           currentTime + Ranges.SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) {
        // If current time is at the end of the final buffered region, then any playback
        // stall is most likely caused by buffering in a low bandwidth environment. The tech
        // should fire a `waiting` event in this scenario, but due to browser and tech
        // inconsistencies (e.g. The Flash tech does not fire a `waiting` event when the end
        // of the buffer is reached and has fallen off the live window). Calling
        // `techWaiting_` here allows us to simulate responding to a native `waiting` event
        // when the tech fails to emit one.
        return this.techWaiting_();
      }
  
      if (this.consecutiveUpdates >= 5 &&
          currentTime === this.lastRecordedTime) {
        this.consecutiveUpdates++;
        this.waiting_();
      } else if (currentTime === this.lastRecordedTime) {
        this.consecutiveUpdates++;
      } else {
        this.consecutiveUpdates = 0;
        this.lastRecordedTime = currentTime;
      }
    }
  
    /**
     * Cancels any pending timers and resets the 'timeupdate' mechanism
     * designed to detect that we are stalled
     *
     * @private
     */
    cancelTimer_() {
      this.consecutiveUpdates = 0;
  
      if (this.timer_) {
        this.logger_('cancelTimer_');
        clearTimeout(this.timer_);
      }
  
      this.timer_ = null;
    }
  
    /**
     * Fixes situations where there's a bad seek
     *
     * @return {Boolean} whether an action was taken to fix the seek
     * @private
     */
    fixesBadSeeks_() {
      const seeking = this.tech_.seeking();
      const seekable = this.seekable();
      const currentTime = this.tech_.currentTime();
      let seekTo;
  
      if (seeking && this.afterSeekableWindow_(seekable, currentTime)) {
        const seekableEnd = seekable.end(seekable.length - 1);
  
        // sync to live point (if VOD, our seekable was updated and we're simply adjusting)
        seekTo = seekableEnd;
      }
  
      if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) {
        const seekableStart = seekable.start(0);
  
        // sync to the beginning of the live window
        // provide a buffer of .1 seconds to handle rounding/imprecise numbers
        seekTo = seekableStart + Ranges.SAFE_TIME_DELTA;
      }
  
      if (typeof seekTo !== 'undefined') {
        this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` +
                    `seekable range ${Ranges.printableRange(seekable)}. Seeking to ` +
                    `${seekTo}.`);
  
        this.tech_.setCurrentTime(seekTo);
        return true;
      }
  
      return false;
    }
  
    /**
     * Handler for situations when we determine the player is waiting.
     *
     * @private
     */
    waiting_() {
      if (this.techWaiting_()) {
        return;
      }
  
      // All tech waiting checks failed. Use last resort correction
      let currentTime = this.tech_.currentTime();
      let buffered = this.tech_.buffered();
      let currentRange = Ranges.findRange(buffered, currentTime);
  
      // Sometimes the player can stall for unknown reasons within a contiguous buffered
      // region with no indication that anything is amiss (seen in Firefox). Seeking to
      // currentTime is usually enough to kickstart the player. This checks that the player
      // is currently within a buffered region before attempting a corrective seek.
      // Chrome does not appear to continue `timeupdate` events after a `waiting` event
      // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also
      // make sure there is ~3 seconds of forward buffer before taking any corrective action
      // to avoid triggering an `unknownwaiting` event when the network is slow.
      if (currentRange.length && currentTime + 3 <= currentRange.end(0)) {
        this.cancelTimer_();
        this.tech_.setCurrentTime(currentTime);
  
        this.logger_(`Stopped at ${currentTime} while inside a buffered region ` +
          `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` +
          `playback by seeking to the current time.`);
  
        // unknown waiting corrections may be useful for monitoring QoS
        this.tech_.trigger({type: 'usage', name: 'hls-unknown-waiting'});
        return;
      }
    }
  
    /**
     * Handler for situations when the tech fires a `waiting` event
     *
     * @return {Boolean}
     *         True if an action (or none) was needed to correct the waiting. False if no
     *         checks passed
     * @private
     */
    techWaiting_() {
      let seekable = this.seekable();
      let currentTime = this.tech_.currentTime();
  
      if (this.tech_.seeking() && this.fixesBadSeeks_()) {
        // Tech is seeking or bad seek fixed, no action needed
        return true;
      }
  
      if (this.tech_.seeking() || this.timer_ !== null) {
        // Tech is seeking or already waiting on another action, no action needed
        return true;
      }
  
      if (this.beforeSeekableWindow_(seekable, currentTime)) {
        let livePoint = seekable.end(seekable.length - 1);
  
        this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` +
                     `live point (seekable end) ${livePoint}`);
        this.cancelTimer_();
        this.tech_.setCurrentTime(livePoint);
  
        // live window resyncs may be useful for monitoring QoS
        this.tech_.trigger({type: 'usage', name: 'hls-live-resync'});
        return true;
      }
  
      let buffered = this.tech_.buffered();
      let nextRange = Ranges.findNextRange(buffered, currentTime);
  
      if (this.videoUnderflow_(nextRange, buffered, currentTime)) {
        // Even though the video underflowed and was stuck in a gap, the audio overplayed
        // the gap, leading currentTime into a buffered range. Seeking to currentTime
        // allows the video to catch up to the audio position without losing any audio
        // (only suffering ~3 seconds of frozen video and a pause in audio playback).
        this.cancelTimer_();
        this.tech_.setCurrentTime(currentTime);
  
        // video underflow may be useful for monitoring QoS
        this.tech_.trigger({type: 'usage', name: 'hls-video-underflow'});
        return true;
      }
  
      // check for gap
      if (nextRange.length > 0) {
        let difference = nextRange.start(0) - currentTime;
  
        this.logger_(
          `Stopped at ${currentTime}, setting timer for ${difference}, seeking ` +
          `to ${nextRange.start(0)}`);
  
        this.timer_ = setTimeout(this.skipTheGap_.bind(this),
                                 difference * 1000,
                                 currentTime);
        return true;
      }
  
      // All checks failed. Returning false to indicate failure to correct waiting
      return false;
    }
  
    afterSeekableWindow_(seekable, currentTime) {
      if (!seekable.length) {
        // we can't make a solid case if there's no seekable, default to false
        return false;
      }
  
      if (currentTime > seekable.end(seekable.length - 1) + Ranges.SAFE_TIME_DELTA) {
        return true;
      }
  
      return false;
    }
  
    beforeSeekableWindow_(seekable, currentTime) {
      if (seekable.length &&
          // can't fall before 0 and 0 seekable start identifies VOD stream
          seekable.start(0) > 0 &&
          currentTime < seekable.start(0) - Ranges.SAFE_TIME_DELTA) {
        return true;
      }
  
      return false;
    }
  
    videoUnderflow_(nextRange, buffered, currentTime) {
      if (nextRange.length === 0) {
        // Even if there is no available next range, there is still a possibility we are
        // stuck in a gap due to video underflow.
        let gap = this.gapFromVideoUnderflow_(buffered, currentTime);
  
        if (gap) {
          this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` +
                       `Seeking to current time ${currentTime}`);
  
          return true;
        }
      }
  
      return false;
    }
  
    /**
     * Timer callback. If playback still has not proceeded, then we seek
     * to the start of the next buffered region.
     *
     * @private
     */
    skipTheGap_(scheduledCurrentTime) {
      let buffered = this.tech_.buffered();
      let currentTime = this.tech_.currentTime();
      let nextRange = Ranges.findNextRange(buffered, currentTime);
  
      this.cancelTimer_();
  
      if (nextRange.length === 0 ||
          currentTime !== scheduledCurrentTime) {
        return;
      }
  
      this.logger_('skipTheGap_:',
                   'currentTime:', currentTime,
                   'scheduled currentTime:', scheduledCurrentTime,
                   'nextRange start:', nextRange.start(0));
  
      // only seek if we still have not played
      this.tech_.setCurrentTime(nextRange.start(0) + Ranges.TIME_FUDGE_FACTOR);
  
      this.tech_.trigger({type: 'usage', name: 'hls-gap-skip'});
    }
  
    gapFromVideoUnderflow_(buffered, currentTime) {
      // At least in Chrome, if there is a gap in the video buffer, the audio will continue
      // playing for ~3 seconds after the video gap starts. This is done to account for
      // video buffer underflow/underrun (note that this is not done when there is audio
      // buffer underflow/underrun -- in that case the video will stop as soon as it
      // encounters the gap, as audio stalls are more noticeable/jarring to a user than
      // video stalls). The player's time will reflect the playthrough of audio, so the
      // time will appear as if we are in a buffered region, even if we are stuck in a
      // "gap."
      //
      // Example:
      // video buffer:   0 => 10.1, 10.2 => 20
      // audio buffer:   0 => 20
      // overall buffer: 0 => 10.1, 10.2 => 20
      // current time: 13
      //
      // Chrome's video froze at 10 seconds, where the video buffer encountered the gap,
      // however, the audio continued playing until it reached ~3 seconds past the gap
      // (13 seconds), at which point it stops as well. Since current time is past the
      // gap, findNextRange will return no ranges.
      //
      // To check for this issue, we see if there is a gap that starts somewhere within
      // a 3 second range (3 seconds +/- 1 second) back from our current time.
      let gaps = Ranges.findGaps(buffered);
  
      for (let i = 0; i < gaps.length; i++) {
        let start = gaps.start(i);
        let end = gaps.end(i);
  
        // gap is starts no more than 4 seconds back
        if (currentTime - start < 4 && currentTime - start > 2) {
          return {
            start,
            end
          };
        }
      }
  
      return null;
    }
  
    /**
     * A debugging logger noop that is set to console.log only if debugging
     * is enabled globally
     *
     * @private
     */
    logger_() {}
  }