text: /** @license
   * Copyright 2016 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  describe('Player', () => {
    const Util = shaka.test.Util;
    const waitUntilPlayheadReaches = Util.waitUntilPlayheadReaches;
  
    /** @type {!jasmine.Spy} */
    let onErrorSpy;
  
    /** @type {!HTMLVideoElement} */
    let video;
    /** @type {!shaka.Player} */
    let player;
    /** @type {!shaka.util.EventManager} */
    let eventManager;
  
    let compiledShaka;
  
    beforeAll(async () => {
      video = shaka.test.UiUtils.createVideoElement();
      document.body.appendChild(video);
  
      compiledShaka = await Util.loadShaka(getClientArg('uncompiled'));
    });
  
    beforeEach(async () => {
      await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
      player = new compiledShaka.Player(video);
  
      // Grab event manager from the uncompiled library:
      eventManager = new shaka.util.EventManager();
  
      onErrorSpy = jasmine.createSpy('onError');
      onErrorSpy.and.callFake((event) => {
        fail(event.detail);
      });
      eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
    });
  
    afterEach(async () => {
      eventManager.release();
  
      await player.destroy();
    });
  
    afterAll(() => {
      document.body.removeChild(video);
    });
  
    describe('attach', () => {
      beforeEach(async () => {
        // To test attach, we want to construct a player without a video element
        // attached in advance.  To do that, we destroy the player that was
        // constructed in the outermost beforeEach(), then construct a new one
        // without a video element.
        await player.destroy();
        player = new compiledShaka.Player();
      });
  
      it('can be used before load()', async () => {
        await player.attach(video);
        await player.load('test:sintel_compiled');
      });
    });  // describe('attach')
  
    describe('getStats', () => {
      it('gives stats about current stream', async () => {
        // This is tested more in player_unit.js.  This is here to test the public
        // API and to check for renaming.
        await player.load('test:sintel_compiled');
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 1, 10);
  
        const stats = player.getStats();
        const expected = {
          width: jasmine.any(Number),
          height: jasmine.any(Number),
          streamBandwidth: jasmine.any(Number),
  
          decodedFrames: jasmine.any(Number),
          droppedFrames: jasmine.any(Number),
          corruptedFrames: jasmine.any(Number),
          estimatedBandwidth: jasmine.any(Number),
  
          loadLatency: jasmine.any(Number),
          manifestTimeSeconds: jasmine.any(Number),
          drmTimeSeconds: jasmine.any(Number),
          playTime: jasmine.any(Number),
          pauseTime: jasmine.any(Number),
          bufferingTime: jasmine.any(Number),
          licenseTime: jasmine.any(Number),
  
          // We should have loaded the first Period by now, so we should have a
          // history.
          switchHistory: jasmine.arrayContaining([{
            timestamp: jasmine.any(Number),
            id: jasmine.any(Number),
            type: 'variant',
            fromAdaptation: true,
            bandwidth: 0,
          }]),
  
          stateHistory: jasmine.arrayContaining([{
            state: 'playing',
            timestamp: jasmine.any(Number),
            duration: jasmine.any(Number),
          }]),
        };
        expect(stats).toEqual(expected);
      });
  
      // Regression test for Issue #968 where trying to get the stats before
      // calling load would fail because not all components had been initialized.
      it('can get stats before loading content', async () => {
        // Destroy Player created in beforeEach.
        await player.destroy();
  
        // We are opting not to initialize the player with a video element so that
        // it is in the least loaded state possible.
        player = new compiledShaka.Player();
  
        const stats = player.getStats();
        expect(stats).toBeTruthy();
        await player.destroy();
      });
    });  // describe('getStats')
  
    describe('setTextTrackVisibility', () => {
      // Using mode='disabled' on TextTrack causes cues to go null, which leads
      // to a crash in TextEngine.  This validates that we do not trigger this
      // behavior when changing visibility of text.
  
      it('does not cause cues to be null', async () => {
        await player.load('test:sintel_compiled');
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 1, 10);
  
        // This TextTrack was created as part of load() when we set up the
        // TextDisplayer.
        const textTrack = video.textTracks[0];
        expect(textTrack).not.toBe(null);
  
        if (textTrack) {
          // This should not be null initially.
          expect(textTrack.cues).not.toBe(null);
  
          await player.setTextTrackVisibility(true);
          // This should definitely not be null when visible.
          expect(textTrack.cues).not.toBe(null);
  
          await player.setTextTrackVisibility(false);
          // This should not transition to null when invisible.
          expect(textTrack.cues).not.toBe(null);
        }
      });
  
      it('is called automatically if language prefs match', async () => {
        // If the text is a match for the user's preferences, and audio differs
        // from text, we enable text display automatically.
  
        // NOTE: This is also a regression test for #1696, in which a change
        // to this feature broke StreamingEngine initialization.
  
        const preferredTextLanguage = 'fa';  // The same as in the content itself
        player.configure({preferredTextLanguage: preferredTextLanguage});
  
        // Now load a version of Sintel with delayed setup of video & audio
        // streams and wait for completion.
        await player.load('test:sintel_realistic_compiled');
        // By this point, a MediaSource error would be thrown in a repro of bug
        // #1696.
  
        // Make sure the automatic setting took effect.
        expect(player.isTextTrackVisible()).toBe(true);
  
        // Make sure the content we tested with has text tracks, that the config
        // we used matches the text language, and that the audio language differs.
        // These will catch any changes to the underlying content that would
        // invalidate the test setup.
        expect(player.getTextTracks().length).not.toBe(0);
        const textTrack = player.getTextTracks()[0];
        expect(textTrack.language).toBe(preferredTextLanguage);
  
        const variantTrack = player.getVariantTracks()[0];
        expect(variantTrack.language).not.toBe(textTrack.language);
      });
  
      it('is not called automatically without language pref match', async () => {
        // If the text preference doesn't match the content, we do not enable text
        // display automatically.
  
        const preferredTextLanguage = 'xx';  // Differs from the content itself
        player.configure({preferredTextLanguage: preferredTextLanguage});
  
        // Now load the content and wait for completion.
        await player.load('test:sintel_realistic_compiled');
  
        // Make sure the automatic setting did not happen.
        expect(player.isTextTrackVisible()).toBe(false);
  
        // Make sure the content we tested with has text tracks, that the config
        // we used does not match the text language, and that the text and audio
        // languages do not match each other (to keep this distinct from the next
        // test case).  This will catch any changes to the underlying content that
        // would invalidate the test setup.
        expect(player.getTextTracks().length).not.toBe(0);
        const textTrack = player.getTextTracks()[0];
        expect(textTrack.language).not.toBe(preferredTextLanguage);
  
        const variantTrack = player.getVariantTracks()[0];
        expect(variantTrack.language).not.toBe(textTrack.language);
      });
  
      it('is not called automatically with audio and text match', async () => {
        // If the audio and text tracks use the same language, we do not enable
        // text display automatically, no matter the text preference.
  
        const preferredTextLanguage = 'und';  // The same as in the content itself
        player.configure({preferredTextLanguage: preferredTextLanguage});
  
        // Now load the content and wait for completion.
        await player.load('test:sintel_compiled');
  
        // Make sure the automatic setting did not happen.
        expect(player.isTextTrackVisible()).toBe(false);
  
        // Make sure the content we tested with has text tracks, that the
        // config we used matches the content, and that the text and audio
        // languages match each other.  This will catch any changes to the
        // underlying content that would invalidate the test setup.
        expect(player.getTextTracks().length).not.toBe(0);
        const textTrack = player.getTextTracks()[0];
        expect(textTrack.language).toBe(preferredTextLanguage);
  
        const variantTrack = player.getVariantTracks()[0];
        expect(variantTrack.language).toBe(textTrack.language);
      });
  
      // Repro for https://github.com/google/shaka-player/issues/1879.
      it('actually appends cues when enabled initially', async () => {
        let cues = [];
        /** @const {!shaka.test.FakeTextDisplayer} */
        const displayer = new shaka.test.FakeTextDisplayer();
        displayer.appendSpy.and.callFake((added) => {
          cues = cues.concat(added);
        });
  
        player.configure('textDisplayFactory', () => displayer);
  
        const preferredTextLanguage = 'fa';  // The same as in the content itself
        player.configure({preferredTextLanguage: preferredTextLanguage});
  
        await player.load('test:sintel_realistic_compiled');
        await Util.delay(1);  // Allow the first segments to be appended.
  
        expect(player.isTextTrackVisible()).toBe(true);
        expect(displayer.isTextVisible()).toBe(true);
        expect(cues.length).toBeGreaterThan(0);
      });
  
      it('actually appends cues for external text', async () => {
        let cues = [];
        /** @const {!shaka.test.FakeTextDisplayer} */
        const displayer = new shaka.test.FakeTextDisplayer();
        displayer.appendSpy.and.callFake((added) => {
          cues = cues.concat(added);
        });
  
        player.configure('textDisplayFactory', () => displayer);
  
        const eventManager = new shaka.util.EventManager();
        /** @type {shaka.test.Waiter} */
        const waiter = new shaka.test.Waiter(eventManager);
  
  
        await player.load('test:sintel_no_text_compiled');
        const locationUri = new goog.Uri(location.href);
        const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt');
        const absoluteUri = locationUri.resolve(partialUri);
        await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles',
            'text/vtt');
  
        const textTracks = player.getTextTracks();
        expect(textTracks).toBeTruthy();
        expect(textTracks.length).toBe(1);
  
        player.setTextTrackVisibility(true);
        await waiter.waitForEvent(player, 'texttrackvisibility');
        // Wait for the text cues to get appended.
        // TODO: this should be based on an event instead.
        await Util.delay(1);
  
        expect(player.isTextTrackVisible()).toBe(true);
        expect(displayer.isTextVisible()).toBe(true);
        expect(cues.length).toBeGreaterThan(0);
      });
    });  // describe('setTextTrackVisibility')
  
    describe('plays', () => {
      it('with external text tracks', async () => {
        await player.load('test:sintel_no_text_compiled');
  
        // For some reason, using path-absolute URLs (i.e. without the hostname)
        // like this doesn't work on Safari.  So manually resolve the URL.
        const locationUri = new goog.Uri(location.href);
        const partialUri = new goog.Uri('/base/test/test/assets/text-clip.vtt');
        const absoluteUri = locationUri.resolve(partialUri);
        await player.addTextTrack(absoluteUri.toString(), 'en', 'subtitles',
            'text/vtt');
  
        const textTracks = player.getTextTracks();
        expect(textTracks).toBeTruthy();
        expect(textTracks.length).toBe(1);
  
        expect(textTracks[0].active).toBe(true);
        expect(textTracks[0].language).toBe('en');
      });
  
      it('with cea closed captions', async () => {
        await player.load('test:cea-708_mp4_compiled');
  
        const textTracks = player.getTextTracks();
        expect(textTracks).toBeTruthy();
        expect(textTracks.length).toBe(1);
        expect(textTracks[0].language).toBe('en');
      });
  
      it('while changing languages with short Periods', async () => {
        // See: https://github.com/google/shaka-player/issues/797
        player.configure({preferredAudioLanguage: 'en'});
        await player.load('test:sintel_short_periods_compiled');
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 8, 30);
  
        // The Period changes at 10 seconds.  Assert that we are in the previous
        // Period and have buffered into the next one.
        expect(video.currentTime).toBeLessThan(9);
        // The two periods might not be in a single contiguous buffer, so don't
        // check end(0).  Gap-jumping will deal with any discontinuities.
        const bufferEnd = video.buffered.end(video.buffered.length - 1);
        expect(bufferEnd).toBeGreaterThan(11);
  
        // Change to a different language; this should clear the buffers and
        // cause a Period transition again.
        expect(getActiveLanguage()).toBe('en');
        player.selectAudioLanguage('es');
        await waitUntilPlayheadReaches(eventManager, video, 21, 30);
  
        // Should have gotten past the next Period transition and still be
        // playing the new language.
        expect(getActiveLanguage()).toBe('es');
      });
  
      it('at higher playback rates', async () => {
        await player.load('test:sintel_compiled');
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 1, 10);
  
        // Enabling trick play should change our playback rate to the same rate.
        player.trickPlay(2);
        expect(video.playbackRate).toBe(2);
  
        // Let playback continue playing for a bit longer.
        await shaka.test.Util.delay(2);
  
        // Cancelling trick play should return our playback rate to normal.
        player.cancelTrickPlay();
        expect(video.playbackRate).toBe(1);
      });
  
      // Regression test for #2326.
      //
      // 1. Construct an instance with a video element.
      // 2. Don't call or await attach().
      // 3. Call load() with a MIME type, which triggers a check for native
      //    playback support.
      //
      // Note that a real playback may use a HEAD request to fetch a MIME type,
      // even if one is not specified in load().
      it('immediately after construction with MIME type', async () => {
        const testSchemeMimeType = 'application/x-test-manifest';
        player = new compiledShaka.Player(video);
        await player.load('test:sintel_compiled', 0, testSchemeMimeType);
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 1, 10);
      });
  
      /**
       * Gets the language of the active Variant.
       * @return {string}
       */
      function getActiveLanguage() {
        const tracks = player.getVariantTracks().filter((t) => {
          return t.active;
        });
        expect(tracks.length).toBeGreaterThan(0);
        return tracks[0].language;
      }
    });  // describe('plays')
  
    describe('TextDisplayer plugin', () => {
      // Simulate the use of an external TextDisplayer plugin.
      /** @type {shaka.test.FakeTextDisplayer} */
      let textDisplayer;
      beforeEach(() => {
        textDisplayer = new shaka.test.FakeTextDisplayer();
  
        textDisplayer.isTextVisibleSpy.and.callFake(() => {
          return false;
        });
        textDisplayer.destroySpy.and.returnValue(Promise.resolve());
        player.configure('textDisplayFactory', () => textDisplayer);
  
        // Make sure the configuration was taken.
        const configuredFactory = player.getConfiguration().textDisplayFactory;
        const configuredTextDisplayer = configuredFactory();
        expect(configuredTextDisplayer).toBe(textDisplayer);
      });
  
      // Regression test for https://github.com/google/shaka-player/issues/1187
      it('does not throw on destroy', async () => {
        await player.load('test:sintel_compiled');
        video.play();
        await waitUntilPlayheadReaches(eventManager, video, 1, 10);
        await player.unload();
        // Before we fixed #1187, the call to destroy() on textDisplayer was
        // renamed in the compiled version and could not be called.
        expect(textDisplayer.destroySpy).toHaveBeenCalled();
      });
    });  // describe('TextDisplayer plugin')
  
    describe('TextAndRoles', () => {
      // Regression Test. Makes sure that the language and role fields have been
      // properly exported from the player.
      it('exports language and roles fields', async () => {
        await player.load('test:sintel_compiled');
        const languagesAndRoles = player.getTextLanguagesAndRoles();
        expect(languagesAndRoles.length).toBeTruthy();
        for (const languageAndRole of languagesAndRoles) {
          expect(languageAndRole.language).not.toBeUndefined();
          expect(languageAndRole.role).not.toBeUndefined();
        }
      });
    });  // describe('TextAndRoles')
  
    describe('streaming event', () => {
      // Calling switch early during load() caused a failed assertion in Player
      // and the track selection was ignored.  Because this bug involved
      // interactions between Player and StreamingEngine, it is an integration
      // test and not a unit test.
      // https://github.com/google/shaka-player/issues/1119
      it('allows early selection of specific tracks', async () => {
        /** @type {!jasmine.Spy} */
        const streamingListener = jasmine.createSpy('listener');
  
        // Because this is an issue with failed assertions, destroy the existing
        // player from the compiled version, and create a new one using the
        // uncompiled version.  Then we will get assertions.
        eventManager.unlisten(player, 'error');
        await player.destroy();
        player = new shaka.Player(video);  // NOTE: MUST BE UNCOMPILED
        player.configure({abr: {enabled: false}});
        eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
  
        // When 'streaming' fires, select the first track explicitly.
        player.addEventListener('streaming', Util.spyFunc(streamingListener));
        streamingListener.and.callFake(() => {
          const tracks = player.getVariantTracks();
          player.selectVariantTrack(tracks[0]);
        });
  
        // Now load the content.
        await player.load('test:sintel');
  
        // When the bug triggers, we fail assertions in Player.
        // Make sure the listener was triggered, so that it could trigger the
        // code path in this bug.
        expect(streamingListener).toHaveBeenCalled();
      });
  
      // After fixing the issue above, calling switch early during a second load()
      // caused a failed assertion in StreamingEngine, because we did not reset
      // switchingPeriods_ in Player.  Because this bug involved interactions
      // between Player and StreamingEngine, it is an integration test and not a
      // unit test.
      // https://github.com/google/shaka-player/issues/1119
      it('allows selection of tracks in subsequent loads', async () => {
        /** @type {!jasmine.Spy} */
        const streamingListener = jasmine.createSpy('listener');
  
        // Because this is an issue with failed assertions, destroy the existing
        // player from the compiled version, and create a new one using the
        // uncompiled version.  Then we will get assertions.
        eventManager.unlisten(player, 'error');
        await player.destroy();
        player = new shaka.Player(video);  // NOTE: MUST BE UNCOMPILED
        player.configure({abr: {enabled: false}});
        eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
  
        // This bug only triggers when you do this on the second load.
        // So we load one piece of content, then set up the streaming listener
        // to change tracks, then we load a second piece of content.
        await player.load('test:sintel');
  
        // Give StreamingEngine time to complete all setup and to call back into
        // the Player with canSwitch_.  If you move on too quickly to the next
        // load(), the bug does not reproduce.
        await shaka.test.Util.delay(1);
  
        player.addEventListener('streaming', Util.spyFunc(streamingListener));
  
        streamingListener.and.callFake(() => {
          const track = player.getVariantTracks()[0];
          player.selectVariantTrack(track);
        });
  
        // Now load again to trigger the failed assertion.
        await player.load('test:sintel');
  
        // When the bug triggers, we fail assertions in StreamingEngine.
        // So just make sure the listener was triggered, so that it could
        // trigger the code path in this bug.
        expect(streamingListener).toHaveBeenCalled();
      });
    });  // describe('streaming event')
  
    describe('tracks', () => {
      // This is a regression test for b/138941217, in which tracks briefly
      // vanished during the loading process.  On Chromecast devices, where the
      // timing is very different from on desktop, this could occur such that
      // there were no tracks after load() is resolved.
      // This is an integration test so that we can check the behavior of the
      // Player against actual platform behavior on all supported platforms.
      it('remain available at every stage of loading', async () => {
        let tracksFound = false;
  
        /**
         * @param {string} when When the check takes place.
         *
         * Will fail the test if tracks disappear after they first become
         * available.
         */
        const checkTracks = (when) => {
          // If tracks have already been found, expect them to still be found.
          const tracksNow = player.getVariantTracks().length != 0;
          if (tracksFound) {
            expect(tracksNow).withContext(when).toBe(true);
          } else {
            // If tracks are now found, they should not, at any point during
            // the loading process, disappear again.
            if (tracksNow) {
              tracksFound = true;
            }
          }
          shaka.log.debug(
              'checkTracks', when,
              'tracksFound=', tracksFound,
              'tracksNow=', tracksNow);
        };
  
        /** @param {Event} event */
        const checkOnEvent = (event) => {
          checkTracks(event.type + ' event');
        };
  
        // On each of these events, we will notice when tracks first appear, and
        // verify that they never disappear at any point in the loading sequence.
        eventManager.listen(video, 'canplay', checkOnEvent);
        eventManager.listen(video, 'canplaythrough', checkOnEvent);
        eventManager.listen(video, 'durationchange', checkOnEvent);
        eventManager.listen(video, 'emptied', checkOnEvent);
        eventManager.listen(video, 'loadeddata', checkOnEvent);
        eventManager.listen(video, 'loadedmetadata', checkOnEvent);
        eventManager.listen(video, 'loadstart', checkOnEvent);
        eventManager.listen(video, 'pause', checkOnEvent);
        eventManager.listen(video, 'play', checkOnEvent);
        eventManager.listen(video, 'playing', checkOnEvent);
        eventManager.listen(video, 'seeked', checkOnEvent);
        eventManager.listen(video, 'seeking', checkOnEvent);
        eventManager.listen(video, 'stalled', checkOnEvent);
        eventManager.listen(video, 'waiting', checkOnEvent);
        eventManager.listen(player, 'trackschanged', checkOnEvent);
  
        const waiter = (new shaka.test.Waiter(eventManager)).timeoutAfter(10);
        const canPlayThrough = waiter.waitForEvent(video, 'canplaythrough');
  
        // Important: use a stream that starts somewhere other than zero, so that
        // the video element's time is initially different from the start time of
        // playback, and there is no content at time zero.
        await player.load('test:sintel_start_at_3_compiled', 5);
        shaka.log.debug('load resolved');
  
        // When load is resolved(), tracks should definitely exist.
        expect(tracksFound).toBe(true);
  
        // Let the test keep running until we can play through.  In the original
        // bug, tracks would disappear _after_ load() on some platforms.
        await canPlayThrough;
      });
    });  // describe('tracks')
  
    describe('buffering', () => {
      const startBuffering = jasmine.objectContaining({buffering: true});
      const endBuffering = jasmine.objectContaining({buffering: false});
      /** @type {!jasmine.Spy} */
      let onBuffering;
      /** @type {!shaka.test.Waiter} */
      let waiter;
  
      beforeEach(() => {
        onBuffering = jasmine.createSpy('onBuffering');
        player.addEventListener('buffering', Util.spyFunc(onBuffering));
  
        waiter = new shaka.test.Waiter(eventManager)
            .timeoutAfter(10)
            .failOnTimeout(true);
      });
  
      it('enters/exits buffering state at start', async () => {
        // Set a large rebuffer goal to ensure we can see the buffering before
        // we start playing.
        player.configure('streaming.rebufferingGoal', 30);
  
        await player.load('test:sintel_long_compiled');
        video.pause();
        expect(onBuffering).toHaveBeenCalledTimes(1);
        expect(onBuffering).toHaveBeenCalledWith(startBuffering);
        onBuffering.calls.reset();
  
        await waiter.waitForEvent(player, 'buffering');
        expect(onBuffering).toHaveBeenCalledTimes(1);
        expect(onBuffering).toHaveBeenCalledWith(endBuffering);
  
        expect(getBufferedAhead()).toBeGreaterThanOrEqual(30);
      });
  
      it('enters/exits buffering state while playing', async () => {
        player.configure('streaming.rebufferingGoal', 1);
        player.configure('streaming.bufferingGoal', 10);
  
        await player.load('test:sintel_long_compiled');
        video.pause();
        if (player.isBuffering()) {
          await waiter.waitForEvent(player, 'buffering');
        }
        onBuffering.calls.reset();
  
        player.configure('streaming.rebufferingGoal', 30);
        video.currentTime = 70;
        await waiter.waitForEvent(player, 'buffering');
        expect(onBuffering).toHaveBeenCalledTimes(1);
        expect(onBuffering).toHaveBeenCalledWith(startBuffering);
        onBuffering.calls.reset();
  
        expect(getBufferedAhead()).toBeLessThan(30);
  
        await waiter.waitForEvent(player, 'buffering');
        expect(onBuffering).toHaveBeenCalledTimes(1);
        expect(onBuffering).toHaveBeenCalledWith(endBuffering);
  
        expect(getBufferedAhead()).toBeGreaterThanOrEqual(30);
      });
  
      it('buffers ahead of the playhead', async () => {
        player.configure('streaming.bufferingGoal', 10);
  
        await player.load('test:sintel_long_compiled');
        video.pause();
        await waitUntilBuffered(10);
  
        player.configure('streaming.bufferingGoal', 30);
        await waitUntilBuffered(30);
  
        player.configure('streaming.bufferingGoal', 60);
        await waitUntilBuffered(60);
        await Util.delay(0.2);
        expect(getBufferedAhead()).toBeLessThan(70);  // 60 + segment_size
  
        // We don't remove buffered content ahead of the playhead, so seek to
        // clear the buffer.
        player.configure('streaming.bufferingGoal', 10);
        video.currentTime = 120;
        await waitUntilBuffered(10);
        await Util.delay(0.2);
        expect(getBufferedAhead()).toBeLessThan(20);  // 10 + segment_size
      });
  
      it('clears buffer behind playhead', async () => {
        player.configure('streaming.bufferingGoal', 30);
        player.configure('streaming.bufferBehind', 30);
  
        await player.load('test:sintel_long_compiled');
        video.pause();
        await waitUntilBuffered(30);
        video.currentTime = 20;
        await waitUntilBuffered(30);
  
        expect(getBufferedBehind()).toBe(20);  // Buffered to start still.
        video.currentTime = 50;
        await waitUntilBuffered(30);
        expect(getBufferedBehind()).toBeLessThan(30);
  
        player.configure('streaming.bufferBehind', 10);
        // We only evict content when we append a segment, so increase the
        // buffering goal so we append another segment.
        player.configure('streaming.bufferingGoal', 40);
        await waitUntilBuffered(40);
        expect(getBufferedBehind()).toBeLessThan(10);
      });
  
      function getBufferedAhead() {
        const end = shaka.media.TimeRangesUtils.bufferEnd(video.buffered);
        return end - video.currentTime;
      }
  
      function getBufferedBehind() {
        const start = shaka.media.TimeRangesUtils.bufferStart(video.buffered);
        return video.currentTime - start;
      }
  
      async function waitUntilBuffered(amount) {
        for (const _ of shaka.util.Iterables.range(25)) {
          shaka.util.Functional.ignored(_);
          // We buffer from an internal segment, so this shouldn't take long to
          // buffer.
          await Util.delay(0.1);  // eslint-disable-line no-await-in-loop
          if (getBufferedAhead() >= amount) {
            return;
          }
        }
        throw new Error('Timeout waiting to buffer');
      }
    });  // describe('buffering')
  
    describe('configuration', () => {
      it('has the correct number of arguments in compiled callbacks', () => {
        // Get the default configuration for both the compiled & uncompiled
        // versions for comparison.
        const compiledConfig = (new compiledShaka.Player()).getConfiguration();
        const uncompiledConfig = (new shaka.Player()).getConfiguration();
  
        compareConfigFunctions(compiledConfig, uncompiledConfig);
  
        /**
         * Find all the callbacks in the configuration recursively and compare
         * their lengths (number of arguments).  We warn the app developer when a
         * configured callback has the wrong number of arguments, so our own
         * compiled versions must be correct.
         *
         * @param {Object} compiled
         * @param {Object} uncompiled
         * @param {string=} basePath The path to this point in the config, for
         *   logging purposes.
         */
        function compareConfigFunctions(compiled, uncompiled, basePath = '') {
          for (const key in uncompiled) {
            const uncompiledValue = uncompiled[key];
            const compiledValue = compiled[key];
            const path = basePath + '.' + key;
  
            if (uncompiledValue && uncompiledValue.constructor == Object) {
              // This is an anonymous Object, so recurse on it.
              compareConfigFunctions(compiledValue, uncompiledValue, path);
            } else if (typeof uncompiledValue == 'function') {
              // This is a function, so check its length.  The uncompiled version
              // is considered canonically correct, so we use the uncompiled
              // length as the expectation.
              shaka.log.debug('[' + path + ']',
                  compiledValue.length, 'should be', uncompiledValue.length);
              expect(compiledValue.length).withContext(path)
                  .toBe(uncompiledValue.length);
            }
          }
        }
      });
    });  // describe('configuration')
  
    describe('adaptation', () => {
      /** @type {!shaka.test.FakeAbrManager} */
      let abrManager;
  
      beforeEach(() => {
        abrManager = new shaka.test.FakeAbrManager();
        player.configure('abrFactory', () => abrManager);
      });
  
      it('fires "adaptation" event', async () => {
        const abrEnabled = new Promise((resolve) => {
          abrManager.enable.and.callFake(resolve);
        });
  
        await player.load('test:sintel_multi_lingual_multi_res_compiled');
  
        expect(abrManager.switchCallback).toBeTruthy();
        expect(abrManager.variants.length).toBeGreaterThan(1);
        expect(abrManager.chooseIndex).toBe(0);
  
        /** @type {shaka.test.Waiter} */
        const waiter = new shaka.test.Waiter(eventManager)
            .timeoutAfter(1).failOnTimeout(true);
  
        await waiter.waitForPromise(abrEnabled, 'AbrManager enabled');
  
        const p = waiter.waitForEvent(player, 'adaptation');
        abrManager.switchCallback(abrManager.variants[1]);
        await expectAsync(p).toBeResolved();
      });
  
      it('doesn\'t fire "adaptation" when not changing streams', async () => {
        const abrEnabled = new Promise((resolve) => {
          abrManager.enable.and.callFake(resolve);
        });
  
        await player.load('test:sintel_multi_lingual_multi_res_compiled');
  
        expect(abrManager.switchCallback).toBeTruthy();
  
        /** @type {shaka.test.Waiter} */
        const waiter = new shaka.test.Waiter(eventManager)
            .timeoutAfter(1).failOnTimeout(true);
  
        await waiter.waitForPromise(abrEnabled, 'AbrManager enabled');
  
        const p = waiter.waitForEvent(player, 'adaptation');
        for (let i = 0; i < 3; i++) {
          abrManager.switchCallback(abrManager.variants[abrManager.chooseIndex]);
        }
        await expectAsync(p).toBeRejected();  // Timeout
      });
    });  // describe('adaptation')
  });
  